Merge "MediaRouter: Fix dynamic group bug" into androidx-main
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
index 2369c2c..be85f72 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
@@ -116,6 +116,7 @@
}
// @exportToFramework:startStrip()
+
@Test
public void testSetSchema_dataClass() throws Exception {
mDb1.setSchema(
@@ -124,6 +125,7 @@
// @exportToFramework:endStrip()
// @exportToFramework:startStrip()
+
@Test
public void testGetSchema() throws Exception {
AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
@@ -192,6 +194,7 @@
}
// @exportToFramework:startStrip()
+
@Test
public void testPutDocuments_dataClass() throws Exception {
// Schema registration
@@ -452,6 +455,7 @@
}
// @exportToFramework:startStrip()
+
@Test
public void testGetDocuments_dataClass() throws Exception {
// Schema registration
@@ -557,6 +561,60 @@
}
@Test
+ public void testQuery_relevanceScoring() throws Exception {
+ // Schema registration
+ mDb1.setSchema(
+ new SetSchemaRequest.Builder()
+ .addSchema(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email1 =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("Mary had a little lamb")
+ .setBody("A little lamb, little lamb")
+ .build();
+ AppSearchEmail email2 =
+ new AppSearchEmail.Builder("uri2")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("I'm a little teapot")
+ .setBody("short and stout. Here is my handle, here is my spout.")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putDocuments(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocument(email1, email2).build()));
+
+ // Query for "little". It should match both emails.
+ SearchResults searchResults = mDb1.query("little", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // The email1 should be ranked higher because 'little' appears three times in email1 and
+ // only once in email2.
+ assertThat(documents).containsExactly(email1, email2).inOrder();
+
+ // Query for "little OR stout". It should match both emails.
+ searchResults = mDb1.query("little OR stout", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+ .build());
+ documents = convertSearchResultsToDocuments(searchResults);
+
+ // The email2 should be ranked higher because 'little' appears once and "stout", which is a
+ // rarer term, appears once. email1 only has the three 'little' appearances.
+ assertThat(documents).containsExactly(email2, email1).inOrder();
+ }
+
+ @Test
public void testQuery_typeFilter() throws Exception {
// Schema registration
AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
@@ -692,10 +750,23 @@
mDb1.setSchema(
new SetSchemaRequest.Builder()
.addSchema(AppSearchEmail.SCHEMA)
- .build()).get();
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
// Index two documents
- AppSearchEmail email1 =
+ AppSearchEmail email =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
@@ -704,56 +775,64 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- AppSearchEmail email2 =
- new AppSearchEmail.Builder("uri2")
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
- .setFrom("from@example.com")
- .setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
checkIsBatchResultSuccess(mDb1.putDocuments(
new PutDocumentsRequest.Builder()
- .addGenericDocument(email1, email2).build()));
+ .addGenericDocument(email, note).build()));
- // Query with type property paths {"Email", ["subject", "to"]}
+ // Query with type property paths {"Email", ["body", "to"]}
SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+ .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The two email documents should have been returned with only the "subject" and "to"
- // properties.
- AppSearchEmail expected1 =
- new AppSearchEmail.Builder("uri2")
- .setNamespace("namespace")
- .setCreationTimestampMillis(1000)
- .setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
- .build();
- AppSearchEmail expected2 =
+ // The email document should have been returned with only the "body" and "to"
+ // properties. The note document should have been returned with all of its properties.
+ AppSearchEmail expectedEmail =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
.build();
- assertThat(documents).containsExactly(expected1, expected2);
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
}
- // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
@Test
public void testQuery_projectionEmpty() throws Exception {
// Schema registration
mDb1.setSchema(
new SetSchemaRequest.Builder()
.addSchema(AppSearchEmail.SCHEMA)
- .build()).get();
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
// Index two documents
- AppSearchEmail email1 =
+ AppSearchEmail email =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
@@ -762,18 +841,15 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- AppSearchEmail email2 =
- new AppSearchEmail.Builder("uri2")
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
- .setFrom("from@example.com")
- .setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
checkIsBatchResultSuccess(mDb1.putDocuments(
new PutDocumentsRequest.Builder()
- .addGenericDocument(email1, email2).build()));
+ .addGenericDocument(email, note).build()));
// Query with type property paths {"Email", []}
SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
@@ -782,18 +858,20 @@
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The two email documents should have been returned without any properties.
- AppSearchEmail expected1 =
- new AppSearchEmail.Builder("uri2")
- .setNamespace("namespace")
- .setCreationTimestampMillis(1000)
- .build();
- AppSearchEmail expected2 =
+ // The email document should have been returned without any properties. The note document
+ // should have been returned with all of its properties.
+ AppSearchEmail expectedEmail =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
.build();
- assertThat(documents).containsExactly(expected1, expected2);
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
}
@Test
@@ -802,10 +880,23 @@
mDb1.setSchema(
new SetSchemaRequest.Builder()
.addSchema(AppSearchEmail.SCHEMA)
- .build()).get();
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
// Index two documents
- AppSearchEmail email1 =
+ AppSearchEmail email =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
@@ -814,44 +905,232 @@
.setSubject("testPut example")
.setBody("This is the body of the testPut email")
.build();
- AppSearchEmail email2 =
- new AppSearchEmail.Builder("uri2")
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
- .setFrom("from@example.com")
- .setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
- .setBody("This is the body of the testPut email")
- .build();
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
checkIsBatchResultSuccess(mDb1.putDocuments(
new PutDocumentsRequest.Builder()
- .addGenericDocument(email1, email2).build()));
+ .addGenericDocument(email, note).build()));
- // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
+ // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.addProjection("NonExistentType", Collections.emptyList())
- .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+ .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
- // The two email documents should have been returned with only the "subject" and "to"
- // properties.
- AppSearchEmail expected1 =
- new AppSearchEmail.Builder("uri2")
- .setNamespace("namespace")
- .setCreationTimestampMillis(1000)
- .setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
- .build();
- AppSearchEmail expected2 =
+ // The email document should have been returned with only the "body" and "to" properties.
+ // The note document should have been returned with all of its properties.
+ AppSearchEmail expectedEmail =
new AppSearchEmail.Builder("uri1")
.setNamespace("namespace")
.setCreationTimestampMillis(1000)
.setTo("to1@example.com", "to2@example.com")
- .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
.build();
- assertThat(documents).containsExactly(expected1, expected2);
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
+ }
+
+ @Test
+ public void testQuery_wildcardProjection() throws Exception {
+ // Schema registration
+ mDb1.setSchema(
+ new SetSchemaRequest.Builder()
+ .addSchema(AppSearchEmail.SCHEMA)
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putDocuments(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocument(email, note).build()));
+
+ // Query with type property paths {"*", ["body", "to"]}
+ SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // The email document should have been returned with only the "body" and "to"
+ // properties. The note document should have been returned with only the "body" property.
+ AppSearchEmail expectedEmail =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setTo("to1@example.com", "to2@example.com")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("body", "Note body").build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
+ }
+
+ @Test
+ public void testQuery_wildcardProjectionEmpty() throws Exception {
+ // Schema registration
+ mDb1.setSchema(
+ new SetSchemaRequest.Builder()
+ .addSchema(AppSearchEmail.SCHEMA)
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putDocuments(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocument(email, note).build()));
+
+ // Query with type property paths {"*", []}
+ SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // The email and note documents should have been returned without any properties.
+ AppSearchEmail expectedEmail =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .build();
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000).build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
+ }
+
+ @Test
+ public void testQuery_wildcardProjectionNonExistentType() throws Exception {
+ // Schema registration
+ mDb1.setSchema(
+ new SetSchemaRequest.Builder()
+ .addSchema(AppSearchEmail.SCHEMA)
+ .addSchema(new AppSearchSchema.Builder("Note")
+ .addProperty(new PropertyConfig.Builder("title")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .addProperty(new PropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setTokenizerType(
+ PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+ .build()).build()).get();
+
+ // Index two documents
+ AppSearchEmail email =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument note =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("title", "Note title")
+ .setPropertyString("body", "Note body").build();
+ checkIsBatchResultSuccess(mDb1.putDocuments(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocument(email, note).build()));
+
+ // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
+ SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .addProjection("NonExistentType", Collections.emptyList())
+ .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+ // The email document should have been returned with only the "body" and "to"
+ // properties. The note document should have been returned with only the "body" property.
+ AppSearchEmail expectedEmail =
+ new AppSearchEmail.Builder("uri1")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setTo("to1@example.com", "to2@example.com")
+ .setBody("This is the body of the testPut email")
+ .build();
+ GenericDocument expectedNote =
+ new GenericDocument.Builder<>("uri2", "Note")
+ .setNamespace("namespace")
+ .setCreationTimestampMillis(1000)
+ .setPropertyString("body", "Note body").build();
+ assertThat(documents).containsExactly(expectedNote, expectedEmail);
}
@Test
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 51eb7f5..e97d137 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -944,10 +944,13 @@
// Qualify the given schema types
for (TypePropertyMask typePropertyMask :
resultSpecBuilder.getTypePropertyMasksList()) {
- String qualifiedType = prefix + typePropertyMask.getSchemaType();
- if (existingSchemaTypes.contains(qualifiedType)) {
+ String unprefixedType = typePropertyMask.getSchemaType();
+ boolean isWildcard =
+ unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
+ String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
+ if (isWildcard || existingSchemaTypes.contains(prefixedType)) {
prefixedTypePropertyMasks.add(
- typePropertyMask.toBuilder().setSchemaType(qualifiedType).build());
+ typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
}
}
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
similarity index 95%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
rename to benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
index 8d2e9a2..e88aea4 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
@@ -18,7 +18,7 @@
import android.content.Intent
import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.JankMetric
+import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.MacrobenchmarkConfig
import androidx.benchmark.macro.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -36,7 +36,7 @@
@LargeTest
@SdkSuppress(minSdkVersion = 29)
@RunWith(Parameterized::class)
-class JankMetricValidation(
+class FrameTimingMetricValidation(
private val compilationMode: CompilationMode
) {
@get:Rule
@@ -54,7 +54,7 @@
fun start() {
val config = MacrobenchmarkConfig(
packageName = PACKAGE_NAME,
- metrics = listOf(JankMetric()),
+ metrics = listOf(FrameTimingMetric()),
compilationMode = compilationMode,
iterations = 10
)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
index e8c152e..bc58b13 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
@@ -48,4 +48,4 @@
intent.setPackage(TARGET_PACKAGE)
setupIntent(intent)
launchIntentAndWait(intent)
-}
\ No newline at end of file
+}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 2d211f9..50b0812 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -100,10 +100,20 @@
data class MacrobenchmarkConfig(
val packageName: String,
- val metrics: List<Metric>,
+ var metrics: List<Metric>,
val compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
val iterations: Int
-)
+) {
+ init {
+ val metricSet = metrics.toSet()
+ val hasStartupMetric = metricSet.any { it is StartupTimingMetric }
+ if (hasStartupMetric) {
+ val metrics = metrics.toMutableList()
+ metrics += PerfettoMetric()
+ this.metrics = metrics.toList()
+ }
+ }
+}
/**
* macrobenchmark test entrypoint, which doesn't depend on JUnit.
@@ -141,24 +151,23 @@
val metricResults = List(config.iterations) { iteration ->
setupBlock(scope, isFirstRun)
isFirstRun = false
- try {
- perfettoCollector.start()
- config.metrics.forEach {
- it.start()
+ perfettoCollector.captureTrace(uniqueName, iteration) { tracePath ->
+ try {
+ config.metrics.forEach {
+ it.start()
+ }
+ measureBlock(scope)
+ } finally {
+ config.metrics.forEach {
+ it.stop()
+ }
}
- measureBlock(scope)
- } finally {
- config.metrics.forEach {
- it.stop()
- }
- perfettoCollector.stop(uniqueName, iteration)
+ config.metrics
+ // capture list of Map<String,Long> per metric
+ .map { it.getMetrics(config.packageName, tracePath) }
+ // merge into one map
+ .reduce { sum, element -> sum + element }
}
-
- config.metrics
- // capture list of Map<String,Long> per metric
- .map { it.getMetrics(config.packageName) }
- // merge into one map
- .reduce { sum, element -> sum + element }
}.mergeToMetricResults()
InstrumentationResults.instrumentationReport {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
index aee2877..70aa835 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -16,6 +16,9 @@
package androidx.benchmark.macro
+import android.util.Log
+import androidx.benchmark.perfetto.PerfettoResultsParser.parseResult
+import androidx.benchmark.perfetto.PerfettoTraceParser
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.helpers.CpuUsageHelper
@@ -31,14 +34,13 @@
abstract fun start()
abstract fun stop()
-
/**
* After stopping, collect metrics
*
* TODO: takes package for package level filtering, but probably want a
* general config object coming into [start].
*/
- abstract fun getMetrics(packageName: String): Map<String, Long>
+ abstract fun getMetrics(packageName: String, tracePath: String): Map<String, Long>
}
class StartupTimingMetric : Metric() {
@@ -56,7 +58,7 @@
helper.stopCollecting()
}
- override fun getMetrics(packageName: String): Map<String, Long> {
+ override fun getMetrics(packageName: String, tracePath: String): Map<String, Long> {
return helper.getMetrics(packageName)
}
}
@@ -81,12 +83,12 @@
helper.stopCollecting()
}
- override fun getMetrics(packageName: String): Map<String, Long> {
+ override fun getMetrics(packageName: String, tracePath: String): Map<String, Long> {
return helper.metrics
}
}
-class JankMetric : Metric() {
+class FrameTimingMetric : Metric() {
private lateinit var packageName: String
private val helper = JankCollectionHelper()
@@ -146,7 +148,7 @@
"slow_bmp_upload" to "slowBitmapUploadFrameCount",
"slow_issue_draw_cmds" to "slowIssueDrawCommandsFrameCount",
"total_frames" to "totalFrameCount",
- "janky_frames_percent" to "jankyFramePercent",
+ "janky_frames_percent" to "jankyFramePercent"
)
/**
@@ -157,10 +159,10 @@
"frameTime90thPercentileMs",
"frameTime95thPercentileMs",
"frameTime99thPercentileMs",
- "totalFrameCount",
+ "totalFrameCount"
)
- override fun getMetrics(packageName: String): Map<String, Long> {
+ override fun getMetrics(packageName: String, tracePath: String): Map<String, Long> {
return helper.metrics
.map {
val prefix = "gfxinfo_${packageName}_"
@@ -181,6 +183,48 @@
}
/**
+ * Only does startup metrics now. Will need to expand scope.
+ */
+internal class PerfettoMetric : Metric() {
+ private lateinit var packageName: String
+ private lateinit var device: UiDevice
+ private lateinit var parser: PerfettoTraceParser
+
+ override fun configure(config: MacrobenchmarkConfig) {
+ packageName = config.packageName
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = instrumentation.device()
+ parser = PerfettoTraceParser()
+ }
+
+ override fun start() {
+ parser.copyTraceProcessorShell()
+ }
+
+ override fun stop() {
+ }
+
+ override fun getMetrics(packageName: String, tracePath: String): Map<String, Long> {
+ val path = parser.shellFile?.absolutePath
+ return if (path != null) {
+ // TODO: Construct `METRICS` based on the config.
+ val command = "$path --run-metric $METRICS $tracePath --metrics-output=json"
+ Log.d(TAG, "Executing command $command")
+ val json = device.executeShellCommand(command)
+ Log.d(TAG, "Trace Processor result \n\n $json")
+ parseResult(json, packageName)
+ } else {
+ emptyMap()
+ }
+ }
+
+ companion object {
+ private const val TAG = "PerfettoMetric"
+ private const val METRICS = "android_startup"
+ }
+}
+
+/**
* Not public, as this needs clarified metric names
*/
internal class TotalPssMetric : Metric() {
@@ -198,7 +242,7 @@
helper.stopCollecting()
}
- override fun getMetrics(packageName: String): Map<String, Long> {
+ override fun getMetrics(packageName: String, tracePath: String): Map<String, Long> {
return helper.metrics
}
}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
index 840dee5..39d26a4 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
@@ -19,6 +19,7 @@
import android.os.Build
import android.util.Log
import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.destinationPath
import androidx.benchmark.perfetto.reportAdditionalFileToCopy
@@ -27,14 +28,26 @@
*/
class PerfettoCaptureWrapper {
private var capture: PerfettoCapture? = null
-
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
capture = PerfettoCapture()
}
}
- fun start(): Boolean {
+ fun <T> captureTrace(
+ benchmarkName: String,
+ iteration: Int,
+ block: (String) -> T
+ ): T {
+ try {
+ start()
+ return block(PerfettoHelper.getPerfettoTmpOutputFilePath())
+ } finally {
+ stop(benchmarkName, iteration)
+ }
+ }
+
+ private fun start(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(TAG, "Recording perfetto trace")
capture?.start()
@@ -42,19 +55,14 @@
return true
}
- fun stop(benchmarkName: String, iteration: Int): Boolean {
+ private fun stop(benchmarkName: String, iteration: Int): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val iterString = iteration.toString().padStart(3, '0')
val traceName = "${benchmarkName}_iter$iterString.trace"
-
val destination = destinationPath(traceName).absolutePath
capture?.stop(destination)
reportAdditionalFileToCopy("perfetto_trace_$iterString", destination)
}
return true
}
-
- companion object {
- private const val TAG = "PerfettoCollector"
- }
}
diff --git a/benchmark/perfetto/build.gradle b/benchmark/perfetto/build.gradle
index 8210bdd..652a512 100644
--- a/benchmark/perfetto/build.gradle
+++ b/benchmark/perfetto/build.gradle
@@ -17,6 +17,7 @@
import static androidx.build.dependencies.DependenciesKt.*
import androidx.build.LibraryGroups
import androidx.build.Publish
+import androidx.build.SupportConfigKt
plugins {
id("AndroidXPlugin")
@@ -30,6 +31,12 @@
// lower minSdkVersion to enable optional usage, based on API level.
minSdkVersion 18
}
+ sourceSets {
+ main.assets.srcDirs += new File(
+ SupportConfigKt.getPrebuiltsRoot(project),
+ "androidx/traceprocessor/trace_processor_shell"
+ )
+ }
}
dependencies {
@@ -45,6 +52,7 @@
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
}
+
androidx {
name = "Android Benchmark - Perfetto"
publish = Publish.NONE
diff --git a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index 77e056a..1e95916 100644
--- a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -16,7 +16,6 @@
package androidx.benchmark.perfetto
-import android.content.Context
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
@@ -51,16 +50,13 @@
* TODO: provide configuration options
*/
fun start() {
- val context: Context = InstrumentationRegistry.getInstrumentation().context
-
+ val context = InstrumentationRegistry.getInstrumentation().context
// Write textproto asset to external files dir, so it can be read by shell
// TODO: use binary proto (which will also give us rooted 28 support)
val configBytes = context.resources.openRawResource(R.raw.trace_config).readBytes()
val textProtoFile = File(context.getExternalFilesDir(null), "trace_config.textproto")
-
try {
textProtoFile.writeBytes(configBytes)
-
// Start tracing
if (!helper.startCollecting(textProtoFile.absolutePath, true)) {
// TODO: move internal failures to be exceptions
@@ -83,4 +79,4 @@
throw IllegalStateException("Unable to store perfetto trace")
}
}
-}
\ No newline at end of file
+}
diff --git a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.java b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.java
index 6060a4d..fa60360 100644
--- a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.java
+++ b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.java
@@ -19,7 +19,10 @@
import android.os.SystemClock;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
@@ -31,9 +34,12 @@
/**
* PerfettoHelper is used to start and stop the perfetto tracing and move the
* output perfetto trace file to destination folder.
+ *
+ * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(28)
-class PerfettoHelper {
+public class PerfettoHelper {
private static final String LOG_TAG = PerfettoHelper.class.getSimpleName();
// Command to start the perfetto tracing in the background.
// perfetto -b -c /data/misc/perfetto-traces/trace_config.pb -o
@@ -51,7 +57,8 @@
// Check if perfetto is stopped every 5 secs.
private static final long PERFETTO_KILL_WAIT_TIME = 5000;
- private UiDevice mUIDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ private final UiDevice mUIDevice =
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
/**
* Start the perfetto tracing in background using the given config file.
@@ -64,7 +71,7 @@
* @param isTextProtoConfig true if the config file is textproto format otherwise false.
* @return true if trace collection started successfully otherwise return false.
*/
- public boolean startCollecting(String configFilePath, boolean isTextProtoConfig) {
+ public boolean startCollecting(@Nullable String configFilePath, boolean isTextProtoConfig) {
if (configFilePath == null || configFilePath.isEmpty()) {
Log.e(LOG_TAG, "Perfetto config file name is null or empty.");
return false;
@@ -115,7 +122,7 @@
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace collection is successful otherwise false.
*/
- public boolean stopCollecting(long waitTimeInMsecs, String destinationFile) {
+ public boolean stopCollecting(long waitTimeInMsecs, @NonNull String destinationFile) {
// Wait for the dump interval before stopping the trace.
Log.i(LOG_TAG, String.format(
"Waiting for %d msecs before stopping perfetto.", waitTimeInMsecs));
@@ -190,13 +197,22 @@
}
/**
+ * @return the {@link String} path to the temporary output file used to store the trace file
+ * during collection.
+ */
+ @NonNull
+ public static String getPerfettoTmpOutputFilePath() {
+ return PERFETTO_TMP_OUTPUT_FILE;
+ }
+
+ /**
* Copy the temporary perfetto trace output file from /data/misc/perfetto-traces/ to given
* destinationFile.
*
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace file copied successfully otherwise false.
*/
- private boolean copyFileOutput(String destinationFile) {
+ private boolean copyFileOutput(@NonNull String destinationFile) {
Path path = Paths.get(destinationFile);
String destDirectory = path.getParent().toString();
// Check if the directory already exists
diff --git a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoResultsParser.kt b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoResultsParser.kt
new file mode 100644
index 0000000..dc0d611
--- /dev/null
+++ b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoResultsParser.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.benchmark.perfetto
+
+import org.json.JSONArray
+import org.json.JSONObject
+
+object PerfettoResultsParser {
+ fun parseResult(jsonTrace: String, packageName: String): Map<String, Long> {
+ val map = mutableMapOf<String, Long>()
+ val json = JSONObject(jsonTrace)
+ val androidStartup = json.optJSONObject(ANDROID_STARTUP)
+ if (androidStartup != null) {
+ val startup = androidStartup.optJSONArray(STARTUP)
+ if (startup != null && startup.length() > 0) {
+ parseStartupResult(startup, packageName, map)
+ }
+ }
+ return map
+ }
+
+ private fun parseStartupResult(
+ json: JSONArray,
+ packageName: String,
+ map: MutableMap<String, Long>
+ ) {
+ val length = json.length()
+ for (i in 0 until length) {
+ val startupResult = json.getJSONObject(i)
+ val targetPackageName = startupResult.optString(PACKAGE_NAME)
+ if (packageName == targetPackageName) {
+ val firstFrameMetric = startupResult.optJSONObject(TO_FIRST_FRAME)
+ if (firstFrameMetric != null) {
+ val duration = firstFrameMetric.optDouble(DUR_MS, 0.0)
+ map[STARTUP_MS] = duration.toLong()
+ }
+ }
+ }
+ }
+
+ private const val ANDROID_STARTUP = "android_startup"
+ private const val STARTUP = "startup"
+ private const val PACKAGE_NAME = "package_name"
+ private const val TO_FIRST_FRAME = "to_first_frame"
+ private const val DUR_MS = "dur_ms"
+ private const val TIME_ACTIVITY_START = "time_activity_start"
+ private const val TIME_ACTIVITY_RESUME = "time_activity_resume"
+
+ // Metric Keys
+ private const val STARTUP_MS = "perfetto_startupMs"
+}
diff --git a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoTraceParser.kt b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoTraceParser.kt
new file mode 100644
index 0000000..785be51
--- /dev/null
+++ b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoTraceParser.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.benchmark.perfetto
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+
+/**
+ * Enables parsing perfetto traces on-device on Q+ devices.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+class PerfettoTraceParser {
+
+ /**
+ * The actual [File] path to the `trace_processor_shell`.
+ */
+ var shellFile: File? = null
+
+ /**
+ * Copies `trace_processor_shell` and enables parsing of the perfetto trace files.
+ */
+ fun copyTraceProcessorShell() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val context: Context = instrumentation.context
+ shellFile = File(context.cacheDir, "trace_processor_shell")
+ // TODO: support other ABIs
+ if (Build.SUPPORTED_64_BIT_ABIS.isEmpty()) {
+ throw IllegalStateException("Unsupported ABI")
+ }
+ // Write the trace_processor_shell to the external directory so we can process
+ // perfetto metrics on device.
+ val shellFile = shellFile
+ if (shellFile != null && !shellFile.exists()) {
+ val created = shellFile.createNewFile()
+ shellFile.setWritable(true)
+ shellFile.setExecutable(true, false)
+ if (!created) {
+ throw IllegalStateException("Unable to create new file $shellFile")
+ }
+ shellFile.outputStream().use {
+ // TODO: Copy the file based on the ABI
+ context.assets.open("trace_processor_shell_aarch64").copyTo(it)
+ }
+ }
+ }
+}
diff --git a/biometric/biometric-ktx/api/current.txt b/biometric/biometric-ktx/api/current.txt
index d2293f0..1342c5a8 100644
--- a/biometric/biometric-ktx/api/current.txt
+++ b/biometric/biometric-ktx/api/current.txt
@@ -1,22 +1,46 @@
// Signature format: 4.0
package androidx.biometric.auth {
+ public final class AuthPromptErrorException extends java.lang.Exception {
+ ctor public AuthPromptErrorException(int errorCode, CharSequence errorMessage);
+ method public int getErrorCode();
+ method public CharSequence getErrorMessage();
+ property public final int errorCode;
+ property public final CharSequence errorMessage;
+ }
+
+ public final class AuthPromptFailureException extends java.lang.Exception {
+ ctor public AuthPromptFailureException();
+ }
+
public final class Class2BiometricAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class2BiometricOrCredentialAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class3BiometricAuthExtensionsKt {
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
}
public final class Class3BiometricOrCredentialAuthExtensionsKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
diff --git a/biometric/biometric-ktx/api/public_plus_experimental_current.txt b/biometric/biometric-ktx/api/public_plus_experimental_current.txt
index d2293f0..1342c5a8 100644
--- a/biometric/biometric-ktx/api/public_plus_experimental_current.txt
+++ b/biometric/biometric-ktx/api/public_plus_experimental_current.txt
@@ -1,22 +1,46 @@
// Signature format: 4.0
package androidx.biometric.auth {
+ public final class AuthPromptErrorException extends java.lang.Exception {
+ ctor public AuthPromptErrorException(int errorCode, CharSequence errorMessage);
+ method public int getErrorCode();
+ method public CharSequence getErrorMessage();
+ property public final int errorCode;
+ property public final CharSequence errorMessage;
+ }
+
+ public final class AuthPromptFailureException extends java.lang.Exception {
+ ctor public AuthPromptFailureException();
+ }
+
public final class Class2BiometricAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class2BiometricOrCredentialAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class3BiometricAuthExtensionsKt {
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
}
public final class Class3BiometricOrCredentialAuthExtensionsKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
diff --git a/biometric/biometric-ktx/api/restricted_current.txt b/biometric/biometric-ktx/api/restricted_current.txt
index d2293f0..1342c5a8 100644
--- a/biometric/biometric-ktx/api/restricted_current.txt
+++ b/biometric/biometric-ktx/api/restricted_current.txt
@@ -1,22 +1,46 @@
// Signature format: 4.0
package androidx.biometric.auth {
+ public final class AuthPromptErrorException extends java.lang.Exception {
+ ctor public AuthPromptErrorException(int errorCode, CharSequence errorMessage);
+ method public int getErrorCode();
+ method public CharSequence getErrorMessage();
+ property public final int errorCode;
+ property public final CharSequence errorMessage;
+ }
+
+ public final class AuthPromptFailureException extends java.lang.Exception {
+ ctor public AuthPromptFailureException();
+ }
+
public final class Class2BiometricAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class2BiometricOrCredentialAuthExtensionsKt {
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
public final class Class3BiometricAuthExtensionsKt {
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass3BiometricAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
+ method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
}
public final class Class3BiometricOrCredentialAuthExtensionsKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult> p);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
}
diff --git a/biometric/biometric-ktx/build.gradle b/biometric/biometric-ktx/build.gradle
index 570ffa1..4dc85dc 100755
--- a/biometric/biometric-ktx/build.gradle
+++ b/biometric/biometric-ktx/build.gradle
@@ -14,10 +14,13 @@
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
+
import androidx.build.LibraryGroups
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.getKOTLIN_COROUTINES_CORE
+import static androidx.build.dependencies.DependenciesKt.getKOTLIN_STDLIB
+
plugins {
id("AndroidXPlugin")
@@ -27,6 +30,7 @@
dependencies {
api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES_CORE)
api(project(":biometric:biometric"))
}
@@ -36,4 +40,4 @@
mavenGroup = LibraryGroups.BIOMETRIC
inceptionYear = "2020"
description = "Kotlin extensions for the Biometric Library."
-}
\ No newline at end of file
+}
diff --git a/biometric/biometric-ktx/samples/build.gradle b/biometric/biometric-ktx/samples/build.gradle
new file mode 100644
index 0000000..bf95a76
--- /dev/null
+++ b/biometric/biometric-ktx/samples/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+dependencies {
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":biometric:biometric-ktx"))
+}
+
+androidx {
+ name = "AndroidX Biometric Samples"
+ type = LibraryType.SAMPLES
+ mavenGroup = LibraryGroups.BIOMETRIC
+ inceptionYear = "2021"
+ description = "Contains the sample code for the AndroidX Biometric library"
+}
diff --git a/biometric/biometric-ktx/samples/lint-baseline.xml b/biometric/biometric-ktx/samples/lint-baseline.xml
new file mode 100644
index 0000000..8f1aa4b
--- /dev/null
+++ b/biometric/biometric-ktx/samples/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+
+</issues>
diff --git a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml b/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1dacecc
--- /dev/null
+++ b/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.biometric.samples"/>
diff --git a/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt b/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt
new file mode 100644
index 0000000..4b7aeed
--- /dev/null
+++ b/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.biometric.samples.auth
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import androidx.annotation.Sampled
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.auth.AuthPromptErrorException
+import androidx.biometric.auth.AuthPromptFailureException
+import androidx.biometric.auth.AuthPromptHost
+import androidx.biometric.auth.Class2BiometricAuthPrompt
+import androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt
+import androidx.biometric.auth.Class3BiometricAuthPrompt
+import androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt
+import androidx.biometric.auth.authenticate
+import androidx.fragment.app.Fragment
+import java.nio.charset.Charset
+import java.security.KeyStore
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+
+// Stubbed definitions for samples
+private const val KEYSTORE_INSTANCE = "AndroidKeyStore"
+private const val KEY_NAME = "mySecretKey"
+private const val title = ""
+private const val subtitle = ""
+private const val description = ""
+private const val negativeButtonText = ""
+private fun sendEncryptedPayload(payload: ByteArray?): ByteArray? = payload
+
+@Sampled
+suspend fun Fragment.class2BiometricAuth() {
+ val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
+
+ // Construct AuthPrompt with localized Strings to be displayed to UI.
+ val authPrompt = Class2BiometricAuthPrompt.Builder(title, negativeButtonText).apply {
+ setSubtitle(subtitle)
+ setDescription(description)
+ setConfirmationRequired(true)
+ }.build()
+
+ try {
+ val authResult = authPrompt.authenticate(AuthPromptHost(this))
+
+ // Encrypt a payload using the result of crypto-based auth.
+ val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
+
+ // Use the encrypted payload somewhere interesting.
+ sendEncryptedPayload(encryptedPayload)
+ } catch (e: AuthPromptErrorException) {
+ // Handle irrecoverable error during authentication.
+ // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
+ // androidx.biometric.BiometricPrompt.AuthenticationError.
+ } catch (e: AuthPromptFailureException) {
+ // Handle auth failure due biometric credentials being rejected.
+ }
+}
+
+@Sampled
+suspend fun Fragment.class2BiometricOrCredentialAuth() {
+ val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
+
+ // Construct AuthPrompt with localized Strings to be displayed to UI.
+ val authPrompt = Class2BiometricOrCredentialAuthPrompt.Builder(title).apply {
+ setSubtitle(subtitle)
+ setDescription(description)
+ setConfirmationRequired(true)
+ }.build()
+
+ try {
+ val authResult = authPrompt.authenticate(AuthPromptHost(this))
+
+ // Encrypt a payload using the result of crypto-based auth.
+ val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
+
+ // Use the encrypted payload somewhere interesting.
+ sendEncryptedPayload(encryptedPayload)
+ } catch (e: AuthPromptErrorException) {
+ // Handle irrecoverable error during authentication.
+ // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
+ // androidx.biometric.BiometricPrompt.AuthenticationError.
+ } catch (e: AuthPromptFailureException) {
+ // Handle auth failure due biometric credentials being rejected.
+ }
+}
+
+@Sampled
+@Suppress("UnsafeNewApiCall", "NewApi")
+suspend fun Fragment.class3BiometricAuth() {
+ // To use Class3 authentication, we need to create a CryptoObject.
+ // First create a spec for the key to be generated.
+ val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ val keySpec = KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose).apply {
+ setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ setUserAuthenticationRequired(true)
+
+ // Require authentication for each use of the key.
+ val timeout = 0
+ // Set the key type according to the allowed auth types.
+ val keyType =
+ KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
+ setUserAuthenticationParameters(timeout, keyType)
+ }.build()
+
+ // Generate and store the key in the Android keystore.
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
+ init(keySpec)
+ generateKey()
+ }
+
+ // Prepare the crypto object to use for authentication.
+ val cipher = Cipher.getInstance(
+ "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
+ KeyProperties.ENCRYPTION_PADDING_PKCS7
+ ).apply {
+ val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
+ init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
+ }
+
+ val cryptoObject = BiometricPrompt.CryptoObject(cipher)
+ val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
+
+ // Construct AuthPrompt with localized Strings to be displayed to UI.
+ val authPrompt = Class3BiometricAuthPrompt.Builder(title, negativeButtonText).apply {
+ setSubtitle(subtitle)
+ setDescription(description)
+ setConfirmationRequired(true)
+ }.build()
+
+ try {
+ val authResult = authPrompt.authenticate(AuthPromptHost(this), cryptoObject)
+
+ // Encrypt a payload using the result of crypto-based auth.
+ val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
+
+ // Use the encrypted payload somewhere interesting.
+ sendEncryptedPayload(encryptedPayload)
+ } catch (e: AuthPromptErrorException) {
+ // Handle irrecoverable error during authentication.
+ // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
+ // androidx.biometric.BiometricPrompt.AuthenticationError.
+ } catch (e: AuthPromptFailureException) {
+ // Handle auth failure due biometric credentials being rejected.
+ }
+}
+
+@Sampled
+@Suppress("UnsafeNewApiCall", "NewApi")
+suspend fun Fragment.class3BiometricOrCredentialAuth() {
+ // To use Class3 authentication, we need to create a CryptoObject.
+ // First create a spec for the key to be generated.
+ val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ val keySpec = KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose).apply {
+ setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ setUserAuthenticationRequired(true)
+
+ // Require authentication for each use of the key.
+ val timeout = 0
+ // Set the key type according to the allowed auth types.
+ val keyType =
+ KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
+ setUserAuthenticationParameters(timeout, keyType)
+ }.build()
+
+ // Generate and store the key in the Android keystore.
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
+ init(keySpec)
+ generateKey()
+ }
+
+ // Prepare the crypto object to use for authentication.
+ val cipher = Cipher.getInstance(
+ "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
+ KeyProperties.ENCRYPTION_PADDING_PKCS7
+ ).apply {
+ val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
+ init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
+ }
+
+ val cryptoObject = BiometricPrompt.CryptoObject(cipher)
+ val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
+
+ // Construct AuthPrompt with localized Strings to be displayed to UI.
+ val authPrompt = Class3BiometricOrCredentialAuthPrompt.Builder(title).apply {
+ setSubtitle(subtitle)
+ setDescription(description)
+ setConfirmationRequired(true)
+ }.build()
+
+ try {
+ val authResult = authPrompt.authenticate(AuthPromptHost(this), cryptoObject)
+
+ // Encrypt a payload using the result of crypto-based auth.
+ val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
+
+ // Use the encrypted payload somewhere interesting.
+ sendEncryptedPayload(encryptedPayload)
+ } catch (e: AuthPromptErrorException) {
+ // Handle irrecoverable error during authentication.
+ // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
+ // androidx.biometric.BiometricPrompt.AuthenticationError.
+ } catch (e: AuthPromptFailureException) {
+ // Handle auth failure due biometric credentials being rejected.
+ }
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt
new file mode 100644
index 0000000..c1daef7
--- /dev/null
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.biometric.auth
+/**
+ * Thrown when an unrecoverable error has been encountered and authentication has stopped.
+ *
+ * @param errorCode An integer ID associated with the error.
+ * @param errorMessage A human-readable string that describes the error.
+ */
+public class AuthPromptErrorException(
+ public val errorCode: Int,
+ public val errorMessage: CharSequence
+) : Exception(errorMessage.toString())
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
similarity index 68%
copy from navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
copy to biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
index f9f0fbb..6a1c34e 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package androidx.navigation;
+package androidx.biometric.auth
/**
- * An interface marking generated Args classes.
+ * Thrown when an authentication attempt by the user has been rejected, e.g., the user's
+ * biometrics were not recognized.
*/
-public interface NavArgs {
-}
+public class AuthPromptFailureException : Exception()
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt
index 4d67b8c..4c43052 100755
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt
@@ -15,11 +15,44 @@
*/
package androidx.biometric.auth
+import androidx.biometric.BiometricPrompt.AuthenticationResult
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.Executor
/**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricAuthPrompt.authenticate(AuthPromptHost, AuthPromptCallback)
+ *
+ * @sample androidx.biometric.samples.auth.class2BiometricAuth
+ */
+public suspend fun Class2BiometricAuthPrompt.authenticate(
+ host: AuthPromptHost,
+): AuthenticationResult {
+ return suspendCancellableCoroutine { continuation ->
+ val authPrompt = startAuthentication(
+ host,
+ Runnable::run,
+ CoroutineAuthPromptCallback(continuation)
+ )
+
+ continuation.invokeOnCancellation {
+ authPrompt.cancelAuthentication()
+ }
+ }
+}
+
+/**
* Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
*
* Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
@@ -68,6 +101,44 @@
* @param subtitle An optional subtitle to be displayed on the prompt.
* @param description An optional description to be displayed on the prompt.
* @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricAuthPrompt
+ */
+public suspend fun FragmentActivity.authenticateWithClass2Biometrics(
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass2BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this))
+}
+
+/**
+ * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
+ *
+ * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
+ * will also be accepted.
+ *
+ * @param title The title to be displayed on the prompt.
+ * @param negativeButtonText The label for the negative button on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
* @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
* @param callback The object that will receive and process authentication events.
* @return An [AuthPrompt] handle to the shown prompt.
@@ -96,6 +167,61 @@
}
/**
+ * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
+ *
+ * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
+ * will also be accepted.
+ *
+ * @param title The title to be displayed on the prompt.
+ * @param negativeButtonText The label for the negative button on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricAuthPrompt
+ */
+public suspend fun Fragment.authenticateWithClass2Biometrics(
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass2BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this))
+}
+
+/**
+ * Creates a [Class2BiometricAuthPrompt] with the given parameters.
+ */
+private fun buildClass2BiometricAuthPrompt(
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): Class2BiometricAuthPrompt = Class2BiometricAuthPrompt.Builder(title, negativeButtonText)
+ .apply {
+ subtitle?.let { setSubtitle(it) }
+ description?.let { setDescription(it) }
+ setConfirmationRequired(confirmationRequired)
+ }
+ .build()
+
+/**
* Creates a [Class2BiometricAuthPrompt] with the given parameters and starts authentication.
*/
private fun startClass2BiometricAuthenticationInternal(
@@ -108,15 +234,17 @@
executor: Executor? = null,
callback: AuthPromptCallback
): AuthPrompt {
- val prompt = Class2BiometricAuthPrompt.Builder(title, negativeButtonText).apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }.build()
+ val prompt = buildClass2BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired
+ )
return if (executor == null) {
prompt.startAuthentication(host, callback)
} else {
prompt.startAuthentication(host, executor, callback)
}
-}
\ No newline at end of file
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt
index 0b3802f..8c772e3 100755
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt
@@ -15,11 +15,47 @@
*/
package androidx.biometric.auth
+import androidx.biometric.BiometricPrompt.AuthenticationResult
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.Executor
/**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricOrCredentialAuthPrompt.authenticate(
+ * AuthPromptHost,
+ * AuthPromptCallback
+ * )
+ *
+ * @sample androidx.biometric.samples.auth.class2BiometricOrCredentialAuth
+ */
+public suspend fun Class2BiometricOrCredentialAuthPrompt.authenticate(
+ host: AuthPromptHost,
+): AuthenticationResult {
+ return suspendCancellableCoroutine { continuation ->
+ val authPrompt = startAuthentication(
+ host,
+ Runnable::run,
+ CoroutineAuthPromptCallback(continuation)
+ )
+
+ continuation.invokeOnCancellation {
+ authPrompt.cancelAuthentication()
+ }
+ }
+}
+
+/**
* Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
* or the screen lock credential (i.e. PIN, pattern, or password) for the device.
*
@@ -66,6 +102,42 @@
* @param subtitle An optional subtitle to be displayed on the prompt.
* @param description An optional description to be displayed on the prompt.
* @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricOrCredentialAuthPrompt
+ */
+public suspend fun FragmentActivity.authenticateWithClass2BiometricsOrCredentials(
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass2BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this))
+}
+
+/**
+ * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
+ * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
+ *
+ * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
+ * will also be accepted.
+ *
+ * @param title The title to be displayed on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
* @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
* @param callback The object that will receive and process authentication events.
* @return An [AuthPrompt] handle to the shown prompt.
@@ -92,6 +164,57 @@
}
/**
+ * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
+ * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
+ *
+ * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
+ * will also be accepted.
+ *
+ * @param title The title to be displayed on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ * @return An [AuthPrompt] handle to the shown prompt.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class2BiometricOrCredentialAuthPrompt
+ */
+public suspend fun Fragment.authenticateWithClass2BiometricsOrCredentials(
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass2BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this))
+}
+
+/**
+ * Creates a [Class2BiometricOrCredentialAuthPrompt] with the given parameters.
+ */
+private fun buildClass2BiometricOrCredentialAuthPrompt(
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): Class2BiometricOrCredentialAuthPrompt = Class2BiometricOrCredentialAuthPrompt.Builder(title)
+ .apply {
+ subtitle?.let { setSubtitle(it) }
+ description?.let { setDescription(it) }
+ setConfirmationRequired(confirmationRequired)
+ }
+ .build()
+
+/**
* Creates a [Class2BiometricOrCredentialAuthPrompt] with the given parameters and starts
* authentication.
*/
@@ -104,15 +227,16 @@
executor: Executor? = null,
callback: AuthPromptCallback
): AuthPrompt {
- val prompt = Class2BiometricOrCredentialAuthPrompt.Builder(title).apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }.build()
+ val prompt = buildClass2BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
return if (executor == null) {
prompt.startAuthentication(host, callback)
} else {
prompt.startAuthentication(host, executor, callback)
}
-}
\ No newline at end of file
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt
index cd7a639..0e24d45 100755
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt
@@ -15,12 +15,48 @@
*/
package androidx.biometric.auth
-import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AuthenticationResult
+import androidx.biometric.BiometricPrompt.CryptoObject
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.Executor
/**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ * @param crypto A cryptographic object to be associated with this authentication.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricAuthPrompt.authenticate(AuthPromptHost, AuthPromptCallback)
+ *
+ * @sample androidx.biometric.samples.auth.class3BiometricAuth
+ */
+public suspend fun Class3BiometricAuthPrompt.authenticate(
+ host: AuthPromptHost,
+ crypto: CryptoObject?,
+): AuthenticationResult {
+ return suspendCancellableCoroutine { continuation ->
+ val authPrompt = startAuthentication(
+ host,
+ crypto,
+ Runnable::run,
+ CoroutineAuthPromptCallback(continuation)
+ )
+
+ continuation.invokeOnCancellation {
+ authPrompt.cancelAuthentication()
+ }
+ }
+}
+
+/**
* Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
*
* @param crypto A cryptographic object to be associated with this authentication.
@@ -35,8 +71,8 @@
*
* @see Class3BiometricAuthPrompt
*/
-public fun FragmentActivity.startClass3BiometricAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
+public fun FragmentActivity.authenticateWithClass3Biometrics(
+ crypto: CryptoObject?,
title: CharSequence,
negativeButtonText: CharSequence,
subtitle: CharSequence? = null,
@@ -67,14 +103,51 @@
* @param subtitle An optional subtitle to be displayed on the prompt.
* @param description An optional description to be displayed on the prompt.
* @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricAuthPrompt
+ */
+public suspend fun FragmentActivity.authenticateWithClass3Biometrics(
+ crypto: CryptoObject?,
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass3BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this), crypto)
+}
+
+/**
+ * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
+ *
+ * @param crypto A cryptographic object to be associated with this authentication.
+ * @param title The title to be displayed on the prompt.
+ * @param negativeButtonText The label for the negative button on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
* @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
* @param callback The object that will receive and process authentication events.
* @return An [AuthPrompt] handle to the shown prompt.
*
* @see Class3BiometricAuthPrompt
*/
-public fun Fragment.startClass3BiometricAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
+public fun Fragment.authenticateWithClass3Biometrics(
+ crypto: CryptoObject?,
title: CharSequence,
negativeButtonText: CharSequence,
subtitle: CharSequence? = null,
@@ -97,11 +170,65 @@
}
/**
+ * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
+ *
+ * @param crypto A cryptographic object to be associated with this authentication.
+ * @param title The title to be displayed on the prompt.
+ * @param negativeButtonText The label for the negative button on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricAuthPrompt
+ */
+public suspend fun Fragment.authenticateWithClass3Biometrics(
+ crypto: CryptoObject?,
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass3BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this), crypto)
+}
+
+/**
+ * Creates a [Class3BiometricAuthPrompt] with the given parameters.
+ */
+private fun buildClass3BiometricAuthPrompt(
+ title: CharSequence,
+ negativeButtonText: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): Class3BiometricAuthPrompt = Class3BiometricAuthPrompt.Builder(title, negativeButtonText)
+ .apply {
+ subtitle?.let { setSubtitle(it) }
+ description?.let { setDescription(it) }
+ setConfirmationRequired(confirmationRequired)
+ }
+ .build()
+
+/**
* Creates a [Class3BiometricAuthPrompt] with the given parameters and starts authentication.
*/
private fun startClass3BiometricAuthenticationInternal(
host: AuthPromptHost,
- crypto: BiometricPrompt.CryptoObject?,
+ crypto: CryptoObject?,
title: CharSequence,
negativeButtonText: CharSequence,
subtitle: CharSequence? = null,
@@ -110,15 +237,17 @@
executor: Executor? = null,
callback: AuthPromptCallback
): AuthPrompt {
- val prompt = Class3BiometricAuthPrompt.Builder(title, negativeButtonText).apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }.build()
+ val prompt = buildClass3BiometricAuthPrompt(
+ title,
+ negativeButtonText,
+ subtitle,
+ description,
+ confirmationRequired
+ )
return if (executor == null) {
prompt.startAuthentication(host, crypto, callback)
} else {
prompt.startAuthentication(host, crypto, executor, callback)
}
-}
\ No newline at end of file
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt
index 1376e36..ec27ec6 100755
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt
@@ -17,12 +17,52 @@
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AuthenticationResult
+import androidx.biometric.BiometricPrompt.CryptoObject
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.Executor
/**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ * @param crypto A cryptographic object to be associated with this authentication.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricOrCredentialAuthPrompt.authenticate(
+ * AuthPromptHost,
+ * AuthPromptCallback
+ * )
+ *
+ * @sample androidx.biometric.samples.auth.class3BiometricOrCredentialAuth
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public suspend fun Class3BiometricOrCredentialAuthPrompt.authenticate(
+ host: AuthPromptHost,
+ crypto: CryptoObject?,
+): AuthenticationResult {
+ return suspendCancellableCoroutine { continuation ->
+ val authPrompt = startAuthentication(
+ host,
+ crypto,
+ Runnable::run,
+ CoroutineAuthPromptCallback(continuation)
+ )
+
+ continuation.invokeOnCancellation {
+ authPrompt.cancelAuthentication()
+ }
+ }
+}
+
+/**
* Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
* or the screen lock credential (i.e. PIN, pattern, or password) for the device.
*
@@ -39,7 +79,7 @@
*/
@RequiresApi(Build.VERSION_CODES.R)
public fun FragmentActivity.startClass3BiometricOrCredentialAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
+ crypto: CryptoObject?,
title: CharSequence,
subtitle: CharSequence? = null,
description: CharSequence? = null,
@@ -68,6 +108,42 @@
* @param subtitle An optional subtitle to be displayed on the prompt.
* @param description An optional description to be displayed on the prompt.
* @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricOrCredentialAuthPrompt
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public suspend fun FragmentActivity.authenticateWithClass3BiometricsOrCredentials(
+ crypto: CryptoObject?,
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass3BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this), crypto)
+}
+
+/**
+ * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
+ * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
+ *
+ * @param crypto A cryptographic object to be associated with this authentication.
+ * @param title The title to be displayed on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
* @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
* @param callback The object that will receive and process authentication events.
* @return An [AuthPrompt] handle to the shown prompt.
@@ -76,7 +152,7 @@
*/
@RequiresApi(Build.VERSION_CODES.R)
public fun Fragment.startClass3BiometricOrCredentialAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
+ crypto: CryptoObject?,
title: CharSequence,
subtitle: CharSequence? = null,
description: CharSequence? = null,
@@ -97,13 +173,65 @@
}
/**
+ * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
+ * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
+ *
+ * @param crypto A cryptographic object to be associated with this authentication.
+ * @param title The title to be displayed on the prompt.
+ * @param subtitle An optional subtitle to be displayed on the prompt.
+ * @param description An optional description to be displayed on the prompt.
+ * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see Class3BiometricOrCredentialAuthPrompt
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public suspend fun Fragment.authenticateWithClass3BiometricsOrCredentials(
+ crypto: CryptoObject?,
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): AuthenticationResult {
+ val authPrompt = buildClass3BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired,
+ )
+
+ return authPrompt.authenticate(AuthPromptHost(this), crypto)
+}
+
+/**
+ * Creates a [Class3BiometricOrCredentialAuthPrompt] with the given parameters.
+ */
+private fun buildClass3BiometricOrCredentialAuthPrompt(
+ title: CharSequence,
+ subtitle: CharSequence? = null,
+ description: CharSequence? = null,
+ confirmationRequired: Boolean = true,
+): Class3BiometricOrCredentialAuthPrompt = Class3BiometricOrCredentialAuthPrompt.Builder(title)
+ .apply {
+ subtitle?.let { setSubtitle(it) }
+ description?.let { setDescription(it) }
+ setConfirmationRequired(confirmationRequired)
+ }
+ .build()
+
+/**
* Creates a [Class3BiometricOrCredentialAuthPrompt] with the given parameters and starts
* authentication.
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun startClass3BiometricOrCredentialAuthenticationInternal(
host: AuthPromptHost,
- crypto: BiometricPrompt.CryptoObject?,
+ crypto: CryptoObject?,
title: CharSequence,
subtitle: CharSequence? = null,
description: CharSequence? = null,
@@ -111,15 +239,16 @@
executor: Executor? = null,
callback: AuthPromptCallback
): AuthPrompt {
- val prompt = Class3BiometricOrCredentialAuthPrompt.Builder(title).apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }.build()
+ val prompt = buildClass3BiometricOrCredentialAuthPrompt(
+ title,
+ subtitle,
+ description,
+ confirmationRequired
+ )
return if (executor == null) {
prompt.startAuthentication(host, crypto, callback)
} else {
prompt.startAuthentication(host, crypto, executor, callback)
}
-}
\ No newline at end of file
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt
new file mode 100644
index 0000000..fb6de45
--- /dev/null
+++ b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.biometric.auth
+
+import androidx.biometric.BiometricPrompt.AuthenticationResult
+import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.CancellableContinuation
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Implementation of [AuthPromptCallback] used to transform callback results for coroutine APIs.
+ */
+internal class CoroutineAuthPromptCallback(
+ private val continuation: CancellableContinuation<AuthenticationResult>
+) : AuthPromptCallback() {
+ override fun onAuthenticationError(
+ activity: FragmentActivity?,
+ errorCode: Int,
+ errString: CharSequence
+ ) {
+ continuation.resumeWithException(AuthPromptErrorException(errorCode, errString))
+ }
+
+ override fun onAuthenticationSucceeded(
+ activity: FragmentActivity?,
+ result: AuthenticationResult
+ ) {
+ continuation.resumeWith(Result.success(result))
+ }
+
+ override fun onAuthenticationFailed(activity: FragmentActivity?) {
+ continuation.resumeWithException(AuthPromptFailureException())
+ }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricManager.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricManager.java
index 4f83a98..c126640 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricManager.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricManager.java
@@ -371,14 +371,12 @@
return BIOMETRIC_ERROR_NO_HARDWARE;
}
- // No authenticators are enrolled if the device is not secured.
- if (!mInjector.isDeviceSecuredWithCredential()) {
- return BIOMETRIC_ERROR_NONE_ENROLLED;
- }
-
- // Credential authentication is always possible if the device is secured.
+ // Credential authentication is always possible if the device is secured. Conversely, no
+ // form of authentication is possible if the device is not secured.
if (AuthenticatorUtils.isDeviceCredentialAllowed(authenticators)) {
- return BIOMETRIC_SUCCESS;
+ return mInjector.isDeviceSecuredWithCredential()
+ ? BIOMETRIC_SUCCESS
+ : BIOMETRIC_ERROR_NONE_ENROLLED;
}
// The class of some non-fingerprint biometrics can be checked on API 29.
@@ -393,7 +391,7 @@
// Having fingerprint hardware is a prerequisite, since BiometricPrompt internally
// calls FingerprintManager#getErrorString() on API 28 (b/151443237).
return mInjector.isFingerprintHardwarePresent()
- ? canAuthenticateWithFingerprintOrUnknown()
+ ? canAuthenticateWithFingerprintOrUnknownBiometric()
: BIOMETRIC_ERROR_NO_HARDWARE;
}
@@ -443,7 +441,7 @@
}
// If all else fails, check if fingerprint authentication is available.
- return canAuthenticateWithFingerprintOrUnknown();
+ return canAuthenticateWithFingerprintOrUnknownBiometric();
}
/**
@@ -465,14 +463,23 @@
}
/**
- * Checks if the user can authenticate with fingerprint, falling back to
- * {@link #BIOMETRIC_STATUS_UNKNOWN} for any error condition.
+ * Checks if the user can authenticate with fingerprint or with a biometric sensor for which
+ * there is no platform method to check availability.
*
- * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with fingerprint, or
- * {@link #BIOMETRIC_STATUS_UNKNOWN} otherwise.
+ * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with fingerprint. Otherwise,
+ * returns an error code indicating why the user can't authenticate, or
+ * {@link #BIOMETRIC_STATUS_UNKNOWN} if it is unknown whether the user can authenticate.
*/
@AuthenticationStatus
- private int canAuthenticateWithFingerprintOrUnknown() {
+ private int canAuthenticateWithFingerprintOrUnknownBiometric() {
+ // If the device is not secured, authentication is definitely not possible. Use
+ // FingerprintManager to distinguish between the "no hardware" and "none enrolled" cases.
+ if (!mInjector.isDeviceSecuredWithCredential()) {
+ return canAuthenticateWithFingerprint();
+ }
+
+ // Check for definite availability of fingerprint. Otherwise, return "unknown" to allow for
+ // non-fingerprint biometrics (e.g. iris) that may be available via BiometricPrompt.
return canAuthenticateWithFingerprint() == BIOMETRIC_SUCCESS
? BIOMETRIC_SUCCESS
: BIOMETRIC_STATUS_UNKNOWN;
diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricManagerTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricManagerTest.java
index abbd6f0..d359008 100644
--- a/biometric/biometric/src/test/java/androidx/biometric/BiometricManagerTest.java
+++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricManagerTest.java
@@ -240,39 +240,78 @@
@Test
@Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.Q)
- public void testCanAuthenticate_ReturnsError_WhenDeviceNotSecured_OnApi29() {
+ public void testCanAuthenticate_ReturnsError_WhenUnsecured_BiometricOnly_OnApi29() {
final android.hardware.biometrics.BiometricManager frameworkBiometricManager =
mock(android.hardware.biometrics.BiometricManager.class);
- when(frameworkBiometricManager.canAuthenticate()).thenReturn(BIOMETRIC_SUCCESS);
- when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
- when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(true);
+ when(frameworkBiometricManager.canAuthenticate()).thenReturn(BIOMETRIC_ERROR_NO_HARDWARE);
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+ when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(false);
final BiometricManager.Injector injector = new TestInjector.Builder()
.setBiometricManager(frameworkBiometricManager)
.setDeviceSecurable(true)
- .setFingerprintHardwarePresent(true)
+ .setFingerprintHardwarePresent(false)
.build();
final BiometricManager biometricManager = new BiometricManager(injector);
final int authenticators = Authenticators.BIOMETRIC_WEAK;
assertThat(biometricManager.canAuthenticate(authenticators))
+ .isEqualTo(BIOMETRIC_ERROR_NO_HARDWARE);
+ }
+
+ @Test
+ @Config(maxSdk = Build.VERSION_CODES.P)
+ public void testCanAuthenticate_ReturnsError_WhenUnsecured_BiometricOnly_OnApi28AndBelow() {
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+ when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(false);
+
+ final BiometricManager.Injector injector = new TestInjector.Builder()
+ .setFingerprintManager(mFingerprintManager)
+ .setDeviceSecurable(true)
+ .setFingerprintHardwarePresent(false)
+ .build();
+
+ final BiometricManager biometricManager = new BiometricManager(injector);
+ final int authenticators = Authenticators.BIOMETRIC_WEAK;
+ assertThat(biometricManager.canAuthenticate(authenticators))
+ .isEqualTo(BIOMETRIC_ERROR_NO_HARDWARE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.Q)
+ public void testCanAuthenticate_ReturnsError_WhenUnsecured_CredentialAllowed_OnApi29() {
+ final android.hardware.biometrics.BiometricManager frameworkBiometricManager =
+ mock(android.hardware.biometrics.BiometricManager.class);
+ when(frameworkBiometricManager.canAuthenticate()).thenReturn(BIOMETRIC_ERROR_NO_HARDWARE);
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+ when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(false);
+
+ final BiometricManager.Injector injector = new TestInjector.Builder()
+ .setBiometricManager(frameworkBiometricManager)
+ .setDeviceSecurable(true)
+ .setFingerprintHardwarePresent(false)
+ .build();
+
+ final BiometricManager biometricManager = new BiometricManager(injector);
+ final int authenticators = Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL;
+ assertThat(biometricManager.canAuthenticate(authenticators))
.isEqualTo(BIOMETRIC_ERROR_NONE_ENROLLED);
}
@Test
@Config(maxSdk = Build.VERSION_CODES.P)
- public void testCanAuthenticate_ReturnsError_WhenDeviceNotSecured_OnApi28AndBelow() {
- when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
- when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(true);
+ public void testCanAuthenticate_ReturnsError_WhenUnsecured_CredentialAllowed_OnApi28AndBelow() {
+ when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+ when(mFingerprintManager.hasEnrolledFingerprints()).thenReturn(false);
final BiometricManager.Injector injector = new TestInjector.Builder()
.setFingerprintManager(mFingerprintManager)
.setDeviceSecurable(true)
- .setFingerprintHardwarePresent(true)
+ .setFingerprintHardwarePresent(false)
.build();
final BiometricManager biometricManager = new BiometricManager(injector);
- final int authenticators = Authenticators.BIOMETRIC_WEAK;
+ final int authenticators = Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL;
assertThat(biometricManager.canAuthenticate(authenticators))
.isEqualTo(BIOMETRIC_ERROR_NONE_ENROLLED);
}
diff --git a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt b/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt
index 896f04e8..0e92e2a 100755
--- a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt
+++ b/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt
@@ -26,7 +26,7 @@
import androidx.biometric.auth.AuthPromptCallback
import androidx.biometric.auth.startClass2BiometricAuthentication
import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
-import androidx.biometric.auth.startClass3BiometricAuthentication
+import androidx.biometric.auth.authenticateWithClass3Biometrics
import androidx.biometric.auth.startClass3BiometricOrCredentialAuthentication
import androidx.biometric.auth.startCredentialAuthentication
import androidx.biometric.integration.testapp.R.string.biometric_prompt_description
@@ -191,7 +191,7 @@
)
R.id.class3_biometric_button ->
- startClass3BiometricAuthentication(
+ authenticateWithClass3Biometrics(
crypto = createCryptoOrNull(),
title = title,
subtitle = subtitle,
diff --git a/biometric/settings.gradle b/biometric/settings.gradle
index 697db78..7a96db8 100644
--- a/biometric/settings.gradle
+++ b/biometric/settings.gradle
@@ -20,6 +20,7 @@
setupPlayground(this, "..")
selectProjectsFromAndroidX({ name ->
if (name.startsWith(":biometric")) return true
+ if (name == ":annotation:annotation-sampled") return true
return false
})
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index fbea9d9..29e6fb6 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -124,19 +124,19 @@
val VERSIONED_PARCELABLE = Version("1.2.0-alpha01")
val VIEWPAGER = Version("1.1.0-alpha01")
val VIEWPAGER2 = Version("1.1.0-alpha02")
- val WEAR = Version("1.2.0-alpha05")
- val WEAR_COMPLICATIONS = Version("1.0.0-alpha05")
+ val WEAR = Version("1.2.0-alpha06")
+ val WEAR_COMPLICATIONS = Version("1.0.0-alpha06")
val WEAR_INPUT = Version("1.1.0-alpha01")
val WEAR_ONGOING = Version("1.0.0-alpha01")
val WEAR_PHONE_INTERACTIONS = Version("1.0.0-alpha01")
val WEAR_REMOTE_INTERACTIONS = Version("1.0.0-alpha01")
val WEAR_TILES = Version("1.0.0-alpha01")
val WEAR_TILES_DATA = WEAR_TILES
- val WEAR_WATCHFACE = Version("1.0.0-alpha05")
- val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha05")
- val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha05")
- val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha05")
- val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha05")
+ val WEAR_WATCHFACE = Version("1.0.0-alpha06")
+ val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha06")
+ val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha06")
+ val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha06")
+ val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha06")
val WEBKIT = Version("1.5.0-alpha01")
val WINDOW = Version("1.0.0-alpha02")
val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
diff --git a/car/app/app-aaos/api/current.txt b/car/app/app-aaos/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/car/app/app-aaos/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/car/app/app-aaos/api/public_plus_experimental_current.txt b/car/app/app-aaos/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/car/app/app-aaos/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/car/app/app-aaos/api/res-current.txt b/car/app/app-aaos/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/car/app/app-aaos/api/res-current.txt
diff --git a/car/app/app-aaos/api/restricted_current.txt b/car/app/app-aaos/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/car/app/app-aaos/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/car/app/app-aaos/build.gradle b/car/app/app-aaos/build.gradle
new file mode 100644
index 0000000..87a2082
--- /dev/null
+++ b/car/app/app-aaos/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ // Add dependencies here
+}
+
+androidx {
+ name = "Android for Cars App Library AAOS Extension"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.CAR_APP
+ inceptionYear = "2021"
+ description = "AAOS specific funationaltiy to build navigation, parking, and charging apps for cars"
+}
diff --git a/car/app/app-aaos/lint-baseline.xml b/car/app/app-aaos/lint-baseline.xml
new file mode 100644
index 0000000..8f1aa4b
--- /dev/null
+++ b/car/app/app-aaos/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+
+</issues>
diff --git a/car/app/app-aaos/src/androidTest/AndroidManifest.xml b/car/app/app-aaos/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3a56865
--- /dev/null
+++ b/car/app/app-aaos/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.car.app.aaos.test">
+
+</manifest>
diff --git a/car/app/app-aaos/src/main/AndroidManifest.xml b/car/app/app-aaos/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c562b9f
--- /dev/null
+++ b/car/app/app-aaos/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.car.app.aaos">
+
+</manifest>
\ No newline at end of file
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 6f4f9e1..98f0298 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -172,7 +172,7 @@
method public androidx.car.app.model.CarText? getTitle();
method public int getType();
method public boolean isStandard();
- method public androidx.car.app.model.Action.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Action.Builder newBuilder();
method public static String typeToString(int);
field public static final androidx.car.app.model.Action APP_ICON;
field public static final androidx.car.app.model.Action BACK;
@@ -236,7 +236,7 @@
method public androidx.core.graphics.drawable.IconCompat? getIcon();
method public androidx.car.app.model.CarColor? getTint();
method public int getType();
- method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.CarIcon.Builder newBuilder();
field public static final androidx.car.app.model.CarIcon ALERT;
field public static final androidx.car.app.model.CarIcon APP_ICON;
field public static final androidx.car.app.model.CarIcon BACK;
@@ -251,6 +251,7 @@
public static final class CarIcon.Builder {
ctor public CarIcon.Builder(androidx.core.graphics.drawable.IconCompat);
+ ctor public CarIcon.Builder(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.CarIcon build();
method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
}
@@ -446,24 +447,25 @@
method public androidx.car.app.model.MessageTemplate build();
method public androidx.car.app.model.MessageTemplate.Builder setActionList(java.util.List<androidx.car.app.model.Action!>);
method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
- method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(Throwable?);
method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
- method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
}
public class Metadata {
method public static androidx.car.app.model.Metadata.Builder builder();
method public androidx.car.app.model.Place? getPlace();
- method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Metadata.Builder newBuilder();
method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
}
public static final class Metadata.Builder {
ctor public Metadata.Builder();
+ ctor public Metadata.Builder(androidx.car.app.model.Metadata);
method public androidx.car.app.model.Metadata build();
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
@@ -532,11 +534,12 @@
method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
method public androidx.car.app.model.LatLng getLatLng();
method public androidx.car.app.model.PlaceMarker? getMarker();
- method public androidx.car.app.model.Place.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Place.Builder newBuilder();
}
public static final class Place.Builder {
ctor public Place.Builder(androidx.car.app.model.LatLng);
+ ctor public Place.Builder(androidx.car.app.model.Place);
method public androidx.car.app.model.Place build();
method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
@@ -929,7 +932,6 @@
method public androidx.car.app.model.CarIcon? getLanesImage();
method public androidx.car.app.navigation.model.Maneuver? getManeuver();
method public androidx.car.app.model.CarText? getRoad();
- method public androidx.car.app.navigation.model.Step.Builder newBuilder();
}
public static final class Step.Builder {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 6f4f9e1..98f0298 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -172,7 +172,7 @@
method public androidx.car.app.model.CarText? getTitle();
method public int getType();
method public boolean isStandard();
- method public androidx.car.app.model.Action.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Action.Builder newBuilder();
method public static String typeToString(int);
field public static final androidx.car.app.model.Action APP_ICON;
field public static final androidx.car.app.model.Action BACK;
@@ -236,7 +236,7 @@
method public androidx.core.graphics.drawable.IconCompat? getIcon();
method public androidx.car.app.model.CarColor? getTint();
method public int getType();
- method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.CarIcon.Builder newBuilder();
field public static final androidx.car.app.model.CarIcon ALERT;
field public static final androidx.car.app.model.CarIcon APP_ICON;
field public static final androidx.car.app.model.CarIcon BACK;
@@ -251,6 +251,7 @@
public static final class CarIcon.Builder {
ctor public CarIcon.Builder(androidx.core.graphics.drawable.IconCompat);
+ ctor public CarIcon.Builder(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.CarIcon build();
method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
}
@@ -446,24 +447,25 @@
method public androidx.car.app.model.MessageTemplate build();
method public androidx.car.app.model.MessageTemplate.Builder setActionList(java.util.List<androidx.car.app.model.Action!>);
method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
- method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(Throwable?);
method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
- method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
}
public class Metadata {
method public static androidx.car.app.model.Metadata.Builder builder();
method public androidx.car.app.model.Place? getPlace();
- method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Metadata.Builder newBuilder();
method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
}
public static final class Metadata.Builder {
ctor public Metadata.Builder();
+ ctor public Metadata.Builder(androidx.car.app.model.Metadata);
method public androidx.car.app.model.Metadata build();
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
@@ -532,11 +534,12 @@
method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
method public androidx.car.app.model.LatLng getLatLng();
method public androidx.car.app.model.PlaceMarker? getMarker();
- method public androidx.car.app.model.Place.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Place.Builder newBuilder();
}
public static final class Place.Builder {
ctor public Place.Builder(androidx.car.app.model.LatLng);
+ ctor public Place.Builder(androidx.car.app.model.Place);
method public androidx.car.app.model.Place build();
method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
@@ -929,7 +932,6 @@
method public androidx.car.app.model.CarIcon? getLanesImage();
method public androidx.car.app.navigation.model.Maneuver? getManeuver();
method public androidx.car.app.model.CarText? getRoad();
- method public androidx.car.app.navigation.model.Step.Builder newBuilder();
}
public static final class Step.Builder {
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 6f4f9e1..98f0298 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -172,7 +172,7 @@
method public androidx.car.app.model.CarText? getTitle();
method public int getType();
method public boolean isStandard();
- method public androidx.car.app.model.Action.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Action.Builder newBuilder();
method public static String typeToString(int);
field public static final androidx.car.app.model.Action APP_ICON;
field public static final androidx.car.app.model.Action BACK;
@@ -236,7 +236,7 @@
method public androidx.core.graphics.drawable.IconCompat? getIcon();
method public androidx.car.app.model.CarColor? getTint();
method public int getType();
- method public androidx.car.app.model.CarIcon.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.CarIcon.Builder newBuilder();
field public static final androidx.car.app.model.CarIcon ALERT;
field public static final androidx.car.app.model.CarIcon APP_ICON;
field public static final androidx.car.app.model.CarIcon BACK;
@@ -251,6 +251,7 @@
public static final class CarIcon.Builder {
ctor public CarIcon.Builder(androidx.core.graphics.drawable.IconCompat);
+ ctor public CarIcon.Builder(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.CarIcon build();
method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
}
@@ -446,24 +447,25 @@
method public androidx.car.app.model.MessageTemplate build();
method public androidx.car.app.model.MessageTemplate.Builder setActionList(java.util.List<androidx.car.app.model.Action!>);
method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
- method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method @Deprecated public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+ method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(Throwable?);
method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
- method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
}
public class Metadata {
method public static androidx.car.app.model.Metadata.Builder builder();
method public androidx.car.app.model.Place? getPlace();
- method public androidx.car.app.model.Metadata.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Metadata.Builder newBuilder();
method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
}
public static final class Metadata.Builder {
ctor public Metadata.Builder();
+ ctor public Metadata.Builder(androidx.car.app.model.Metadata);
method public androidx.car.app.model.Metadata build();
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
@@ -532,11 +534,12 @@
method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
method public androidx.car.app.model.LatLng getLatLng();
method public androidx.car.app.model.PlaceMarker? getMarker();
- method public androidx.car.app.model.Place.Builder newBuilder();
+ method @Deprecated public androidx.car.app.model.Place.Builder newBuilder();
}
public static final class Place.Builder {
ctor public Place.Builder(androidx.car.app.model.LatLng);
+ ctor public Place.Builder(androidx.car.app.model.Place);
method public androidx.car.app.model.Place build();
method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
@@ -929,7 +932,6 @@
method public androidx.car.app.model.CarIcon? getLanesImage();
method public androidx.car.app.navigation.model.Maneuver? getManeuver();
method public androidx.car.app.model.CarText? getRoad();
- method public androidx.car.app.navigation.model.Step.Builder newBuilder();
}
public static final class Step.Builder {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
index 3989dad..35d2146 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Action.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -144,8 +144,12 @@
/**
* Returns a {@link Builder} instance configured with the same data as this {@link Action}
* instance.
+ *
+ * @deprecated use constructor.
*/
+ // TODO(b/177484889): remove once host is changed to use new public ctor.
@NonNull
+ @Deprecated
public Builder newBuilder() {
return new Builder(this);
}
@@ -405,7 +409,13 @@
public Builder() {
}
- Builder(Action action) {
+ /**
+ * Returns a {@link Builder} instance configured with the same data as the given
+ * {@link Action} instance.
+ *
+ * @throws NullPointerException if {@code icon} is {@code null}.
+ */
+ Builder(@NonNull Action action) {
mTitle = action.getTitle();
mIcon = action.getIcon();
mBackgroundColor = action.getBackgroundColor();
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
index eb1b30e..b858168 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
@@ -21,6 +21,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.graphics.PorterDuff.Mode;
import android.os.Build.VERSION;
@@ -105,6 +106,7 @@
*/
// TODO(shiufai): investigate how to expose IntDefs if needed.
@RestrictTo(LIBRARY)
+ @SuppressLint("UniqueConstants") // TYPE_APP will be removed in a follow-up change.
@IntDef(
value = {
TYPE_CUSTOM,
@@ -210,7 +212,10 @@
/**
* Returns a {@link Builder} instance configured with the same data as this {@link CarIcon}
* instance.
+ * @deprecated use constructor.
*/
+ // TODO(b/177484889): remove once host is changed to use new public ctor.
+ @Deprecated
@NonNull
public Builder newBuilder() {
return new Builder(this);
@@ -425,7 +430,14 @@
mTint = null;
}
- Builder(@NonNull CarIcon carIcon) {
+ /**
+ * Returns a {@link Builder} instance configured with the same data as the given
+ * {@link CarIcon} instance.
+ *
+ * @throws NullPointerException if {@code icon} is {@code null}.
+ */
+ public Builder(@NonNull CarIcon carIcon) {
+ requireNonNull(carIcon);
mType = carIcon.getType();
mIcon = carIcon.getIcon();
mTint = carIcon.getTint();
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
index c899dd7..10abbed 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -175,8 +175,7 @@
/**
* Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
- * show a
- * title.
+ * show a title.
*/
@NonNull
public Builder setTitle(@Nullable CharSequence title) {
@@ -185,44 +184,51 @@
}
/**
- * Sets the {@link CharSequence} to display as the message in the template.
- *
- * @throws NullPointerException if {@code message} is null.
- */
- @NonNull
- public Builder setMessage(@NonNull CharSequence message) {
- this.mMessage = CarText.create(requireNonNull(message));
- return this;
- }
-
- /**
* Sets a {@link Throwable} for debugging purposes, or {@code null} to not show it.
*
* <p>The cause will be displayed along with the message set in {@link #setDebugMessage}.
*
* <p>The host may choose to not display this debugging information if it doesn't deem it
* appropriate, for example, when running on a production environment rather than in a
- * simulator
- * such as the Desktop Head Unit.
+ * simulator such as the Desktop Head Unit.
+ *
+ * @deprecated use {@link #setDebugMessage(String) instead.}
*/
@NonNull
// Suppress as the cause is transformed into a message before transport.
@SuppressLint("MissingGetterMatchingBuilder")
+ @Deprecated
+ // TODO(b/177591352): remove once host does not reference this method.
public Builder setDebugCause(@Nullable Throwable cause) {
this.mDebugCause = cause;
return this;
}
/**
+ * Sets a {@link Throwable} for debugging purposes, or {@code null} to not show it.
+ *
+ * <p>The cause will be displayed along with the message set in
+ * {@link #setDebugMessage(String)}.
+ *
+ * <p>The host may choose to not display this debugging information if it doesn't deem it
+ * appropriate, for example, when running on a production environment rather than in a
+ * simulator such as the Desktop Head Unit.
+ */
+ @NonNull
+ public Builder setDebugMessage(@Nullable Throwable cause) {
+ this.mDebugCause = cause;
+ return this;
+ }
+
+ /**
* Sets a debug message for debugging purposes, or {@code null} to not show a debug message.
*
* <p>The debug message will be displayed along with the cause set in
- * {@link #setDebugCause}.
+ * {@link #setDebugMessage}.
*
* <p>The host may choose to not display this debugging information if it doesn't deem it
* appropriate, for example, when running on a production environment rather than in a
- * simulator
- * such as the Desktop Head Unit.
+ * simulator such as the Desktop Head Unit.
*/
@NonNull
public Builder setDebugMessage(@Nullable String debugMessage) {
@@ -237,14 +243,11 @@
* <h4>Icon Sizing Guidance</h4>
*
* The provided icon should have a maximum size of 64 x 64 dp. If the icon exceeds this
- * maximum
- * size in either one of the dimensions, it will be scaled down and centered inside the
- * bounding
- * box while preserving the aspect ratio.
+ * maximum size in either one of the dimensions, it will be scaled down and centered
+ * inside the bounding box while preserving the aspect ratio.
*
* <p>See {@link CarIcon} for more details related to providing icon and image resources
- * that
- * work with different car screen pixel densities.
+ * that work with different car screen pixel densities.
*/
@NonNull
public Builder setIcon(@Nullable CarIcon icon) {
@@ -255,8 +258,7 @@
/**
* Sets the {@link Action} that will be displayed in the header of the template, or
- * {@code null}
- * to not display an action.
+ * {@code null} to not display an action.
*
* <h4>Requirements</h4>
*
@@ -315,15 +317,13 @@
*
* <h4>Requirements</h4>
*
- * A non-empty message must be set on the template with {@link
- * Builder#setMessage(CharSequence)}.
+ * A non-empty message must be set on the template.
*
* <p>Either a header {@link Action} or title must be set on the template.
*
* @throws IllegalStateException if the message is empty.
* @throws IllegalStateException if the template does not have either a title or header
- * {@link
- * Action} set.
+ * {@link Action} set.
*/
@NonNull
public MessageTemplate build() {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Metadata.java b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
index f5e152b..8894213 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
@@ -51,7 +51,12 @@
return new Builder().setPlace(requireNonNull(place)).build();
}
- /** Returns a new {@link Builder} with the data from this {@link Metadata} instance. */
+ /**
+ * Returns a new {@link Builder} with the data from this {@link Metadata} instance.
+ * @deprecated use constructor.
+ */
+ // TODO(b/177484889): remove once host is changed to use new public ctor.
+ @Deprecated
@NonNull
public Builder newBuilder() {
return new Builder(this);
@@ -117,8 +122,13 @@
public Builder() {
}
- Builder(Metadata metadata) {
- this.mPlace = metadata.getPlace();
+ /**
+ * Returns a new {@link Builder} with the data from the given {@link Metadata} instance.
+ *
+ * @throws NullPointerException if {@code icon} is {@code null}.
+ */
+ public Builder(@NonNull Metadata metadata) {
+ this.mPlace = requireNonNull(metadata).getPlace();
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Place.java b/car/app/app/src/main/java/androidx/car/app/model/Place.java
index 2932e24..771f4f6 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Place.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Place.java
@@ -45,8 +45,13 @@
return new Builder(requireNonNull(latLng));
}
- /** Returns a {@link Builder} instance with the same data as this {@link Place} instance. */
+ /**
+ * Returns a {@link Builder} instance with the same data as this {@link Place} instance.
+ * @deprecated use constructor.
+ */
+ // TODO(b/177484889): remove once host is changed to use new public ctor.
@NonNull
+ @Deprecated
public Builder newBuilder() {
return new Builder(this);
}
@@ -113,7 +118,12 @@
mLatLng = latLng;
}
- Builder(Place place) {
+ /**
+ * Returns a {@link Builder} instance with the same data as the given {@link Place}
+ * instance.
+ */
+ public Builder(@NonNull Place place) {
+ requireNonNull(place);
mLatLng = place.getLatLng();
mMarker = place.getMarker();
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
index 8d9c01d..0745a1b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
@@ -19,6 +19,8 @@
import static androidx.annotation.RestrictTo.Scope;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -59,13 +61,13 @@
*/
@NonNull
public static final ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE =
- ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxCustomTitles(1).build();
+ new ActionsConstraints.Builder(ACTIONS_CONSTRAINTS_CONSERVATIVE).setMaxCustomTitles(
+ 1).build();
/** Constraints for navigation templates. */
@NonNull
public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION =
- ACTIONS_CONSTRAINTS_CONSERVATIVE
- .newBuilder()
+ new ActionsConstraints.Builder(ACTIONS_CONSTRAINTS_CONSERVATIVE)
.setMaxActions(4)
.setMaxCustomTitles(1)
.addRequiredActionType(Action.TYPE_CUSTOM)
@@ -84,16 +86,6 @@
return new Builder();
}
- /**
- * Returns a new builder that contains the same data as this {@link ActionsConstraints}
- * instance.
- */
- @VisibleForTesting
- @NonNull
- public Builder newBuilder() {
- return new Builder(this);
- }
-
/** Returns the max number of actions allowed. */
public int getMaxActions() {
return mMaxActions;
@@ -239,7 +231,14 @@
public Builder() {
}
- Builder(ActionsConstraints constraints) {
+ /**
+ * Returns a new builder that contains the same data as the given {@link ActionsConstraints}
+ * instance.
+ *
+ * @throws NullPointerException if {@code latLng} is {@code null}.
+ */
+ public Builder(@NonNull ActionsConstraints constraints) {
+ requireNonNull(constraints);
this.mMaxActions = constraints.getMaxActions();
this.mMaxCustomTitles = constraints.getMaxCustomTitles();
this.mRequiredActionTypes.addAll(constraints.getRequiredActionTypes());
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
index 685cf14..e88c148 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
@@ -16,6 +16,8 @@
package androidx.car.app.model.constraints;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.car.app.model.CarIcon;
@@ -67,7 +69,7 @@
/** The constraints for a full-width row in a list (simple + toggle support). */
@NonNull
public static final RowConstraints ROW_CONSTRAINTS_FULL_LIST =
- ROW_CONSTRAINTS_SIMPLE.newBuilder().setToggleAllowed(true).build();
+ new RowConstraints.Builder(ROW_CONSTRAINTS_SIMPLE).setToggleAllowed(true).build();
private final int mMaxTextLinesPerRow;
private final int mMaxActionsExclusive;
@@ -85,14 +87,6 @@
return new Builder();
}
- /**
- * Returns a new builder that contains the same data as this {@link RowConstraints} instance.
- */
- @NonNull
- public Builder newBuilder() {
- return new Builder(this);
- }
-
/** Returns whether the row can have a click listener associated with it. */
public boolean isOnClickListenerAllowed() {
return mIsOnClickListenerAllowed;
@@ -225,10 +219,17 @@
}
/** Returns an empty {@link Builder} instance. */
- Builder() {
+ public Builder() {
}
- Builder(RowConstraints constraints) {
+ /**
+ * Returns a new builder that contains the same data as the given {@link RowConstraints}
+ * instance.
+ *
+ * @throws NullPointerException if {@code latLng} is {@code null}.
+ */
+ public Builder(@NonNull RowConstraints constraints) {
+ requireNonNull(constraints);
mIsOnClickListenerAllowed = constraints.isOnClickListenerAllowed();
mMaxTextLines = constraints.getMaxTextLinesPerRow();
mMaxActionsExclusive = constraints.getMaxActionsExclusive();
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
index d0b1464..e4bbcf5 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
@@ -21,6 +21,8 @@
import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_PANE;
import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_SIMPLE;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.car.app.model.Action;
@@ -50,8 +52,7 @@
/** Default constraints for heterogeneous pane of items, full width. */
@NonNull
public static final RowListConstraints ROW_LIST_CONSTRAINTS_PANE =
- ROW_LIST_CONSTRAINTS_CONSERVATIVE
- .newBuilder()
+ new RowListConstraints.Builder(ROW_LIST_CONSTRAINTS_CONSERVATIVE)
.setMaxActions(2)
.setRowConstraints(ROW_CONSTRAINTS_PANE)
.setAllowSelectableLists(false)
@@ -60,16 +61,14 @@
/** Default constraints for uniform lists of items, no toggles. */
@NonNull
public static final RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE =
- ROW_LIST_CONSTRAINTS_CONSERVATIVE
- .newBuilder()
+ new RowListConstraints.Builder(ROW_LIST_CONSTRAINTS_CONSERVATIVE)
.setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
.build();
/** Default constraints for the route preview card. */
@NonNull
public static final RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW =
- ROW_LIST_CONSTRAINTS_CONSERVATIVE
- .newBuilder()
+ new RowListConstraints.Builder(ROW_LIST_CONSTRAINTS_CONSERVATIVE)
.setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
.setAllowSelectableLists(true)
.build();
@@ -77,8 +76,7 @@
/** Default constraints for uniform lists of items, full width (simple + toggle support). */
@NonNull
public static final RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST =
- ROW_LIST_CONSTRAINTS_CONSERVATIVE
- .newBuilder()
+ new RowListConstraints.Builder(ROW_LIST_CONSTRAINTS_CONSERVATIVE)
.setRowConstraints(ROW_CONSTRAINTS_FULL_LIST)
.setAllowSelectableLists(true)
.build();
@@ -94,12 +92,6 @@
return new Builder();
}
- /** Return a a new builder for this {@link RowListConstraints} instance. */
- @NonNull
- public Builder newBuilder() {
- return new Builder(this);
- }
-
/** Returns the maximum number of actions allowed to be added alongside the list. */
public int getMaxActions() {
return mMaxActions;
@@ -220,7 +212,13 @@
public Builder() {
}
- Builder(RowListConstraints constraints) {
+ /**
+ * Return a a new builder for the given {@link RowListConstraints} instance.
+ *
+ * @throws NullPointerException if {@code latLng} is {@code null}.
+ */
+ public Builder(@NonNull RowListConstraints constraints) {
+ requireNonNull(constraints);
this.mMaxActions = constraints.getMaxActions();
this.mRowConstraints = constraints.getRowConstraints();
this.mAllowSelectableLists = constraints.isAllowSelectableLists();
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
index 867f254..7d9b95c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -70,15 +70,6 @@
return new Builder(requireNonNull(cue));
}
- /**
- * Returns a new {@link Builder} instance configured with the same data as this {@link Step}
- * instance.
- */
- @NonNull
- public Builder newBuilder() {
- return new Builder(this);
- }
-
@Nullable
public Maneuver getManeuver() {
return mManeuver;
@@ -193,19 +184,9 @@
this.mCue = CarText.create(requireNonNull(cue));
}
- Builder(Step step) {
- this.mManeuver = step.getManeuver();
- this.mLanes.clear();
- this.mLanes.addAll(step.getLanes());
- this.mLanesImage = step.getLanesImage();
- this.mCue = requireNonNull(step.getCue());
- this.mRoad = step.getRoad();
- }
-
/**
* Sets the maneuver to be performed on this step or {@code null} if this step doesn't
- * involve a
- * maneuver.
+ * involve a maneuver.
*/
@NonNull
public Builder setManeuver(@Nullable Maneuver maneuver) {
diff --git a/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
index 414e404..8232f95 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
@@ -135,34 +135,6 @@
}
@Test
- public void create_invalidSetOnBackThrows() {
- assertThrows(
- IllegalStateException.class,
- () -> Action.BACK.newBuilder().setOnClickListener(() -> {
- }).build());
- assertThrows(
- IllegalStateException.class,
- () -> Action.BACK.newBuilder().setTitle("BACK").build());
- assertThrows(
- IllegalStateException.class,
- () -> Action.BACK.newBuilder().setIcon(CarIcon.ALERT).build());
- }
-
- @Test
- public void create_invalidSetOnAppIconThrows() {
- assertThrows(
- IllegalStateException.class,
- () -> Action.APP_ICON.newBuilder().setOnClickListener(() -> {
- }).build());
- assertThrows(
- IllegalStateException.class,
- () -> Action.APP_ICON.newBuilder().setTitle("APP").build());
- assertThrows(
- IllegalStateException.class,
- () -> Action.APP_ICON.newBuilder().setIcon(CarIcon.ALERT).build());
- }
-
- @Test
public void equals() {
String title = "foo";
CarIcon icon = CarIcon.ALERT;
diff --git a/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
index 79f15fb..ef80907 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
@@ -77,7 +77,7 @@
@Test
public void newBuilder_fromStandard() {
- CarIcon carIcon = BACK.newBuilder().setTint(GREEN).build();
+ CarIcon carIcon = new CarIcon.Builder(BACK).setTint(GREEN).build();
assertThat(carIcon.getType()).isEqualTo(TYPE_BACK);
assertThat(carIcon.getTint()).isEqualTo(GREEN);
@@ -144,6 +144,6 @@
@Test
public void notEquals() {
- assertThat(BACK.newBuilder().setTint(GREEN).build()).isNotEqualTo(BACK);
+ assertThat(new CarIcon.Builder(BACK).setTint(GREEN).build()).isNotEqualTo(BACK);
}
}
diff --git a/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
index 9909aec..8000014 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
@@ -111,7 +111,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setHeaderAction(Action.BACK)
- .setDebugCause(exception)
+ .setDebugMessage(exception)
.setIcon(icon)
.setActionList(ImmutableList.of(action))
.build();
@@ -131,7 +131,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setHeaderAction(Action.BACK)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
@@ -140,7 +140,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setHeaderAction(Action.BACK)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
@@ -155,7 +155,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -163,7 +163,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage("yo")
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -177,7 +177,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -185,7 +185,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(new IllegalStateException("something else bad"))
+ .setDebugMessage(new IllegalStateException("something else bad"))
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -199,8 +199,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
- .setMessage(mMessage)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -208,7 +207,7 @@
new MessageTemplate.Builder("bar")
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -222,7 +221,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setHeaderAction(Action.BACK)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
@@ -231,7 +230,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setHeaderAction(Action.APP_ICON)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
@@ -246,7 +245,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -254,7 +253,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction, mAction))
.setIcon(mIcon)
.build();
@@ -268,7 +267,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -276,7 +275,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(CarIcon.ERROR)
.build();
@@ -290,7 +289,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle(mTitle)
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
@@ -298,7 +297,7 @@
new MessageTemplate.Builder(mMessage)
.setTitle("Header2")
.setDebugMessage(mDebugMessage)
- .setDebugCause(mCause)
+ .setDebugMessage(mCause)
.setActionList(ImmutableList.of(mAction))
.setIcon(mIcon)
.build();
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index 3ecaca6..df76239 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -9,6 +9,7 @@
ctor public Animatable(T? initialValue, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, T? visibilityThreshold);
method public suspend Object? animateDecay(T? initialVelocity, androidx.compose.animation.core.DecayAnimationSpec<T> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
method public suspend Object? animateTo(T? targetValue, optional androidx.compose.animation.core.AnimationSpec<T> animationSpec, optional T? initialVelocity, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
+ method public androidx.compose.runtime.State<T> asState();
method public T? getLowerBound();
method public T! getTargetValue();
method public androidx.compose.animation.core.TwoWayConverter<T,V> getTypeConverter();
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index 3ecaca6..df76239 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -9,6 +9,7 @@
ctor public Animatable(T? initialValue, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, T? visibilityThreshold);
method public suspend Object? animateDecay(T? initialVelocity, androidx.compose.animation.core.DecayAnimationSpec<T> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
method public suspend Object? animateTo(T? targetValue, optional androidx.compose.animation.core.AnimationSpec<T> animationSpec, optional T? initialVelocity, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
+ method public androidx.compose.runtime.State<T> asState();
method public T? getLowerBound();
method public T! getTargetValue();
method public androidx.compose.animation.core.TwoWayConverter<T,V> getTypeConverter();
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index c5a861a..6f1cc23b 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -9,6 +9,7 @@
ctor public Animatable(T? initialValue, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, T? visibilityThreshold);
method public suspend Object? animateDecay(T? initialVelocity, androidx.compose.animation.core.DecayAnimationSpec<T> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
method public suspend Object? animateTo(T? targetValue, optional androidx.compose.animation.core.AnimationSpec<T> animationSpec, optional T? initialVelocity, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Animatable<T,V>,kotlin.Unit>? block, optional kotlin.coroutines.Continuation<? super androidx.compose.animation.core.AnimationResult<T,V>> p);
+ method public androidx.compose.runtime.State<T> asState();
method public T? getLowerBound();
method public T! getTargetValue();
method public androidx.compose.animation.core.TwoWayConverter<T,V> getTypeConverter();
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/InfiniteTransitionSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/InfiniteTransitionSamples.kt
index d01dc7c..1977ca9 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/InfiniteTransitionSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/InfiniteTransitionSamples.kt
@@ -83,7 +83,8 @@
Box(Modifier.fillMaxSize()) {
Icon(
Icons.Filled.Favorite,
- Modifier.align(Alignment.Center)
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.Center)
.graphicsLayer(
scaleX = scale,
scaleY = scale
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
index 43d0a9e..01fb138 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
@@ -93,7 +93,8 @@
Box(Modifier.fillMaxSize()) {
Icon(
Icons.Filled.Favorite,
- Modifier.align(Alignment.Center)
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.Center)
.graphicsLayer(
scaleX = 3.0f,
scaleY = 3.0f,
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
index 535d987..75f62a2 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.AnimationEndReason.Finished
import androidx.compose.animation.core.AnimationEndReason.Interrupted
import androidx.compose.runtime.AtomicReference
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -381,6 +382,13 @@
endAnimation()
}
+ /**
+ * Returns a [State] representing the current [value] of this animation. This allows
+ * hoisting the animation's current value without causing unnecessary recompositions
+ * when the value changes.
+ */
+ fun asState(): State<T> = internalState
+
private fun Job.cancelAnimation() {
cancel(AnimationCancellationException())
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
index e0aa04d..689c1c3 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
@@ -52,7 +52,8 @@
Box(Modifier.fillMaxSize()) {
Icon(
Icons.Filled.Favorite,
- Modifier.align(Alignment.Center)
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.Center)
.graphicsLayer(
scaleX = 3.0f,
scaleY = 3.0f,
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteTransitionDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteTransitionDemo.kt
index efc6be4..34e6bb1 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteTransitionDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteTransitionDemo.kt
@@ -60,6 +60,7 @@
Box(Modifier.fillMaxSize()) {
Icon(
Icons.Filled.Favorite,
+ null,
Modifier.align(Alignment.Center)
.graphicsLayer(
scaleX = scale,
diff --git a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedVisibilitySamples.kt b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedVisibilitySamples.kt
index 7ed33cb..959558b 100644
--- a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedVisibilitySamples.kt
+++ b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedVisibilitySamples.kt
@@ -170,7 +170,11 @@
modifier = with(ColumnScope) { Modifier.align(Alignment.CenterHorizontally) }
) {
Row(Modifier.padding(start = 12.dp, end = 12.dp)) {
- Icon(Icons.Default.Favorite, Modifier.align(Alignment.CenterVertically))
+ Icon(
+ Icons.Default.Favorite,
+ contentDescription = "Favorite",
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
AnimatedVisibility(
expanded,
modifier = Modifier.align(Alignment.CenterVertically)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
index 1bcfea1..64dcdec 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
@@ -117,7 +117,7 @@
TopAppBar(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Outlined.Home)
+ Icon(Icons.Outlined.Home, "Home")
Text(title)
}
}
@@ -137,7 +137,7 @@
IconButton(
>
) {
- Icon(Icons.Filled.Menu, Modifier.size(ButtonDefaults.IconSize))
+ Icon(Icons.Filled.Menu, "Menu", Modifier.size(ButtonDefaults.IconSize))
}
}
},
@@ -397,11 +397,13 @@
Row {
Image(
imageResource("androidx/compose/desktop/example/circus.jpg"),
+ "Localized description",
Modifier.size(200.dp)
)
Icon(
vectorXmlResource("androidx/compose/desktop/example/ic_baseline_deck_24.xml"),
+ "Localized description",
Modifier.size(100.dp).align(Alignment.CenterVertically),
tint = Color.Blue.copy(alpha = 0.5f)
)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
index 82c219c..ae7cf66 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.layout
+import android.content.Context
import androidx.compose.runtime.Providers
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
@@ -34,6 +35,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -52,11 +54,17 @@
@get:Rule
val rule = createComposeRule()
+ var displaySize: IntSize = IntSize.Zero
+
// region sizing tests
@Before
fun before() {
isDebugInspectorInfoEnabled = true
+ displaySize = ApplicationProvider
+ .getApplicationContext<Context>().resources.displayMetrics.let {
+ IntSize(it.widthPixels, it.heightPixels)
+ }
}
@After
@@ -68,6 +76,7 @@
fun dividerMatchTextHeight_spread() = with(density) {
val aspectRatioBoxSize = Ref<IntSize>()
val dividerSize = Ref<IntSize>()
+
rule.setContent {
ConstraintLayout(
// Make CL fixed width and wrap content height.
@@ -85,7 +94,7 @@
height = Dimension.wrapContent
}
// Try to be large to make wrap content impossible.
- .preferredWidth((rule.displaySize.width).toDp())
+ .preferredWidth((displaySize.width).toDp())
// This could be any (width in height out child) e.g. text
.aspectRatio(2f)
.onGloballyPositioned { coordinates ->
@@ -108,12 +117,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (rule.displaySize.width / 2),
+ (displaySize.width / 2),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (rule.displaySize.width / 2 / 2),
+ (displaySize.width / 2 / 2),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -145,7 +154,7 @@
height = Dimension.preferredWrapContent
}
// Try to be large to make wrap content impossible.
- .preferredWidth((rule.displaySize.width).toDp())
+ .preferredWidth((displaySize.width).toDp())
// This could be any (width in height out child) e.g. text
.aspectRatio(2f)
.onGloballyPositioned { coordinates ->
@@ -168,12 +177,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (rule.displaySize.width / 2),
+ (displaySize.width / 2),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (rule.displaySize.width / 2 / 2),
+ (displaySize.width / 2 / 2),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -205,7 +214,7 @@
height = Dimension.wrapContent
}
// Try to be large to make wrap content impossible.
- .preferredWidth((rule.displaySize.width).toDp())
+ .preferredWidth((displaySize.width).toDp())
// This could be any (width in height out child) e.g. text
.aspectRatio(2f)
.onGloballyPositioned { coordinates ->
@@ -229,12 +238,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (rule.displaySize.width / 2),
+ (displaySize.width / 2),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (rule.displaySize.width / 2 / 2),
+ (displaySize.width / 2 / 2),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -267,7 +276,7 @@
height = Dimension.wrapContent
}
// Try to be large to make wrap content impossible.
- .preferredWidth((rule.displaySize.width).toDp())
+ .preferredWidth((displaySize.width).toDp())
// This could be any (width in height out child) e.g. text
.aspectRatio(2f)
.onGloballyPositioned { coordinates ->
@@ -291,12 +300,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (rule.displaySize.width / 2),
+ (displaySize.width / 2),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (rule.displaySize.width / 2 / 2),
+ (displaySize.width / 2 / 2),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -423,8 +432,8 @@
}
}
- val displayWidth = rule.displaySize.width
- val displayHeight = rule.displaySize.height
+ val displayWidth = displaySize.width
+ val displayHeight = displaySize.height
rule.runOnIdle {
assertEquals(
@@ -493,8 +502,8 @@
}
}
- val displayWidth = rule.displaySize.width
- val displayHeight = rule.displaySize.height
+ val displayWidth = displaySize.width
+ val displayHeight = displaySize.height
rule.runOnIdle {
assertEquals(
@@ -569,8 +578,8 @@
}
}
- val displayWidth = rule.displaySize.width
- val displayHeight = rule.displaySize.height
+ val displayWidth = displaySize.width
+ val displayHeight = displaySize.height
rule.runOnIdle {
assertEquals(
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
index 7a1d75e..ba7afda 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
@@ -45,11 +45,12 @@
/**
* Horizontally places the layout children.
*
- * @param totalSize Available space that can be occupied by the children.
- * @param sizes An array of sizes of all children.
+ * @param totalSize Available space that can be occupied by the children, in pixels.
+ * @param sizes An array of sizes of all children, in pixels.
* @param layoutDirection A layout direction, left-to-right or right-to-left, of the parent
* layout that should be taken into account when determining positions of the children.
- * @param outPositions An array of the size of [sizes] that returns the calculated positions.
+ * @param outPositions An array of the size of [sizes] that returns the calculated
+ * positions relative to the left, in pixels.
*/
fun Density.arrange(
totalSize: Int,
@@ -72,9 +73,10 @@
/**
* Vertically places the layout children.
*
- * @param totalSize Available space that can be occupied by the children.
- * @param sizes An array of sizes of all children.
- * @param outPositions An array of the size of [sizes] that returns the calculated positions.
+ * @param totalSize Available space that can be occupied by the children, in pixels.
+ * @param sizes An array of sizes of all children, in pixels.
+ * @param outPositions An array of the size of [sizes] that returns the calculated
+ * positions relative to the top, in pixels.
*/
fun Density.arrange(
totalSize: Int,
@@ -98,7 +100,8 @@
/**
* Place children horizontally such that they are as close as possible to the beginning of the
- * main axis.
+ * horizontal axis (left if the layout direction is LTR, right otherwise).
+ * Visually: 123#### for LTR and ####321.
*/
@Stable
val Start = object : Horizontal {
@@ -119,6 +122,7 @@
/**
* Place children horizontally such that they are as close as possible to the end of the main
* axis.
+ * Visually: ####123 for LTR and 321#### for RTL.
*/
@Stable
val End = object : Horizontal {
@@ -139,6 +143,7 @@
/**
* Place children vertically such that they are as close as possible to the top of the main
* axis.
+ * Visually: (top) 123#### (bottom)
*/
@Stable
val Top = object : Vertical {
@@ -152,6 +157,7 @@
/**
* Place children vertically such that they are as close as possible to the bottom of the main
* axis.
+ * Visually: (top) ####123 (bottom)
*/
@Stable
val Bottom = object : Vertical {
@@ -164,6 +170,7 @@
/**
* Place children such that they are as close as possible to the middle of the main axis.
+ * Visually: ##123## for LTR and ##321## for RTL.
*/
@Stable
val Center = object : HorizontalOrVertical {
@@ -192,6 +199,7 @@
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
+ * Visually: #1#2#3# for LTR and #3#2#1# for RTL.
*/
@Stable
val SpaceEvenly = object : HorizontalOrVertical {
@@ -220,6 +228,7 @@
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
+ * Visually: 1##2##3 for LTR or 3##2##1 for RTL.
*/
@Stable
val SpaceBetween = object : HorizontalOrVertical {
@@ -249,6 +258,7 @@
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child, but half the amount of space
* existing otherwise between two consecutive children.
+ * Visually: #1##2##3# for LTR and #3##2##1# for RTL
*/
@Stable
val SpaceAround = object : HorizontalOrVertical {
@@ -345,6 +355,8 @@
*
* Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: 123####
*/
@Stable
val Left = object : Horizontal {
@@ -361,6 +373,8 @@
*
* Unlike [Arrangement.Center], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: ##123##
*/
@Stable
val Center = object : Horizontal {
@@ -378,6 +392,8 @@
*
* Unlike [Arrangement.End], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: ####123
*/
@Stable
val Right = object : Horizontal {
@@ -395,6 +411,8 @@
*
* Unlike [Arrangement.SpaceBetween], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: 1##2##3
*/
@Stable
val SpaceBetween = object : Horizontal {
@@ -412,6 +430,8 @@
*
* Unlike [Arrangement.SpaceEvenly], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: #1#2#3#
*/
@Stable
val SpaceEvenly = object : Horizontal {
@@ -430,6 +450,8 @@
*
* Unlike [Arrangement.SpaceAround], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
+ *
+ * Visually: #1##2##3##4#
*/
@Stable
val SpaceAround = object : Horizontal {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
index 78f6cec..7a161de 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
@@ -40,7 +40,7 @@
* Padding is applied before content measurement and takes precedence; content may only be as large
* as the remaining space.
*
- * Negative padding is not permitted. See [offset].
+ * Negative padding is not permitted. See [Modifier.offset].
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.PaddingModifier
@@ -74,7 +74,7 @@
* Padding is applied before content measurement and takes precedence; content may only be as large
* as the remaining space.
*
- * Negative padding is not permitted. See [offset].
+ * Negative padding is not permitted. See [Modifier.offset].
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SymmetricPaddingModifier
@@ -103,7 +103,7 @@
* Padding is applied before content measurement and takes precedence; content may only be as large
* as the remaining space.
*
- * Negative padding is not permitted. See [offset].
+ * Negative padding is not permitted. See [Modifier.offset].
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.PaddingAllModifier
@@ -129,7 +129,7 @@
* top, right and bottom. Padding is applied before content measurement and takes precedence;
* content may only be as large as the remaining space.
*
- * Negative padding is not permitted. See [offset].
+ * Negative padding is not permitted. See [Modifier.offset].
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.PaddingValuesModifier
@@ -158,7 +158,7 @@
* [padding] to apply relative paddings. Padding is applied before content measurement and takes
* precedence; content may only be as large as the remaining space.
*
- * Negative padding is not permitted. See [offset].
+ * Negative padding is not permitted. See [Modifier.offset].
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.AbsolutePaddingModifier
@@ -258,5 +258,14 @@
@Stable
val bottom: Dp = 0.dp
) {
+ /**
+ * Describes a padding of [all] dp along all 4 edges.
+ */
constructor(all: Dp) : this(all, all, all, all)
+
+ /**
+ * Describes a padding of [horizontal] dp along the left and right edges, and of [vertical]
+ * dp along the top and bottom edges.
+ */
+ constructor(horizontal: Dp, vertical: Dp) : this(horizontal, vertical, horizontal, vertical)
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index a4ee665..e2ea5c1 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -66,9 +66,9 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
}
@androidx.compose.runtime.Stable public interface Indication {
@@ -510,8 +510,8 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField-55_Anxs(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
- method @androidx.compose.runtime.Composable public static void BasicTextField-HcQ_yMQ(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-Q80SffI(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-qDYpSg4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
@@ -520,7 +520,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-j_8B-p4(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-KzZcjnU(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions, optional boolean enabled, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class CoreTextKt {
@@ -574,12 +574,18 @@
public final class TextFieldDragGestureFilterKt {
}
+ public final class TextFieldGestureModifiersKt {
+ }
+
public final class TextFieldScrollKt {
}
public final class TextFieldSizeKt {
}
+ public final class TextLayoutResultProxyKt {
+ }
+
}
package androidx.compose.foundation.text.selection {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index a4ee665..e2ea5c1 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -66,9 +66,9 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
}
@androidx.compose.runtime.Stable public interface Indication {
@@ -510,8 +510,8 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField-55_Anxs(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
- method @androidx.compose.runtime.Composable public static void BasicTextField-HcQ_yMQ(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-Q80SffI(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-qDYpSg4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
@@ -520,7 +520,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-j_8B-p4(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-KzZcjnU(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions, optional boolean enabled, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class CoreTextKt {
@@ -574,12 +574,18 @@
public final class TextFieldDragGestureFilterKt {
}
+ public final class TextFieldGestureModifiersKt {
+ }
+
public final class TextFieldScrollKt {
}
public final class TextFieldSizeKt {
}
+ public final class TextLayoutResultProxyKt {
+ }
+
}
package androidx.compose.foundation.text.selection {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index a4ee665..e2ea5c1 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -66,9 +66,9 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
- method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static inline void Image(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale, optional float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter);
}
@androidx.compose.runtime.Stable public interface Indication {
@@ -510,8 +510,8 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField-55_Anxs(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
- method @androidx.compose.runtime.Composable public static void BasicTextField-HcQ_yMQ(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-Q80SffI(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField-qDYpSg4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
@@ -520,7 +520,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-j_8B-p4(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-KzZcjnU(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions, optional boolean enabled, optional boolean readOnly, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
}
public final class CoreTextKt {
@@ -574,12 +574,18 @@
public final class TextFieldDragGestureFilterKt {
}
+ public final class TextFieldGestureModifiersKt {
+ }
+
public final class TextFieldScrollKt {
}
public final class TextFieldSizeKt {
}
+ public final class TextLayoutResultProxyKt {
+ }
+
}
package androidx.compose.foundation.text.selection {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
index 5aaa22ea..d1cc274 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
@@ -18,14 +18,26 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.savedinstancestate.savedInstanceState
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
@Sampled
@Composable
@@ -69,4 +81,30 @@
Text(text = "Placeholder")
}
}
+}
+
+@Sampled
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+fun TextFieldWithIconSample() {
+ var value by savedInstanceState { "initial value" }
+ BasicTextField(
+ value = value,
+ value = it },
+ decorationBox = { innerTextField ->
+ // Because the decorationBox is used, the whole Row gets the same behaviour as the
+ // internal input field would have otherwise. For example, there is no need to add a
+ // Modifier.clickable to the Row anymore to bring the text field into focus when user
+ // taps on a larger text field area which includes paddings and the icon areas.
+ Row(
+ Modifier
+ .background(Color.LightGray, RoundedCornerShape(percent = 30))
+ .padding(16.dp)
+ ) {
+ Icon(Icons.Default.MailOutline, contentDescription = null)
+ Spacer(Modifier.width(16.dp))
+ innerTextField()
+ }
+ }
+ )
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ImageSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ImageSamples.kt
index 904f536..fd4f49f 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ImageSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ImageSamples.kt
@@ -43,7 +43,7 @@
fun ImageSample() {
val ImageBitmap = createTestImage()
// Lays out and draws an image sized to the dimensions of the ImageBitmap
- Image(bitmap = ImageBitmap)
+ Image(bitmap = ImageBitmap, contentDescription = "Localized description")
}
@Sampled
@@ -56,7 +56,8 @@
ImageBitmap,
IntOffset(10, 12),
IntSize(50, 60)
- )
+ ),
+ contentDescription = "Localized description"
)
}
@@ -67,6 +68,7 @@
imageVector.resource.resource?.let {
Image(
imageVector = it,
+ contentDescription = null,
modifier = Modifier.preferredSize(200.dp, 200.dp),
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(Color.Cyan)
@@ -89,7 +91,11 @@
}
}
- Image(painter = customPainter, modifier = Modifier.preferredSize(100.dp, 100.dp))
+ Image(
+ painter = customPainter,
+ contentDescription = "Localized description",
+ modifier = Modifier.preferredSize(100.dp, 100.dp)
+ )
}
/**
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index b49f36d..1e6f611 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -113,7 +114,11 @@
.background(color = Color.White)
.wrapContentSize(Alignment.Center)
) {
- Image(modifier = Modifier.testTag(contentTag), bitmap = createImageBitmap())
+ Image(
+ modifier = Modifier.testTag(contentTag),
+ contentDescription = null,
+ bitmap = createImageBitmap()
+ )
}
}
@@ -162,7 +167,8 @@
imageHeight / 2 - subsectionHeight / 2
),
IntSize(subsectionWidth, subsectionHeight)
- )
+ ),
+ null
)
}
}
@@ -258,6 +264,7 @@
// the bounds
Image(
bitmap = createImageBitmap(),
+ contentDescription = null,
modifier = Modifier
.testTag(contentTag)
.preferredSize(
@@ -325,6 +332,7 @@
) {
Image(
bitmap = ImageBitmap,
+ contentDescription = null,
modifier = Modifier
.testTag(contentTag)
.preferredSize(
@@ -357,6 +365,7 @@
// ImageBitmap that is to be drawn in the bottom end section of the composable
Image(
bitmap = createImageBitmap(),
+ contentDescription = null,
modifier = Modifier
.testTag(contentTag)
.preferredSize(
@@ -420,6 +429,7 @@
loadVectorResource(R.drawable.ic_vector_asset_test).resource.resource?.let {
Image(
it,
+ null,
modifier = Modifier.preferredSizeIn(
minWidth = minWidth,
minHeight = minHeight
@@ -505,6 +515,7 @@
val heightDp = asset.height / AmbientDensity.current.density
Image(
asset,
+ null,
modifier = Modifier
.testTag(testTag)
.background(Color.Green)
@@ -540,14 +551,15 @@
}
Image(
painterResource(painterId.value),
- contentScale = ContentScale.FillBounds,
+ null,
modifier = Modifier.testTag(testTag).clickable {
if (painterId.value == R.drawable.ic_vector_square_asset_test) {
painterId.value = R.drawable.ic_image_test
} else {
painterId.value = R.drawable.ic_vector_square_asset_test
}
- }
+ },
+ contentScale = ContentScale.FillBounds
)
}
@@ -559,4 +571,20 @@
rule.onNodeWithTag(testTag).captureToImage().assertPixels { imageColor }
}
+
+ @Test
+ fun testImageContentDescription() {
+ val testTag = "TestTag"
+ rule.setContent {
+ Image(
+ bitmap = ImageBitmap(100, 100),
+ modifier = Modifier.testTag(testTag),
+ contentDescription = "asdf"
+ )
+ }
+ rule.onNodeWithTag(testTag).fetchSemanticsNode().let {
+ Assert.assertTrue(it.config.contains(SemanticsProperties.ContentDescription))
+ Assert.assertEquals(it.config[SemanticsProperties.ContentDescription], "asdf")
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index 06041a9..32474cd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -28,11 +28,14 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextFieldScrollerPosition
+import androidx.compose.foundation.text.TextLayoutResultProxy
import androidx.compose.foundation.text.maxLinesHeight
import androidx.compose.foundation.text.textFieldScroll
import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.text.textFieldScrollable
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.testutils.assertPixels
@@ -45,6 +48,7 @@
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -61,7 +65,6 @@
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -73,6 +76,19 @@
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * These tests are for testing the text field scrolling modifiers [Modifier.textFieldScroll] and
+ * [Modifier.textFieldScrollable] working together.
+ * The tests are structured in a way that
+ * - two modifiers are applied to the text which exposes its [TextLayoutResult]
+ * - swipe gesture applied
+ * - [TextFieldScrollerPosition] state is checked to see if scrolling happened
+ * Previously we were able to test using CoreTextField. But with the decoration box change these
+ * two modifiers are already applied to the CoreTextField internally. Therefore we have no access
+ * to the [TextFieldScrollerPosition] object anymore. As such, CoreTextField was replaced with
+ * [BasicText] which is equivalent for testing these modifiers
+ */
+
@MediumTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalFoundationApi::class, InternalTextApi::class)
@@ -100,30 +116,12 @@
}
@Test
- fun testTextField_horizontallyScrollable_withLongInput() {
- val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
+ fun textFieldScroll_horizontal_scrollable_withLongInput() {
+ val scrollerPosition = TextFieldScrollerPosition(Orientation.Horizontal)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- softWrap = false,
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .maxLinesHeight(1, TextStyle.Default)
- .textFieldScroll(
- orientation = Orientation.Horizontal,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
- )
- }
+ rule.setupHorizontallyScrollableContent(
+ scrollerPosition, longText, Modifier.preferredSize(width = 300.dp, height = 50.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.maximum).isLessThan(Float.POSITIVE_INFINITY)
@@ -132,29 +130,14 @@
}
@Test
- fun testTextField_verticallyScrollable_withLongInput() {
+ fun textFieldScroll_vertical_scrollable_withLongInput() {
val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .maxLinesHeight(Int.MAX_VALUE, TextStyle.Default)
- .textFieldScroll(
- orientation = Orientation.Vertical,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef,
- )
- )
- }
+ rule.setupVerticallyScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = longText,
+ modifier = Modifier.preferredSize(width = 300.dp, height = 50.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.maximum).isLessThan(Float.POSITIVE_INFINITY)
@@ -163,29 +146,15 @@
}
@Test
- fun testTextField_verticallyScrollable_withLongInput_whenMaxLinesProvided() {
+ fun textFieldScroll_vertical_scrollable_withLongInput_whenMaxLinesProvided() {
val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredWidth(100.dp)
- .maxLinesHeight(3, TextStyle.Default)
- .textFieldScroll(
- orientation = Orientation.Vertical,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef,
- )
- )
- }
+ rule.setupVerticallyScrollableContent(
+ modifier = Modifier.preferredWidth(100.dp),
+ scrollerPosition = scrollerPosition,
+ text = longText,
+ maxLines = 3
+ )
rule.runOnIdle {
assertThat(scrollerPosition.maximum).isLessThan(Float.POSITIVE_INFINITY)
@@ -194,30 +163,14 @@
}
@Test
- fun testTextField_horizontallyNotScrollable_withShortInput() {
- val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue("text")
+ fun textFieldScroll_horizontal_notScrollable_withShortInput() {
+ val scrollerPosition = TextFieldScrollerPosition(Orientation.Horizontal)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- softWrap = false,
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .maxLinesHeight(1, TextStyle.Default)
- .textFieldScroll(
- orientation = Orientation.Horizontal,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef,
- )
- )
- }
+ rule.setupHorizontallyScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = "text",
+ modifier = Modifier.preferredSize(width = 300.dp, height = 50.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.maximum).isEqualTo(0f)
@@ -225,28 +178,14 @@
}
@Test
- fun testTextField_verticallyNotScrollable_withShortInput() {
+ fun textFieldScroll_vertical_notScrollable_withShortInput() {
val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue("text")
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 100.dp)
- .textFieldScroll(
- orientation = Orientation.Vertical,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef,
- )
- )
- }
+ rule.setupVerticallyScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = "text",
+ modifier = Modifier.preferredSize(width = 300.dp, height = 100.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.maximum).isEqualTo(0f)
@@ -256,37 +195,24 @@
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testTextField_horizontal_scrolledAndClipped() {
- val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
-
+ fun textField_singleLine_scrolledAndClipped() {
val parentSize = 200
val textFieldSize = 50
+ val tag = "OuterBox"
with(rule.density) {
rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
Box(
Modifier
.preferredSize(parentSize.toDp())
.background(color = Color.White)
- .testTag(TextfieldTag)
+ .testTag(tag)
) {
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- softWrap = false,
- modifier = Modifier
- .preferredSize(textFieldSize.toDp())
- .textFieldScroll(
- orientation = Orientation.Horizontal,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
+ ScrollableContent(
+ modifier = Modifier.preferredSize(textFieldSize.toDp()),
+ scrollerPosition = TextFieldScrollerPosition(Orientation.Horizontal),
+ text = longText,
+ isVertical = false
)
}
}
@@ -294,7 +220,7 @@
rule.runOnIdle {}
- rule.onNodeWithTag(TextfieldTag)
+ rule.onNodeWithTag(tag)
.captureToImage()
.assertPixels(expectedSize = IntSize(parentSize, parentSize)) { position ->
if (position.x > textFieldSize && position.y > textFieldSize) Color.White else null
@@ -304,36 +230,24 @@
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testTextField_vertical_scrolledAndClipped() {
- val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
-
+ fun textField_multiline_scrolledAndClipped() {
val parentSize = 200
val textFieldSize = 50
+ val tag = "OuterBox"
with(rule.density) {
rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
Box(
Modifier
.preferredSize(parentSize.toDp())
.background(color = Color.White)
- .testTag(TextfieldTag)
+ .testTag(tag)
) {
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredSize(textFieldSize.toDp())
- .textFieldScroll(
- orientation = Orientation.Vertical,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
+ ScrollableContent(
+ modifier = Modifier.preferredSize(textFieldSize.toDp()),
+ scrollerPosition = TextFieldScrollerPosition(),
+ text = longText,
+ isVertical = true
)
}
}
@@ -341,7 +255,7 @@
rule.runOnIdle {}
- rule.onNodeWithTag(TextfieldTag)
+ rule.onNodeWithTag(tag)
.captureToImage()
.assertPixels(expectedSize = IntSize(parentSize, parentSize)) { position ->
if (position.x > textFieldSize && position.y > textFieldSize) Color.White else null
@@ -349,32 +263,14 @@
}
@Test
- fun testTextField_horizontalScroll_swipe_whenLongInput() {
- val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
+ fun textFieldScroll_horizontal_swipe_whenLongInput() {
+ val scrollerPosition = TextFieldScrollerPosition(Orientation.Horizontal)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
-
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- softWrap = false,
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .testTag(TextfieldTag)
- .maxLinesHeight(1, TextStyle.Default)
- .textFieldScroll(
- Orientation.Horizontal,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
- )
- }
+ rule.setupHorizontallyScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = longText,
+ modifier = Modifier.preferredSize(width = 300.dp, height = 50.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.offset).isEqualTo(0f)
@@ -396,30 +292,14 @@
}
@Test
- fun testTextField_verticalScroll_swipe_whenLongInput() {
+ fun textFieldScroll_vertical_swipe_whenLongInput() {
val scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
- rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
-
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .testTag(TextfieldTag)
- .textFieldScroll(
- Orientation.Vertical,
- remember { scrollerPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
- )
- }
+ rule.setupVerticallyScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = longText,
+ modifier = Modifier.preferredSize(width = 300.dp, height = 50.dp)
+ )
rule.runOnIdle {
assertThat(scrollerPosition.offset).isEqualTo(0f)
@@ -441,36 +321,21 @@
}
@Test
- fun textFieldScroller_restoresScrollerPosition() {
+ fun textFieldScroll_restoresScrollerPosition() {
val restorationTester = StateRestorationTester(rule)
- var scrollerPosition = TextFieldScrollerPosition()
- val value = TextFieldValue(longText)
+ var scrollerPosition: TextFieldScrollerPosition? = null
restorationTester.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
-
scrollerPosition = rememberSavedInstanceState(
saver = TextFieldScrollerPosition.Saver
) {
- TextFieldScrollerPosition()
+ TextFieldScrollerPosition(Orientation.Horizontal)
}
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- softWrap = false,
- modifier = Modifier
- .preferredSize(width = 300.dp, height = 50.dp)
- .testTag(TextfieldTag)
- .maxLinesHeight(1, TextStyle.Default)
- .textFieldScroll(
- Orientation.Horizontal,
- scrollerPosition,
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- )
+ ScrollableContent(
+ modifier = Modifier.preferredSize(width = 300.dp, height = 50.dp),
+ scrollerPosition = scrollerPosition!!,
+ text = longText,
+ isVertical = false
)
}
@@ -478,53 +343,41 @@
.performGesture { swipeLeft() }
val swipePosition = rule.runOnIdle {
- scrollerPosition.offset
+ scrollerPosition!!.offset
}
assertThat(swipePosition).isGreaterThan(0f)
rule.runOnIdle {
scrollerPosition = TextFieldScrollerPosition()
- assertThat(scrollerPosition.offset).isEqualTo(0f)
+ assertThat(scrollerPosition!!.offset).isEqualTo(0f)
}
restorationTester.emulateSavedInstanceStateRestore()
rule.runOnIdle {
- assertThat(scrollerPosition.offset).isEqualTo(swipePosition)
+ assertThat(scrollerPosition!!.offset).isEqualTo(swipePosition)
}
}
@Test
- fun testInspectorValue() {
- val position = TextFieldScrollerPosition(initial = 10f)
- val orientation = Orientation.Vertical
- val value = TextFieldValue()
+ fun textFieldScrollable_testInspectorValue() {
+ val position = TextFieldScrollerPosition(Orientation.Vertical, 10f)
+ val interactionState = InteractionState()
rule.setContent {
- val modifier = Modifier.textFieldScroll(
- orientation,
- position,
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- Ref(),
- true
- ) as InspectableValue
- assertThat(modifier.nameFallback).isEqualTo("textFieldScroll")
+ val modifier =
+ Modifier.textFieldScrollable(position, interactionState) as InspectableValue
+ assertThat(modifier.nameFallback).isEqualTo("textFieldScrollable")
assertThat(modifier.valueOverride).isNull()
assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
- "orientation",
"scrollerPosition",
- "textFieldValue",
- "visualTransformation",
"interactionState",
- "textLayoutResult",
"enabled"
)
}
}
@Test
- fun testNestedScrolling() {
+ fun textFieldScroll_testNestedScrolling() {
val size = 300.dp
val text = """
First Line
@@ -532,7 +385,6 @@
Third Line
Fourth Line
""".trimIndent()
- val value = TextFieldValue(text)
val textFieldScrollPosition = TextFieldScrollerPosition()
val scrollerPosition = ScrollState(
@@ -542,28 +394,16 @@
)
rule.setContent {
- val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
Column(
Modifier
.preferredSize(size)
.verticalScroll(scrollerPosition)
) {
- CoreTextField(
- value = value,
- >
- textLayoutResultRef.value = it },
- modifier = Modifier
- .preferredSize(size, 50.dp)
- .testTag(TextfieldTag)
- .textFieldScroll(
- Orientation.Vertical,
- remember { textFieldScrollPosition },
- value,
- VisualTransformation.None,
- remember { InteractionState() },
- textLayoutResultRef
- ),
- textStyle = TextStyle(fontSize = 20.sp)
+ ScrollableContent(
+ modifier = Modifier.preferredSize(size, 50.dp),
+ scrollerPosition = textFieldScrollPosition,
+ text = text,
+ isVertical = true
)
Box(Modifier.preferredSize(size))
Box(Modifier.preferredSize(size))
@@ -593,8 +433,72 @@
rule.runOnIdle {
assertThat(textFieldScrollPosition.offset).isGreaterThan(0f)
- assertThat(textFieldScrollPosition.offset).isEqualTo(textFieldScrollPosition.maximum)
+ assertThat(textFieldScrollPosition.offset)
+ .isWithin(0.5f).of(textFieldScrollPosition.maximum)
assertThat(scrollerPosition.value).isGreaterThan(0f)
}
}
+
+ private fun ComposeTestRule.setupHorizontallyScrollableContent(
+ scrollerPosition: TextFieldScrollerPosition,
+ text: String,
+ modifier: Modifier = Modifier
+ ) {
+ setContent {
+ ScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = text,
+ isVertical = false,
+ modifier = modifier,
+ maxLines = 1
+ )
+ }
+ }
+
+ private fun ComposeTestRule.setupVerticallyScrollableContent(
+ scrollerPosition: TextFieldScrollerPosition,
+ text: String,
+ modifier: Modifier = Modifier,
+ maxLines: Int = Int.MAX_VALUE
+ ) {
+ setContent {
+ ScrollableContent(
+ scrollerPosition = scrollerPosition,
+ text = text,
+ isVertical = true,
+ modifier = modifier,
+ maxLines = maxLines
+ )
+ }
+ }
+
+ @Composable
+ private fun ScrollableContent(
+ modifier: Modifier,
+ scrollerPosition: TextFieldScrollerPosition,
+ text: String,
+ isVertical: Boolean,
+ maxLines: Int = Int.MAX_VALUE
+ ) {
+ val textLayoutResultRef: Ref<TextLayoutResultProxy?> = remember { Ref() }
+ val resolvedMaxLines = if (isVertical) maxLines else 1
+
+ BasicText(
+ text = text,
+ >
+ textLayoutResultRef.value = TextLayoutResultProxy(it)
+ },
+ softWrap = isVertical,
+ modifier = modifier
+ .testTag(TextfieldTag)
+ .maxLinesHeight(resolvedMaxLines, TextStyle.Default)
+ .textFieldScrollable(scrollerPosition)
+ .textFieldScroll(
+ remember { scrollerPosition },
+ TextFieldValue(text),
+ VisualTransformation.None,
+ { textLayoutResultRef.value }
+ )
+ )
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 5aa9fe7..ca6be5a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -21,11 +21,18 @@
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
@@ -40,6 +47,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.onGloballyPositioned
@@ -55,6 +63,7 @@
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.click
import androidx.compose.ui.test.hasImeAction
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.isFocused
@@ -63,6 +72,7 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
@@ -77,6 +87,7 @@
import androidx.compose.ui.text.input.TextFieldValue.Companion.Saver
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -636,4 +647,42 @@
assertThat(lastSeenText).isEqualTo("")
}
}
+
+ @Test
+ fun decorationBox_clickable() {
+ val interactionState = InteractionState()
+ rule.setContent {
+ Column {
+ BasicTextField(
+ value = "test",
+ >
+ textStyle = TextStyle(fontSize = 2.sp),
+ modifier = Modifier.height(100.dp).fillMaxWidth(),
+ decorationBox = {
+ // the core text field is at the very bottom
+ Column {
+ BasicText("Label", Modifier.testTag("label"))
+ Spacer(Modifier.weight(1f))
+ it()
+ }
+ },
+ interactionState = interactionState
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(interactionState.contains(Interaction.Focused)).isFalse()
+ }
+
+ // click outside core text field area
+ rule.onNodeWithTag("label")
+ .performGesture {
+ click(Offset.Zero)
+ }
+
+ rule.runOnIdle {
+ assertThat(interactionState.contains(Interaction.Focused)).isTrue()
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Image.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Image.kt
index 32e24c0..72f9598 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Image.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Image.kt
@@ -33,6 +33,8 @@
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
/**
* A composable that lays out and draws a given [ImageBitmap]. This will attempt to
@@ -49,13 +51,17 @@
* overload that consumes a [Painter] parameter shown in this sample
* @sample androidx.compose.foundation.samples.ImagePainterSubsectionSample
*
- * @param bitmap The [ImageBitmap] to draw.
+ * @param bitmap The [ImageBitmap] to draw
+ * @param contentDescription text used by accessibility services to describe what this image
+ * represents. This should always be provided unless this image is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex.
* background)
* @param alignment Optional alignment parameter used to place the [ImageBitmap] in the given
- * bounds defined by the width and height.
+ * bounds defined by the width and height
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used
- * if the bounds are a different size from the intrinsic size of the [ImageBitmap].
+ * if the bounds are a different size from the intrinsic size of the [ImageBitmap]
* @param alpha Optional opacity to be applied to the [ImageBitmap] when it is rendered onscreen
* @param colorFilter Optional ColorFilter to apply for the [ImageBitmap] when it is rendered
* onscreen
@@ -64,6 +70,7 @@
@Composable
inline fun Image(
bitmap: ImageBitmap,
+ contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
@@ -73,6 +80,7 @@
val imagePainter = remember(bitmap) { ImagePainter(bitmap) }
Image(
painter = imagePainter,
+ contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
@@ -90,13 +98,17 @@
*
* @sample androidx.compose.foundation.samples.ImageVectorSample
*
- * @param imageVector The [ImageVector] to draw.
+ * @param imageVector The [ImageVector] to draw
+ * @param contentDescription text used by accessibility services to describe what this image
+ * represents. This should always be provided unless this image is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex.
* background)
* @param alignment Optional alignment parameter used to place the [ImageVector] in the given
- * bounds defined by the width and height.
+ * bounds defined by the width and height
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used
- * if the bounds are a different size from the intrinsic size of the [ImageVector].
+ * if the bounds are a different size from the intrinsic size of the [ImageVector]
* @param alpha Optional opacity to be applied to the [ImageVector] when it is rendered onscreen
* @param colorFilter Optional ColorFilter to apply for the [ImageVector] when it is rendered
* onscreen
@@ -105,6 +117,7 @@
@Composable
inline fun Image(
imageVector: ImageVector,
+ contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
@@ -112,6 +125,7 @@
colorFilter: ColorFilter? = null
) = Image(
painter = rememberVectorPainter(imageVector),
+ contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
@@ -132,12 +146,16 @@
* @sample androidx.compose.foundation.samples.ImagePainterSample
*
* @param painter to draw
+ * @param contentDescription text used by accessibility services to describe what this image
+ * represents. This should always be provided unless this image is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex.
* background)
* @param alignment Optional alignment parameter used to place the [Painter] in the given
* bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used
- * if the bounds are a different size from the intrinsic size of the [Painter].
+ * if the bounds are a different size from the intrinsic size of the [Painter]
* @param alpha Optional opacity to be applied to the [Painter] when it is rendered onscreen
* the default renders the [Painter] completely opaque
* @param colorFilter Optional colorFilter to apply for the [Painter] when it is rendered onscreen
@@ -145,17 +163,24 @@
@Composable
fun Image(
painter: Painter,
+ contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
+ val semantics = if (contentDescription != null) {
+ Modifier.semantics { this.contentDescription = contentDescription }
+ } else {
+ Modifier
+ }
+
// Explicitly use a simple Layout implementation here as Spacer squashes any non fixed
// constraint with zero
Layout(
emptyContent(),
- modifier.clipToBounds().paint(
+ modifier.then(semantics).clipToBounds().paint(
painter,
alignment = alignment,
contentScale = contentScale,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 1c2db5d..a3428b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -22,12 +22,9 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.node.Ref
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.SoftwareKeyboardController
@@ -67,9 +64,14 @@
* composable is designed to be used when a custom implementation for different design system is
* needed.
*
- * For example, if you need to include a hint in your TextField you can write a composable as below:
+ * For example, if you need to include a placeholder in your TextField, you can write a composable
+ * using the decoration box like this:
* @sample androidx.compose.foundation.samples.PlaceholderBasicTextFieldSample
*
+ * If you want to add decorations to your text field, such as icon or similar, and increase the
+ * hit target area, use the decoration box:
+ * @sample androidx.compose.foundation.samples.TextFieldWithIconSample
+ *
* @param value the input [String] text to be shown in the text field
* @param onValueChange the callback that is triggered when the input service updates the text. An
* updated text comes as a parameter of the callback
@@ -77,8 +79,8 @@
* @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text
* field will be neither editable nor focusable, the input of the text field will not be selectable
* @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text
- * fields will not be editable but otherwise operable. Read-only text fields are usually used to
- * display the pre-filled text that user cannot edit
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle Style configuration that applies at character level such as color, font etc.
* @param keyboardOptions software keyboard options that contains configuration such as
* [KeyboardType] and [ImeAction].
@@ -104,6 +106,12 @@
* if you want to read the [InteractionState] and customize the appearance / behavior of this
* TextField in different [Interaction]s.
* @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
+ * @param decorationBox Composable lambda that allows to add decorations around text field, such
+ * as icon, placeholder, helper messages or similar, and automatically increase the hit target area
+ * of the text field. To allow you to control the placement of the inner text field relative to your
+ * decorations, the text field implementation will pass in a framework-controlled composable
+ * parameter "innerTextField" to the decorationBox lambda you provide. You must call
+ * innerTextField exactly once.
*/
@OptIn(ExperimentalTextApi::class)
@Composable
@@ -122,7 +130,9 @@
onTextLayout: (TextLayoutResult) -> Unit = {},
onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
interactionState: InteractionState = remember { InteractionState() },
- cursorColor: Color = Color.Black
+ cursorColor: Color = Color.Black,
+ decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+ @Composable { innerTextField -> innerTextField() }
) {
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
val textFieldValue = textFieldValueState.copy(text = value)
@@ -147,7 +157,8 @@
>
cursorColor = cursorColor,
interactionState = interactionState,
- singleLine = singleLine
+ singleLine = singleLine,
+ decorationBox = decorationBox
)
}
@@ -178,9 +189,15 @@
* composable is designed to be used when a custom implementation for different design system is
* needed.
*
- * For example, if you need to include a hint in your TextField you can write a composable as below:
+ * For example, if you need to include a placeholder in your TextField, you can write a composable
+ * using the decoration box like this:
* @sample androidx.compose.foundation.samples.PlaceholderBasicTextFieldSample
*
+ *
+ * If you want to add decorations to your text field, such as icon or similar, and increase the
+ * hit target area, use the decoration box:
+ * @sample androidx.compose.foundation.samples.TextFieldWithIconSample
+ *
* @param value The [androidx.compose.ui.text.input.TextFieldValue] to be shown in the
* [BasicTextField].
* @param onValueChange Called when the input service updates the values in [TextFieldValue].
@@ -188,8 +205,8 @@
* @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text
* field will be neither editable nor focusable, the input of the text field will not be selectable
* @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text
- * fields will not be editable but otherwise operable. Read-only text fields are usually used to
- * display the pre-filled text that user cannot edit
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle Style configuration that applies at character level such as color, font etc.
* @param keyboardOptions software keyboard options that contains configuration such as
* [KeyboardType] and [ImeAction].
@@ -215,6 +232,12 @@
* if you want to read the [InteractionState] and customize the appearance / behavior of this
* TextField in different [Interaction]s.
* @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
+ * @param decorationBox Composable lambda that allows to add decorations around text field, such
+ * as icon, placeholder, helper messages or similar, and automatically increase the hit target area
+ * of the text field. To allow you to control the placement of the inner text field relative to your
+ * decorations, the text field implementation will pass in a framework-controlled composable
+ * parameter "innerTextField" to the decorationBox lambda you provide. You must call
+ * innerTextField exactly once.
*/
@Composable
@OptIn(InternalTextApi::class, ExperimentalTextApi::class)
@@ -233,60 +256,26 @@
onTextLayout: (TextLayoutResult) -> Unit = {},
onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
interactionState: InteractionState = remember { InteractionState() },
- cursorColor: Color = Color.Black
+ cursorColor: Color = Color.Black,
+ decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+ @Composable { innerTextField -> innerTextField() }
) {
- val orientation = if (singleLine) Orientation.Horizontal else Orientation.Vertical
- val scrollerPosition = rememberSavedInstanceState(saver = TextFieldScrollerPosition.Saver) {
- TextFieldScrollerPosition()
- }
-
- // We use it to get the cursor position
- val textLayoutResult: Ref<TextLayoutResult?> = remember { Ref() }
-
- val textFieldModifier = modifier
- .maxLinesHeight(if (singleLine) 1 else maxLines, textStyle)
- .textFieldScroll(
- orientation,
- scrollerPosition,
- value,
- visualTransformation,
- interactionState,
- textLayoutResult,
- enabled
- )
-
- if (enabled && !readOnly) {
- CoreTextField(
- value = value,
- >
- textStyle = textStyle,
- >
- visualTransformation = visualTransformation,
- >
- textLayoutResult.value = it
- onTextLayout(it)
- },
- interactionState = interactionState,
- >
- cursorColor = cursorColor,
- imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
- softWrap = !singleLine,
- modifier = textFieldModifier
- )
- } else {
- InactiveTextField(
- value = value,
- modifier = textFieldModifier,
- enabled = enabled,
- textStyle = textStyle,
- singleLine = singleLine,
- maxLines = maxLines,
- visualTransformation = visualTransformation,
- >
- textLayoutResult.value = it
- onTextLayout(it)
- },
- interactionState = interactionState
- )
- }
+ CoreTextField(
+ value = value,
+ >
+ modifier = modifier,
+ textStyle = textStyle,
+ >
+ visualTransformation = visualTransformation,
+ >
+ interactionState = interactionState,
+ >
+ cursorColor = cursorColor,
+ imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
+ softWrap = !singleLine,
+ maxLines = if (singleLine) 1 else maxLines,
+ decorationBox = decorationBox,
+ enabled = enabled,
+ readOnly = readOnly
+ )
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 444f974..a0ac2a4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -19,7 +19,7 @@
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
-import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.selection.TextFieldSelectionHandle
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
import androidx.compose.foundation.text.selection.isSelectionHandleInVisibleBound
@@ -30,16 +30,13 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
+import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.gesture.dragGestureFilter
-import androidx.compose.ui.gesture.longPressDragGestureFilter
-import androidx.compose.ui.gesture.tapGestureFilter
+import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
@@ -60,6 +57,7 @@
import androidx.compose.ui.selection.SimpleLayout
import androidx.compose.ui.semantics.copyText
import androidx.compose.ui.semantics.cutText
+import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.imeAction
import androidx.compose.ui.semantics.onClick
@@ -83,7 +81,9 @@
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.NO_SESSION
+import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Density
import kotlin.math.max
@@ -95,6 +95,11 @@
* This composable provides basic text editing functionality, however does not include any
* decorations such as borders, hints/placeholder.
*
+ * If the editable text is larger than the size of the container, the vertical scrolling
+ * behaviour will be automatically applied. To enable a single line behaviour with horizontal
+ * scrolling instead, set the [maxLines] parameter to 1, [softWrap] to false, and
+ * [ImeOptions.singleLine] to true.
+ *
* Whenever the user edits the text, [onValueChange] is called with the most up to date state
* represented by [TextFieldValue]. [TextFieldValue] contains the text entered by user, as well
* as selection, cursor and text composition information. Please check [TextFieldValue] for the
@@ -131,6 +136,20 @@
* @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
* @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
* text will be positioned as if there was unlimited horizontal space.
+ * @param maxLines The maximum height in terms of maximum number of visible lines. Should be
+ * equal or greater than 1.
+ * @param imeOptions Contains different IME configuration options.
+ * @param enabled controls the enabled state of the text field. When `false`, the text
+ * field will be neither editable nor focusable, the input of the text field will not be selectable
+ * @param readOnly controls the editable state of the [CoreTextField]. When `true`, the text
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
+ * @param decorationBox Composable lambda that allows to add decorations around text field, such
+ * as icon, placeholder, helper messages or similar, and automatically increase the hit target area
+ * of the text field. To allow you to control the placement of the inner text field relative to your
+ * decorations, the text field implementation will pass in a framework-controlled composable
+ * parameter "innerTextField" to the decorationBox lambda you provide. You must call
+ * innerTextField exactly once.
*/
@Composable
@OptIn(
@@ -150,7 +169,12 @@
interactionState: InteractionState? = null,
cursorColor: Color = Color.Unspecified,
softWrap: Boolean = true,
- imeOptions: ImeOptions = ImeOptions.Default
+ maxLines: Int = Int.MAX_VALUE,
+ imeOptions: ImeOptions = ImeOptions.Default,
+ enabled: Boolean = true,
+ readOnly: Boolean = false,
+ decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+ @Composable { innerTextField -> innerTextField() }
) {
// If developer doesn't pass new value to TextField, recompose won't happen but internal state
// and IME may think it is updated. To fix this inconsistent state, enforce recompose.
@@ -158,11 +182,20 @@
val focusRequester = FocusRequester()
// Ambients
- val textInputService = AmbientTextInputService.current
+ // If the text field is disabled or read-only, we should not deal with the input service
+ val textInputService = if (!enabled || readOnly) null else AmbientTextInputService.current
val density = AmbientDensity.current
val resourceLoader = AmbientFontLoader.current
val selectionBackgroundColor = AmbientTextSelectionColors.current.backgroundColor
+ // Scroll state
+ val singleLine = maxLines == 1 && !softWrap && imeOptions.singleLine
+ val orientation = if (singleLine) Orientation.Horizontal else Orientation.Vertical
+ val scrollerPosition = rememberSavedInstanceState(
+ orientation,
+ saver = TextFieldScrollerPosition.Saver
+ ) { TextFieldScrollerPosition(orientation) }
+
// State
val transformedText = remember(value, visualTransformation) {
val transformed = visualTransformation.filter(value.annotatedString)
@@ -215,70 +248,44 @@
manager.textToolbar = AmbientTextToolbar.current
manager.hapticFeedBack = AmbientHapticFeedback.current
manager.focusRequester = focusRequester
+ manager.editable = !readOnly
- val {
+ // Focus
+ val focusModifier = Modifier.textFieldFocusModifier(
+ enabled = enabled,
+ focusRequester = focusRequester,
+ interactionState = interactionState
+ ) {
if (state.hasFocus == it.isFocused) {
- return@onFocusChanged
+ return@textFieldFocusModifier
}
-
state.hasFocus = it.isFocused
- if (it.isFocused) {
- state.inputSession = TextFieldDelegate.onFocus(
+ if (textInputService != null) {
+ notifyTextInputServiceOnFocusChange(
textInputService,
+ state,
value,
- state.processor,
imeOptions,
onValueChangeWrapper,
- onImeActionPerformedWrapper
+ onImeActionPerformedWrapper,
+ onTextInputStarted,
+ offsetMapping
)
- if (state.inputSession != NO_SESSION && textInputService != null) {
- onTextInputStarted(
- SoftwareKeyboardController(
- textInputService,
- state.inputSession
- )
- )
- }
- state.layoutCoordinates?.let { coords ->
- textInputService?.let { textInputService ->
- state.layoutResult?.let { layoutResult ->
- TextFieldDelegate.notifyFocusedRect(
- value,
- state.textDelegate,
- layoutResult,
- coords,
- textInputService,
- state.inputSession,
- state.hasFocus,
- offsetMapping
- )
- }
- }
- }
- } else {
- TextFieldDelegate.onBlur(
- textInputService,
- state.inputSession,
- state.processor,
- false,
- onValueChangeWrapper
- )
- manager.deselect()
}
+ if (!it.isFocused) manager.deselect()
}
- val focusRequestTapModifier = Modifier.tapGestureFilter {
- if (!state.hasFocus) {
- focusRequester.requestFocus()
- } else {
- // if already focused make sure tap request keyboard.
- textInputService?.showSoftwareKeyboard(state.inputSession)
+ val focusRequestTapModifier = Modifier.focusRequestTapModifier(
+ enabled = enabled,
+ >
+ tapToFocus(state, focusRequester, textInputService, !readOnly)
}
- }
+ )
val dragPositionGestureModifier = Modifier.dragPositionGestureFilter(
interactionState = interactionState,
+ enabled = enabled,
>
if (state.hasFocus) {
state.selectionIsOn = false
@@ -300,15 +307,15 @@
}
)
+ val selectionModifier =
+ Modifier.longPressDragGestureFilter(manager.touchSelectionObserver, enabled)
+
val pointerModifier = if (isMouseInput) {
- Modifier.dragGestureFilter(
+ Modifier.mouseDragGestureFilter(
manager.mouseSelectionObserver( focusRequester.requestFocus() }),
- startDragImmediately = true
+ enabled = enabled
)
} else {
- val selectionModifier = Modifier.longPressDragGestureFilter(
- manager.touchSelectionObserver
- )
dragPositionGestureModifier
.then(selectionModifier)
.then(focusRequestTapModifier)
@@ -321,7 +328,7 @@
canvas,
value,
offsetMapping,
- layoutResult,
+ layoutResult.value,
state.selectionPaint
)
}
@@ -344,7 +351,7 @@
TextFieldDelegate.notifyFocusedRect(
value,
state.textDelegate,
- layoutResult,
+ layoutResult.value,
it,
textInputService,
state.inputSession,
@@ -353,6 +360,7 @@
)
}
}
+ state.layoutResult?.innerTextFieldCoordinates = it
}
val semanticsModifier = Modifier.semantics {
@@ -360,9 +368,10 @@
this.imeAction = imeOptions.imeAction
this.text = value.annotatedString
this.textSelectionRange = value.selection
+ if (!enabled) this.disabled()
getTextLayoutResult {
if (state.layoutResult != null) {
- it.add(state.layoutResult!!)
+ it.add(state.layoutResult!!.value)
true
} else {
false
@@ -373,7 +382,9 @@
true
}
setSelection { start, end, traversalMode ->
- if (start == value.selection.start && end == value.selection.end) {
+ if (!enabled) {
+ false
+ } else if (start == value.selection.start && end == value.selection.end) {
false
} else if (start.coerceAtMost(end) >= 0 &&
start.coerceAtLeast(end) <= value.annotatedString.length
@@ -393,11 +404,9 @@
}
}
onClick {
- if (!state.hasFocus) {
- focusRequester.requestFocus()
- } else {
- textInputService?.showSoftwareKeyboard(state.inputSession)
- }
+ // according to the documentation, we still need to provide proper semantics actions
+ // even if the state is 'disabled'
+ tapToFocus(state, focusRequester, textInputService, !readOnly)
true
}
onLongClick {
@@ -409,99 +418,88 @@
manager.copy()
true
}
- cutText {
- manager.cut()
+ if (enabled && !readOnly) {
+ cutText {
+ manager.cut()
+ true
+ }
+ }
+ }
+ if (enabled && !readOnly) {
+ pasteText {
+ manager.paste()
true
}
}
- pasteText {
- manager.paste()
- true
- }
}
- val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorColor)
+ val cursorModifier =
+ Modifier.cursor(state, value, offsetMapping, cursorColor, enabled && !readOnly)
onDispose { manager.hideSelectionToolbar() }
- val modifiers = modifier
- .focusRequester(focusRequester)
- .then(onFocusChanged)
- .then(cursorModifier)
+ // Modifiers that should be applied to the outer text field container. Usually those include
+ // gesture and semantics modifiers.
+ val decorationBoxModifier = modifier
.then(pointerModifier)
- .then(drawModifier)
- .then(onPositionedModifier)
+ .textFieldScrollable(scrollerPosition, interactionState, enabled)
.then(semanticsModifier)
- .textFieldMinSize(textStyle)
- .textFieldKeyboardModifier(manager)
- .focusable(interactionState = interactionState)
-
- SimpleLayout(modifiers) {
- Layout(emptyContent()) { _, constraints ->
- TextFieldDelegate.layout(
- state.textDelegate,
- constraints,
- layoutDirection,
- state.layoutResult
- ).let { (width, height, result) ->
- if (state.layoutResult != result) {
- state.layoutResult = result
- onTextLayout(result)
- }
- layout(
- width,
- height,
- mapOf(
- FirstBaseline to result.firstBaseline.roundToInt(),
- LastBaseline to result.lastBaseline.roundToInt()
- )
- ) {}
- }
+ .then(focusModifier)
+ .onGloballyPositioned {
+ state.layoutResult?.decorationBoxCoordinates = it
}
- if (
- state.hasFocus &&
- state.selectionIsOn &&
- state.layoutCoordinates != null &&
- state.layoutCoordinates!!.isAttached &&
- !isMouseInput
- ) {
- manager.state?.layoutResult?.let {
- if (!value.selection.collapsed) {
- val startOffset = offsetMapping.originalToTransformed(value.selection.start)
- val endOffset = offsetMapping.originalToTransformed(value.selection.end)
- val startDirection = it.getBidiRunDirection(startOffset)
- val endDirection = it.getBidiRunDirection(max(endOffset - 1, 0))
- val directions = Pair(startDirection, endDirection)
- if (state.showSelectionHandleStart) {
- TextFieldSelectionHandle(
- isStartHandle = true,
- directions = directions,
- manager = manager
- )
- }
- if (state.showSelectionHandleEnd) {
- TextFieldSelectionHandle(
- isStartHandle = false,
- directions = directions,
- manager = manager
- )
+ Box(modifier = decorationBoxModifier, propagateMinConstraints = true) {
+ decorationBox {
+ // Modifiers applied directly to the internal input field implementation. In general,
+ // these will most likely include draw, layout and IME related modifiers.
+ val coreTextFieldModifier = Modifier
+ .maxLinesHeight(maxLines, textStyle)
+ .textFieldScroll(
+ scrollerPosition,
+ value,
+ visualTransformation,
+ { state.layoutResult }
+ )
+ .then(cursorModifier)
+ .then(drawModifier)
+ .then(onPositionedModifier)
+ .textFieldMinSize(textStyle)
+ .textFieldKeyboardModifier(manager)
+
+ SimpleLayout(coreTextFieldModifier) {
+ Layout(emptyContent()) { _, constraints ->
+ TextFieldDelegate.layout(
+ state.textDelegate,
+ constraints,
+ layoutDirection,
+ state.layoutResult?.value
+ ).let { (width, height, result) ->
+ if (state.layoutResult?.value != result) {
+ state.layoutResult = TextLayoutResultProxy(result)
+ onTextLayout(result)
+ }
+ layout(
+ width,
+ height,
+ mapOf(
+ FirstBaseline to result.firstBaseline.roundToInt(),
+ LastBaseline to result.lastBaseline.roundToInt()
+ )
+ ) {}
}
}
- manager.state?.let {
- // If in selection mode (when the floating toolbar is shown) a new symbol
- // from the keyboard is entered, text field should enter the editing mode
- // instead.
- if (manager.isTextChanged()) it.showFloatingToolbar = false
- if (it.hasFocus) {
- if (it.showFloatingToolbar) manager.showSelectionToolbar()
- else manager.hideSelectionToolbar()
- }
- }
+ SelectionToolbarAndHandles(
+ manager = manager,
+ show = enabled &&
+ state.hasFocus &&
+ state.selectionIsOn &&
+ state.layoutCoordinates != null &&
+ state.layoutCoordinates!!.isAttached &&
+ !isMouseInput
+ )
}
- } else {
- manager.hideSelectionToolbar()
}
}
}
@@ -524,8 +522,18 @@
/** The last layout coordinates for the Text's layout, used by selection */
var layoutCoordinates: LayoutCoordinates? = null
- /** The latest TextLayoutResult calculated in the measure block */
- var layoutResult: TextLayoutResult? = null
+ /**
+ * You should be using proxy type [TextLayoutResultProxy] if you need to translate touch
+ * offset into text's coordinate system. For example, if you add a gesture on top of the
+ * decoration box and want to know the character in text for the given touch offset on
+ * decoration box.
+ * When you don't need to shift the touch offset, you should be using `layoutResult.value`
+ * which omits the proxy and calls the layout result directly. This is needed when you work
+ * with the text directly, and not the decoration box. For example, cursor modifier gets
+ * position using the [TextFieldValue.selection] value which corresponds to the text directly,
+ * and therefore does not require the translation.
+ */
+ var layoutResult: TextLayoutResultProxy? = null
/**
* The gesture detector status, to indicate whether current status is selection or editing.
@@ -595,4 +603,110 @@
placeholders = emptyList()
)
}
+}
+
+/**
+ * Request focus on tap. If already focused, makes sure the keyboard is requested.
+ */
+private fun tapToFocus(
+ state: TextFieldState,
+ focusRequester: FocusRequester,
+ textInputService: TextInputService?,
+ allowKeyboard: Boolean
+) {
+ if (!state.hasFocus) {
+ focusRequester.requestFocus()
+ } else if (allowKeyboard) {
+ textInputService?.showSoftwareKeyboard(state.inputSession)
+ }
+}
+
+@OptIn(InternalTextApi::class)
+private fun notifyTextInputServiceOnFocusChange(
+ textInputService: TextInputService,
+ state: TextFieldState,
+ value: TextFieldValue,
+ imeOptions: ImeOptions,
+ onValueChange: (TextFieldValue) -> Unit,
+ onImeActionPerformed: (ImeAction) -> Unit,
+ onTextInputStarted: (SoftwareKeyboardController) -> Unit,
+ offsetMapping: OffsetMapping
+) {
+ if (state.hasFocus) {
+ state.inputSession = TextFieldDelegate.onFocus(
+ textInputService,
+ value,
+ state.processor,
+ imeOptions,
+ onValueChange,
+ onImeActionPerformed
+ )
+ if (state.inputSession != NO_SESSION) {
+ onTextInputStarted(SoftwareKeyboardController(textInputService, state.inputSession))
+ }
+ state.layoutCoordinates?.let { coords ->
+ state.layoutResult?.let { layoutResult ->
+ TextFieldDelegate.notifyFocusedRect(
+ value,
+ state.textDelegate,
+ layoutResult.value,
+ coords,
+ textInputService,
+ state.inputSession,
+ state.hasFocus,
+ offsetMapping
+ )
+ }
+ }
+ } else {
+ TextFieldDelegate.onBlur(
+ textInputService,
+ state.inputSession,
+ state.processor,
+ false,
+ onValueChange
+ )
+ }
+}
+
+@Composable
+private fun SelectionToolbarAndHandles(manager: TextFieldSelectionManager, show: Boolean) {
+ if (show) {
+ with(manager) {
+ state?.layoutResult?.value?.let {
+ if (!value.selection.collapsed) {
+ val startOffset = offsetMapping.originalToTransformed(value.selection.start)
+ val endOffset = offsetMapping.originalToTransformed(value.selection.end)
+ val startDirection = it.getBidiRunDirection(startOffset)
+ val endDirection = it.getBidiRunDirection(max(endOffset - 1, 0))
+ val directions = Pair(startDirection, endDirection)
+ if (manager.state?.showSelectionHandleStart == true) {
+ TextFieldSelectionHandle(
+ isStartHandle = true,
+ directions = directions,
+ manager = manager
+ )
+ }
+ if (manager.state?.showSelectionHandleEnd == true) {
+ TextFieldSelectionHandle(
+ isStartHandle = false,
+ directions = directions,
+ manager = manager
+ )
+ }
+ }
+
+ state?.let { textFieldState ->
+ // If in selection mode (when the floating toolbar is shown) a new symbol
+ // from the keyboard is entered, text field should enter the editing mode
+ // instead.
+ if (isTextChanged()) textFieldState.showFloatingToolbar = false
+ if (textFieldState.hasFocus) {
+ if (textFieldState.showFloatingToolbar) showSelectionToolbar()
+ else hideSelectionToolbar()
+ }
+ }
+ }
+ }
+ } else manager.hideSelectionToolbar()
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index 93c94c3..895a836 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -35,6 +35,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.input.OffsetMapping
@@ -47,14 +48,15 @@
state: TextFieldState,
value: TextFieldValue,
offsetMapping: OffsetMapping,
- cursorColor: Color
-) = composed {
+ cursorColor: Color,
+ enabled: Boolean
+) = if (enabled) composed {
// this should be a disposable clock, but it's not available in this module
// however, we only launch one animation and guarantee that we stop it (via snap) in dispose
val animationClocks = AmbientAnimationClock.current
val cursorAlpha = remember(animationClocks) { AnimatedFloatModel(0f, animationClocks) }
- if (state.hasFocus && value.selection.collapsed && cursorColor != Color.Unspecified) {
+ if (state.hasFocus && value.selection.collapsed && cursorColor.isSpecified) {
onCommit(cursorColor, value.annotatedString) {
if (@Suppress("DEPRECATION_ERROR") blinkingCursorEnabled) {
cursorAlpha.animateTo(0f, anim = cursorAnimationSpec)
@@ -71,7 +73,7 @@
if (cursorAlphaValue != 0f) {
val transformedOffset = offsetMapping
.originalToTransformed(value.selection.start)
- val cursorRect = state.layoutResult?.getCursorRect(transformedOffset)
+ val cursorRect = state.layoutResult?.value?.getCursorRect(transformedOffset)
?: Rect(0f, 0f, 0f, 0f)
val cursorWidth = DefaultCursorThickness.toPx()
val cursorX = (cursorRect.left + cursorWidth / 2)
@@ -89,7 +91,7 @@
} else {
Modifier
}
-}
+} else this
@Stable
private class AnimatedFloatModel(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
index 79f0ea9..e8036e0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
@@ -214,7 +214,7 @@
* Sets the cursor position. Should be called when TextField has focus.
*
* @param position The event position in composable coordinate.
- * @param textLayoutResult The text layout result
+ * @param textLayoutResult The text layout result proxy
* @param editProcessor The edit processor
* @param offsetMapping The offset map
* @param onValueChange The callback called when the new editor state arrives.
@@ -222,7 +222,7 @@
@JvmStatic
internal fun setCursorOffset(
position: Offset,
- textLayoutResult: TextLayoutResult,
+ textLayoutResult: TextLayoutResultProxy,
editProcessor: EditProcessor,
offsetMapping: OffsetMapping,
onValueChange: (TextFieldValue) -> Unit
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
index 560431f..94844bc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
@@ -35,7 +35,8 @@
onPress: (Offset) -> Unit,
onRelease: (Offset) -> Unit,
interactionState: InteractionState?,
-): Modifier = composed {
+ enabled: Boolean = true
+): Modifier = if (enabled) composed {
val tracker = remember { DragEventTracker() }
// TODO(shepshapard): PressIndicator doesn't seem to be the right thing to use here. It
// actually may be functionally correct, but might mostly suggest that it should not
@@ -66,7 +67,7 @@
}
}
)
-}
+} else this
/**
* Helper class for tracking dragging event.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
new file mode 100644
index 0000000..b7abf97
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.focusable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.DragObserver
+import androidx.compose.ui.gesture.LongPressDragObserver
+import androidx.compose.ui.gesture.dragGestureFilter
+import androidx.compose.ui.gesture.longPressDragGestureFilter
+import androidx.compose.ui.gesture.tapGestureFilter
+
+// Touch selection
+internal fun Modifier.longPressDragGestureFilter(
+ observer: LongPressDragObserver,
+ enabled: Boolean
+) = if (enabled) this.then(longPressDragGestureFilter(observer)) else this
+
+internal fun Modifier.focusRequestTapModifier(onTap: (Offset) -> Unit, enabled: Boolean) =
+ if (enabled) this.tapGestureFilter(onTap) else this
+
+// Focus modifiers
+internal fun Modifier.textFieldFocusModifier(
+ enabled: Boolean,
+ focusRequester: FocusRequester,
+ interactionState: InteractionState?,
+ onFocusChanged: (FocusState) -> Unit
+): Modifier {
+ if (!enabled) return this
+
+ return this
+ .focusRequester(focusRequester)
+ .onFocusChanged(onFocusChanged)
+ .focusable(interactionState = interactionState)
+}
+
+// Mouse
+internal fun Modifier.mouseDragGestureFilter(
+ dragObserver: DragObserver,
+ enabled: Boolean
+) = if (enabled) this.dragGestureFilter(dragObserver, startDragImmediately = true) else this
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
index 10e7ee7..253c5f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
@@ -16,14 +16,18 @@
package androidx.compose.foundation.text
+import androidx.compose.animation.asDisposableClock
import androidx.compose.foundation.InteractionState
-import androidx.compose.foundation.gestures.rememberScrollableController
+import androidx.compose.foundation.animation.defaultFlingConfig
+import androidx.compose.foundation.gestures.ScrollableController
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.savedinstancestate.Saver
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.savedinstancestate.listSaver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
@@ -35,7 +39,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.Ref
+import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.text.TextLayoutResult
@@ -49,79 +53,96 @@
import kotlin.math.min
import kotlin.math.roundToInt
+// Scrollable
+internal fun Modifier.textFieldScrollable(
+ scrollerPosition: TextFieldScrollerPosition,
+ interactionState: InteractionState? = null,
+ enabled: Boolean = true
+) = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "textFieldScrollable"
+ properties["scrollerPosition"] = scrollerPosition
+ properties["interactionState"] = interactionState
+ properties["enabled"] = enabled
+ }
+) {
+ // do not reverse direction only in case of RTL in horizontal orientation
+ val rtl = AmbientLayoutDirection.current == LayoutDirection.Rtl
+ val reverseDirection = scrollerPosition.orientation == Orientation.Vertical || !rtl
+ val controller = rememberScrollableController(
+ scrollerPosition,
+ interactionState = interactionState
+ ) { delta ->
+ val newOffset = scrollerPosition.offset + delta
+ val consumedDelta = when {
+ newOffset > scrollerPosition.maximum ->
+ scrollerPosition.maximum - scrollerPosition.offset
+ newOffset < 0f -> -scrollerPosition.offset
+ else -> delta
+ }
+ scrollerPosition.offset += consumedDelta
+ consumedDelta
+ }
+ val scroll = Modifier.scrollable(
+ orientation = scrollerPosition.orientation,
+ canScroll = { scrollerPosition.maximum != 0f },
+ reverseDirection = reverseDirection,
+ controller = controller,
+ enabled = enabled
+ )
+ scroll
+}
+
+// Layout
internal fun Modifier.textFieldScroll(
- orientation: Orientation,
scrollerPosition: TextFieldScrollerPosition,
textFieldValue: TextFieldValue,
visualTransformation: VisualTransformation,
- interactionState: InteractionState,
- textLayoutResult: Ref<TextLayoutResult?>,
- enabled: Boolean = true
-) = composed(
- factory = {
- // do not reverse direction only in case of RTL in horizontal orientation
- val rtl = AmbientLayoutDirection.current == LayoutDirection.Rtl
- val reverseDirection = orientation == Orientation.Vertical || !rtl
- val controller =
- rememberScrollableController(interactionState = interactionState) { delta ->
- val newOffset = scrollerPosition.offset + delta
- val consumedDelta = when {
- newOffset > scrollerPosition.maximum ->
- scrollerPosition.maximum - scrollerPosition.offset
- newOffset < 0f -> -scrollerPosition.offset
- else -> delta
- }
- scrollerPosition.offset += consumedDelta
- consumedDelta
- }
- val scroll = Modifier.scrollable(
- orientation = orientation,
- canScroll = { scrollerPosition.maximum != 0f },
- reverseDirection = reverseDirection,
- controller = controller,
- enabled = enabled
- )
+ textLayoutResultProvider: () -> TextLayoutResultProxy?
+): Modifier {
+ val orientation = scrollerPosition.orientation
+ val cursorOffset = scrollerPosition.getOffsetToFollow(textFieldValue.selection)
+ scrollerPosition.previousSelection = textFieldValue.selection
- val cursorOffset = scrollerPosition.getOffsetToFollow(textFieldValue.selection)
- scrollerPosition.previousSelection = textFieldValue.selection
+ val transformedText = visualTransformation.filter(textFieldValue.annotatedString)
- val transformedText = visualTransformation.filter(textFieldValue.annotatedString)
-
- val layout = when (orientation) {
- Orientation.Vertical ->
- VerticalScrollLayoutModifier(
- scrollerPosition,
- cursorOffset,
- transformedText,
- textLayoutResult
- )
- Orientation.Horizontal ->
- HorizontalScrollLayoutModifier(
- scrollerPosition,
- cursorOffset,
- transformedText,
- textLayoutResult
- )
- }
- this.then(scroll).clipToBounds().then(layout)
- },
- inspectorInfo = debugInspectorInfo {
- name = "textFieldScroll"
- properties["orientation"] = orientation
- properties["scrollerPosition"] = scrollerPosition
- properties["enabled"] = enabled
- properties["textFieldValue"] = textFieldValue
- properties["visualTransformation"] = visualTransformation
- properties["interactionState"] = interactionState
- properties["textLayoutResult"] = textLayoutResult
+ val layout = when (orientation) {
+ Orientation.Vertical ->
+ VerticalScrollLayoutModifier(
+ scrollerPosition,
+ cursorOffset,
+ transformedText,
+ textLayoutResultProvider
+ )
+ Orientation.Horizontal ->
+ HorizontalScrollLayoutModifier(
+ scrollerPosition,
+ cursorOffset,
+ transformedText,
+ textLayoutResultProvider
+ )
}
-)
+ return this.clipToBounds().then(layout)
+}
+
+@Composable
+private fun rememberScrollableController(
+ vararg inputs: Any?,
+ interactionState: InteractionState? = null,
+ consumeScrollDelta: (Float) -> Float
+): ScrollableController {
+ val clocks = AmbientAnimationClock.current.asDisposableClock()
+ val flingConfig = defaultFlingConfig()
+ return remember(inputs, clocks, flingConfig, interactionState) {
+ ScrollableController(consumeScrollDelta, flingConfig, clocks, interactionState)
+ }
+}
private data class VerticalScrollLayoutModifier(
val scrollerPosition: TextFieldScrollerPosition,
val cursorOffset: Int,
val transformedText: TransformedText,
- val textLayoutResult: Ref<TextLayoutResult?>
+ val textLayoutResultProvider: () -> TextLayoutResultProxy?
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
@@ -135,7 +156,7 @@
val cursorRect = getCursorRectInScroller(
cursorOffset = cursorOffset,
transformedText = transformedText,
- textLayoutResult = textLayoutResult.value,
+ textLayoutResult = textLayoutResultProvider()?.value,
rtl = false,
textFieldWidth = placeable.width
)
@@ -157,7 +178,7 @@
val scrollerPosition: TextFieldScrollerPosition,
val cursorOffset: Int,
val transformedText: TransformedText,
- val textLayoutResult: Ref<TextLayoutResult?>
+ val textLayoutResultProvider: () -> TextLayoutResultProxy?
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
@@ -171,7 +192,7 @@
val cursorRect = getCursorRectInScroller(
cursorOffset = cursorOffset,
transformedText = transformedText,
- textLayoutResult = textLayoutResult.value,
+ textLayoutResult = textLayoutResultProvider()?.value,
rtl = layoutDirection == LayoutDirection.Rtl,
textFieldWidth = placeable.width
)
@@ -216,7 +237,14 @@
}
@Stable
-internal class TextFieldScrollerPosition(initial: Float = 0f) {
+internal class TextFieldScrollerPosition(
+ initialOrientation: Orientation,
+ initial: Float = 0f,
+) {
+
+ /*@VisibleForTesting*/
+ constructor() : this(Orientation.Vertical)
+
/**
* Left or top offset. Takes values from 0 to [maximum].
* Taken with the opposite sign defines the x or y position of the text field in the
@@ -243,6 +271,8 @@
*/
var previousSelection: TextRange = TextRange.Zero
+ var orientation by mutableStateOf(initialOrientation, structuralEqualityPolicy())
+
fun update(
orientation: Orientation,
cursorRect: Rect,
@@ -283,11 +313,14 @@
}
companion object {
- val Saver = Saver<TextFieldScrollerPosition, Float>(
- save = { it.offset },
+ val Saver = listSaver<TextFieldScrollerPosition, Any>(
+ save = {
+ listOf(it.offset, it.orientation == Orientation.Vertical)
+ },
restore = { restored ->
TextFieldScrollerPosition(
- initial = restored
+ if (restored[1] as Boolean) Orientation.Vertical else Orientation.Horizontal,
+ restored[0] as Float
)
}
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt
new file mode 100644
index 0000000..1778378
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLayoutResultProxy.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextLayoutResult
+
+internal class TextLayoutResultProxy(val value: TextLayoutResult) {
+ // TextLayoutResult methods
+ fun getOffsetForPosition(position: Offset): Int {
+ val shiftedOffset = shiftedOffset(position)
+ return value.getOffsetForPosition(shiftedOffset)
+ }
+
+ fun getLineForVerticalPosition(vertical: Float): Int {
+ val shiftedVertical = shiftedOffset(Offset(0f, vertical)).y
+ return value.getLineForVerticalPosition(shiftedVertical)
+ }
+
+ fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
+ value.getLineEnd(lineIndex, visibleEnd)
+
+ /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+ * in the view. Returns false when the position is in the empty space of left/right of text.
+ */
+ fun isPositionOnText(offset: Offset): Boolean {
+ val shiftedOffset = shiftedOffset(offset)
+ val line = value.getLineForVerticalPosition(shiftedOffset.y)
+ return shiftedOffset.x >= value.getLineLeft(line) &&
+ shiftedOffset.x <= value.getLineRight(line)
+ }
+
+ // Shift offset
+ /** Measured bounds of the decoration box and inner text field. Together used to
+ * calculate the relative touch offset. Because touches are applied on the decoration box, we
+ * need to translate it to the inner text field coordinates.
+ */
+ var innerTextFieldCoordinates: LayoutCoordinates? = null
+ var decorationBoxCoordinates: LayoutCoordinates? = null
+
+ private fun shiftedOffset(offset: Offset): Offset {
+ // If offset is outside visible bounds of the inner text field, use visible bounds edges
+ val visibleInnerTextFieldRect = innerTextFieldCoordinates?.let { inner ->
+ decorationBoxCoordinates?.localBoundingBoxOf(inner)
+ } ?: Rect.Zero
+ val coercedOffset = offset.coerceIn(visibleInnerTextFieldRect)
+
+ // Translates touch to the inner text field coordinates
+ return innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
+ decorationBoxCoordinates?.let { decorationBoxCoordinates ->
+ innerTextFieldCoordinates.localPositionOf(decorationBoxCoordinates, coercedOffset)
+ }
+ } ?: coercedOffset
+ }
+}
+
+private fun Offset.coerceIn(rect: Rect): Offset {
+ val xOffset = when {
+ x < rect.left -> rect.left
+ x > rect.right -> rect.right
+ else -> x
+ }
+ val yOffset = when {
+ y < rect.top -> rect.top
+ y > rect.bottom -> rect.bottom
+ else -> y
+ }
+ return Offset(xOffset, yOffset)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 2bf656b..51ae7af 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -19,6 +19,9 @@
import androidx.compose.foundation.text.TextFieldDelegate
import androidx.compose.foundation.text.TextFieldState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
@@ -96,6 +99,11 @@
var focusRequester: FocusRequester? = null
/**
+ * Defines if paste and cut toolbar menu actions should be shown
+ */
+ var editable by mutableStateOf(true)
+
+ /**
* The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
* recalculated.
*/
@@ -123,7 +131,7 @@
}
// Long Press at the blank area, the cursor should show up at the end of the line.
- if (!isPositionOnText(pxPosition)) {
+ if (state?.layoutResult?.isPositionOnText(pxPosition) != true) {
state?.layoutResult?.let { layoutResult ->
val offset = offsetMapping.transformedToOriginal(
layoutResult.getLineEnd(
@@ -245,7 +253,7 @@
override fun onDrag(dragDistance: Offset): Offset {
dragTotalDistance += dragDistance
- state?.layoutResult?.let { layoutResult ->
+ state?.layoutResult?.value?.let { layoutResult ->
val startOffset = if (isStartHandle)
layoutResult.getOffsetForPosition(dragBeginPosition + dragTotalDistance)
else
@@ -305,7 +313,8 @@
if (!value.selection.collapsed) {
// if selection was not collapsed, set a default cursor location, otherwise
// don't change the location of the cursor.
- val newValue = value.copy(selection = TextRange.Zero)
+ val newCursorOffset = value.selection.max
+ val newValue = value.copy(selection = TextRange(newCursorOffset))
onValueChange(newValue)
}
setSelectionStatus(false)
@@ -405,7 +414,7 @@
internal fun getHandlePosition(isStartHandle: Boolean): Offset {
val offset = if (isStartHandle) value.selection.start else value.selection.end
return getSelectionHandleCoordinates(
- textLayoutResult = state?.layoutResult!!,
+ textLayoutResult = state?.layoutResult!!.value,
offset = offsetMapping.originalToTransformed(offset),
isStart = isStartHandle,
areHandlesCrossed = value.selection.reversed
@@ -448,8 +457,8 @@
textToolbar?.showMenu(
rect = getContentRect(),
>
- >
- >
+ (editable) paste else null,
+ (editable) cut else null,
>
)
}
@@ -483,7 +492,7 @@
state?.layoutCoordinates?.localToRoot(
Offset(
0f,
- it.layoutResult?.getCursorRect(
+ it.layoutResult?.value?.getCursorRect(
value.selection.start.coerceIn(
0,
max(0, value.text.length - 1)
@@ -495,7 +504,7 @@
state?.layoutCoordinates?.localToRoot(
Offset(
0f,
- it.layoutResult?.getCursorRect(
+ it.layoutResult?.value?.getCursorRect(
value.selection.end.coerceIn(
0,
max(0, value.text.length - 1)
@@ -529,7 +538,7 @@
)
val newTransformedSelection = getTextFieldSelection(
- textLayoutResult = state?.layoutResult,
+ textLayoutResult = state?.layoutResult?.value,
rawStartOffset = transformedStartOffset,
rawEndOffset = transformedEndOffset,
previousSelection = if (transformedSelection.collapsed) null else transformedSelection,
@@ -575,18 +584,6 @@
selection = selection.constrain(0, annotatedString.length)
)
}
-
- /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
- * in the view. Returns false when the position is in the empty space of left/right of text.
- */
- private fun isPositionOnText(offset: Offset): Boolean {
- state?.layoutResult?.let {
- val line = it.getLineForVerticalPosition(offset.y)
- if (offset.x < it.getLineLeft(line) || offset.x > it.getLineRight(line)) return false
- return true
- }
- return false
- }
}
@Composable
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
index 3857e63..a02b698 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
@@ -74,8 +74,9 @@
state.layoutResult = mock()
state.textDelegate = mock()
+ whenever(state.layoutResult!!.value).thenReturn(mock())
whenever(state.textDelegate.density).thenReturn(density)
- whenever(state.layoutResult!!.layoutInput).thenReturn(
+ whenever(state.layoutResult!!.value.layoutInput).thenReturn(
TextLayoutInput(
text = AnnotatedString(text),
style = TextStyle.Default,
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
index a93f802e..483dad7 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
@@ -72,6 +72,7 @@
private lateinit var textInputService: TextInputService
private lateinit var layoutCoordinates: LayoutCoordinates
private lateinit var multiParagraphIntrinsics: MultiParagraphIntrinsics
+ private lateinit var textLayoutResultProxy: TextLayoutResultProxy
private lateinit var textLayoutResult: TextLayoutResult
/**
@@ -93,6 +94,8 @@
layoutCoordinates = mock()
multiParagraphIntrinsics = mock()
textLayoutResult = mock()
+ textLayoutResultProxy = mock()
+ whenever(textLayoutResultProxy.value).thenReturn(textLayoutResult)
}
@Test
@@ -101,11 +104,11 @@
val offset = 10
val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
whenever(processor.mBufferState).thenReturn(editorState)
- whenever(textLayoutResult.getOffsetForPosition(position)).thenReturn(offset)
+ whenever(textLayoutResultProxy.getOffsetForPosition(position)).thenReturn(offset)
TextFieldDelegate.setCursorOffset(
position,
- textLayoutResult,
+ textLayoutResultProxy,
processor,
OffsetMapping.Identity,
onValueChange
@@ -295,11 +298,11 @@
val offset = 10
val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
whenever(processor.mBufferState).thenReturn(editorState)
- whenever(textLayoutResult.getOffsetForPosition(position)).thenReturn(offset)
+ whenever(textLayoutResultProxy.getOffsetForPosition(position)).thenReturn(offset)
TextFieldDelegate.setCursorOffset(
position,
- textLayoutResult,
+ textLayoutResultProxy,
processor,
skippingOffsetMap,
onValueChange
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index d490f58..c42210e9 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.TextFieldState
+import androidx.compose.foundation.text.TextLayoutResultProxy
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -28,6 +29,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.OffsetMapping
@@ -49,7 +51,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
@@ -62,7 +63,7 @@
private var value = TextFieldValue(text)
private val lambda: (TextFieldValue) -> Unit = { value = it }
private val spyLambda = spy(lambda)
- private val state = TextFieldState(mock())
+ private lateinit var state: TextFieldState
private val dragBeginPosition = Offset.Zero
private val dragDistance = Offset(300f, 15f)
@@ -70,7 +71,8 @@
private val dragOffset = text.indexOf('r')
private val fakeTextRange = TextRange(0, "Hello".length)
private val dragTextRange = TextRange("Hello".length + 1, text.length)
-
+ private val layoutResult: TextLayoutResult = mock()
+ private val layoutResultProxy: TextLayoutResultProxy = mock()
private val manager = TextFieldSelectionManager()
private val clipboardManager = mock<ClipboardManager>()
@@ -82,17 +84,13 @@
fun setup() {
manager.offsetMapping = offsetMapping
manager.>
- manager.state = state
manager.value = value
manager.clipboardManager = clipboardManager
manager.textToolbar = textToolbar
manager.hapticFeedBack = hapticFeedback
manager.focusRequester = focusRequester
- state.layoutResult = mock()
- state.textDelegate = mock()
- whenever(state.textDelegate.density).thenReturn(density)
- whenever(state.layoutResult!!.layoutInput).thenReturn(
+ whenever(layoutResult.layoutInput).thenReturn(
TextLayoutInput(
text = AnnotatedString(text),
style = TextStyle.Default,
@@ -106,21 +104,27 @@
constraints = Constraints()
)
)
- whenever(state.layoutResult!!.getOffsetForPosition(dragBeginPosition)).thenReturn(
- beginOffset
- )
- whenever(state.layoutResult!!.getOffsetForPosition(dragDistance)).thenReturn(dragOffset)
- whenever(
- state.layoutResult!!.getWordBoundary(ArgumentMatchers.intThat { it in 0 until 5 })
- ).thenAnswer(TextRangeAnswer(fakeTextRange))
- whenever(
- state.layoutResult!!.getWordBoundary(ArgumentMatchers.intThat { it in 6 until 11 })
- ).thenAnswer(TextRangeAnswer(dragTextRange))
-
- whenever(state.layoutResult!!.getBidiRunDirection(any()))
+ whenever(layoutResult.getWordBoundary(beginOffset))
+ .thenAnswer(TextRangeAnswer(fakeTextRange))
+ whenever(layoutResult.getWordBoundary(dragOffset))
+ .thenAnswer(TextRangeAnswer(dragTextRange))
+ whenever(layoutResult.getBidiRunDirection(any()))
.thenReturn(ResolvedTextDirection.Ltr)
- whenever(state.layoutResult!!.getBoundingBox(any())).thenReturn(Rect.Zero)
+ whenever(layoutResult.getBoundingBox(any())).thenReturn(Rect.Zero)
+ // left or right handle drag
+ whenever(layoutResult.getOffsetForPosition(dragBeginPosition)).thenReturn(beginOffset)
+ whenever(layoutResult.getOffsetForPosition(dragDistance)).thenReturn(dragOffset)
+ // touch drag
+ whenever(layoutResultProxy.getOffsetForPosition(dragBeginPosition)).thenReturn(beginOffset)
+ whenever(layoutResultProxy.getOffsetForPosition(dragDistance)).thenReturn(dragOffset)
+
+ whenever(layoutResultProxy.value).thenReturn(layoutResult)
+
+ state = TextFieldState(mock())
+ state.layoutResult = layoutResultProxy
+ manager.state = state
+ whenever(state.textDelegate.density).thenReturn(density)
}
@Test
@@ -133,6 +137,7 @@
@Test
fun TextFieldSelectionManager_touchSelectionObserver_onLongPress() {
+ whenever(layoutResultProxy.isPositionOnText(dragBeginPosition)).thenReturn(true)
manager.touchSelectionObserver.onLongPress(dragBeginPosition)
@@ -155,11 +160,12 @@
// Setup
val fakeLineNumber = 0
val fakeLineEnd = text.length
- whenever(state.layoutResult!!.getLineForVerticalPosition(dragBeginPosition.y))
+ whenever(layoutResultProxy.isPositionOnText(dragBeginPosition)).thenReturn(false)
+ whenever(layoutResultProxy.getLineForVerticalPosition(dragBeginPosition.y))
.thenReturn(fakeLineNumber)
- whenever(state.layoutResult!!.getLineLeft(fakeLineNumber))
+ whenever(layoutResult.getLineLeft(fakeLineNumber))
.thenReturn(dragBeginPosition.x + 1.0f)
- whenever(state.layoutResult!!.getLineEnd(fakeLineNumber)).thenReturn(fakeLineEnd)
+ whenever(layoutResultProxy.getLineEnd(fakeLineNumber)).thenReturn(fakeLineEnd)
// Act
manager.touchSelectionObserver.onLongPress(dragBeginPosition)
@@ -281,7 +287,7 @@
manager.deselect()
verify(textToolbar, times(1)).hide()
- assertThat(value.selection).isEqualTo(TextRange.Zero)
+ assertThat(value.selection).isEqualTo(TextRange("Hello".length))
assertThat(state.selectionIsOn).isFalse()
}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt
index 7328504..981b708 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt
@@ -52,17 +52,17 @@
@Test
fun no_withconstraints_inner_recompose() {
- benchmarkRule.toggleStateBenchmarkComposeMeasureLayout { NoWithConstraintsTestCase() }
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout({ NoWithConstraintsTestCase() })
}
@Test
fun withconstraints_inner_recompose() {
- benchmarkRule.toggleStateBenchmarkComposeMeasureLayout { WithConstraintsTestCase() }
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout({ WithConstraintsTestCase() })
}
@Test
fun withconstraints_changing_constraints() {
- benchmarkRule.toggleStateBenchmarkMeasureLayout { ChangingConstraintsTestCase() }
+ benchmarkRule.toggleStateBenchmarkMeasureLayout({ ChangingConstraintsTestCase() })
}
}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/CheckboxesInRowsBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/CheckboxesInRowsBenchmark.kt
index 379fa20..2d90b70 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/CheckboxesInRowsBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/CheckboxesInRowsBenchmark.kt
@@ -54,7 +54,10 @@
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(checkboxCaseFactory)
+ benchmarkRule.benchmarkFirstCompose(
+ checkboxCaseFactory,
+ assertNoPendingRecompositions = false
+ )
}
@Test
@@ -74,22 +77,31 @@
@Test
fun toggleCheckbox_recompose() {
- benchmarkRule.toggleStateBenchmarkRecompose(checkboxCaseFactory)
+ benchmarkRule.toggleStateBenchmarkRecompose(
+ checkboxCaseFactory,
+ assertOneRecomposition = false
+ )
}
@Test
fun toggleCheckbox_measure() {
- benchmarkRule.toggleStateBenchmarkMeasure(checkboxCaseFactory)
+ benchmarkRule.toggleStateBenchmarkMeasure(
+ checkboxCaseFactory,
+ assertOneRecomposition = false
+ )
}
@Test
fun toggleCheckbox_layout() {
- benchmarkRule.toggleStateBenchmarkLayout(checkboxCaseFactory)
+ benchmarkRule.toggleStateBenchmarkLayout(
+ checkboxCaseFactory,
+ assertOneRecomposition = false
+ )
}
@Test
fun toggleCheckbox_draw() {
- benchmarkRule.toggleStateBenchmarkDraw(checkboxCaseFactory)
+ benchmarkRule.toggleStateBenchmarkDraw(checkboxCaseFactory, assertOneRecomposition = false)
}
@Test
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/RadioGroupBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/RadioGroupBenchmark.kt
index d704a6f..c62622f 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/RadioGroupBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/RadioGroupBenchmark.kt
@@ -57,7 +57,7 @@
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(radioCaseFactory)
+ benchmarkRule.benchmarkFirstCompose(radioCaseFactory, assertNoPendingRecompositions = false)
}
@Test
@@ -77,22 +77,25 @@
@Test
fun toggleRadio_recompose() {
- benchmarkRule.toggleStateBenchmarkRecompose(radioCaseFactory)
+ benchmarkRule.toggleStateBenchmarkRecompose(
+ radioCaseFactory,
+ assertOneRecomposition = false
+ )
}
@Test
fun toggleRadio_measure() {
- benchmarkRule.toggleStateBenchmarkMeasure(radioCaseFactory)
+ benchmarkRule.toggleStateBenchmarkMeasure(radioCaseFactory, assertOneRecomposition = false)
}
@Test
fun toggleRadio_layout() {
- benchmarkRule.toggleStateBenchmarkLayout(radioCaseFactory)
+ benchmarkRule.toggleStateBenchmarkLayout(radioCaseFactory, assertOneRecomposition = false)
}
@Test
fun toggleRadio_draw() {
- benchmarkRule.toggleStateBenchmarkDraw(radioCaseFactory)
+ benchmarkRule.toggleStateBenchmarkDraw(radioCaseFactory, assertOneRecomposition = false)
}
}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt
index aea9e5a..0c5451b 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt
@@ -44,22 +44,22 @@
@Test
fun withTrailingLambdas_compose() {
- benchmarkRule.benchmarkFirstCompose { WithTrailingLambdas() }
+ benchmarkRule.benchmarkFirstCompose({ WithTrailingLambdas() })
}
@Test
fun withTrailingLambdas_recompose() {
- benchmarkRule.toggleStateBenchmarkRecompose { WithTrailingLambdas() }
+ benchmarkRule.toggleStateBenchmarkRecompose({ WithTrailingLambdas() })
}
@Test
fun withoutTrailingLambdas_compose() {
- benchmarkRule.benchmarkFirstCompose { WithoutTrailingLambdas() }
+ benchmarkRule.benchmarkFirstCompose({ WithoutTrailingLambdas() })
}
@Test
fun withoutTrailingLambdas_recompose() {
- benchmarkRule.toggleStateBenchmarkRecompose { WithoutTrailingLambdas() }
+ benchmarkRule.toggleStateBenchmarkRecompose({ WithoutTrailingLambdas() })
}
}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
index bfe8f59..fb531d0 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
@@ -41,7 +41,7 @@
@Test
fun xml_compose() {
- benchmarkRule.benchmarkFirstCompose { XmlVectorTestCase() }
+ benchmarkRule.benchmarkFirstCompose({ XmlVectorTestCase() })
}
@Test
@@ -61,7 +61,7 @@
@Test
fun programmatic_compose() {
- benchmarkRule.benchmarkFirstCompose { ProgrammaticVectorTestCase() }
+ benchmarkRule.benchmarkFirstCompose({ ProgrammaticVectorTestCase() })
}
@Test
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
index 3c14b88..f84c765 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
@@ -182,21 +182,21 @@
LayoutDirection.Rtl -> Icons.Filled.ArrowForward
}
IconButton( {
- Icon(icon)
+ Icon(icon, null)
}
}
@Composable
fun Filter(onClick: () -> Unit) {
IconButton(modifier = Modifier.testTag(Tags.FilterButton), {
- Icon(Icons.Filled.Search)
+ Icon(Icons.Filled.Search, null)
}
}
@Composable
fun Settings(onClick: () -> Unit) {
IconButton( {
- Icon(Icons.Filled.Settings)
+ Icon(Icons.Filled.Settings, null)
}
}
}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index 47e8e9c..b01334f 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -88,7 +88,7 @@
}
TopAppBar(backgroundColor = appBarColor, contentColor = onSurface) {
IconButton(modifier = Modifier.align(Alignment.CenterVertically), {
- Icon(Icons.Filled.Close)
+ Icon(Icons.Filled.Close, null)
}
FilterField(
filterText,
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt
index 6e2bb73..656fb76 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/Interoperability.kt
@@ -210,6 +210,7 @@
Icon(
imageVector = vectorResource(R.drawable.ic_plane),
+ contentDescription = stringResource(R.string.plane_description),
tint = colorResource(R.color.Blue700)
)
}
@@ -421,15 +422,16 @@
}
object string {
const val ok = 4
+ const val plane_description = 5
}
object dimen {
- const val padding_small = 5
+ const val padding_small = 6
}
object drawable {
- const val ic_plane = 6
+ const val ic_plane = 7
}
object color {
- const val Blue700 = 7
+ const val Blue700 = 8
}
}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/navigation/Navigation.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/navigation/Navigation.kt
index f11321f..18ac649 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/navigation/Navigation.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/navigation/Navigation.kt
@@ -56,11 +56,13 @@
* No action required if it's modified.
*/
-@Composable private fun NavigationSnippet1() {
+@Composable
+private fun NavigationSnippet1() {
val navController = rememberNavController()
}
-@Composable private fun NavigationSnippet2(navController: NavHostController) {
+@Composable
+private fun NavigationSnippet2(navController: NavHostController) {
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
@@ -69,7 +71,8 @@
}
private object NavigationSnippet3 {
- @Composable fun Profile(navController: NavController) {
+ @Composable
+ fun Profile(navController: NavController) {
/*...*/
Button( navController.navigate("friends") }) {
Text(text = "Navigate next")
@@ -78,14 +81,16 @@
}
}
-@Composable private fun NavigationSnippet4(navController: NavHostController) {
+@Composable
+private fun NavigationSnippet4(navController: NavHostController) {
NavHost(navController = navController, startDestination = "profile/{userId}") {
/*...*/
composable("profile/{userId}") { /*...*/ }
}
}
-@Composable private fun NavigationSnippet5(navController: NavHostController) {
+@Composable
+private fun NavigationSnippet5(navController: NavHostController) {
NavHost(navController = navController, startDestination = "profile/{userId}") {
/*...*/
composable(
@@ -95,17 +100,20 @@
}
}
-@Composable private fun NavGraphBuilder.NavigationSnippet6(navController: NavHostController) {
+@Composable
+private fun NavGraphBuilder.NavigationSnippet6(navController: NavHostController) {
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
}
-@Composable private fun NavigationSnippet7(navController: NavHostController) {
+@Composable
+private fun NavigationSnippet7(navController: NavHostController) {
navController.navigate("profile/user1234")
}
-@Composable private fun NavGraphBuilder.NavigationSnippet8(navController: NavHostController) {
+@Composable
+private fun NavGraphBuilder.NavigationSnippet8(navController: NavHostController) {
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "me" })
@@ -116,7 +124,8 @@
/* Deep links */
-@Composable private fun NavGraphBuilder.NavigationSnippet9(navController: NavHostController) {
+@Composable
+private fun NavGraphBuilder.NavigationSnippet9(navController: NavHostController) {
val uri = "https://example.com"
composable(
@@ -127,7 +136,8 @@
}
}
-@Composable private fun NavigationSnippet10() {
+@Composable
+private fun NavigationSnippet10() {
val id = "exampleId"
val context = AmbientContext.current
val deepLinkIntent = Intent(
@@ -143,7 +153,8 @@
}
}
-@Composable private fun NavigationSnippet11(items: List<Screen>) {
+@Composable
+private fun NavigationSnippet11(items: List<Screen>) {
val navController = rememberNavController()
Scaffold(
bottomBar = {
@@ -152,7 +163,7 @@
val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
items.forEach { screen ->
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
>
@@ -178,7 +189,8 @@
}
}
-@Composable private fun NavGraphBuilder.NavigationSnippet12(navController: NavHostController) {
+@Composable
+private fun NavGraphBuilder.NavigationSnippet12(navController: NavHostController) {
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "me" })
@@ -200,12 +212,31 @@
}
}
-@Composable private fun Profile() { }
-@Composable private fun Profile(userId: String?, content: @Composable (String) -> Unit) { }
-@Composable private fun Profile(navController: NavHostController) { }
-@Composable private fun FriendsList() { }
-@Composable private fun FriendsList(navController: NavHostController) { }
-@Composable private fun Profile(navController: NavHostController, arg: String?) { TODO() }
+@Composable
+private fun Profile() {
+}
+
+@Composable
+private fun Profile(userId: String?, content: @Composable (String) -> Unit) {
+}
+
+@Composable
+private fun Profile(navController: NavHostController) {
+}
+
+@Composable
+private fun FriendsList() {
+}
+
+@Composable
+private fun FriendsList(navController: NavHostController) {
+}
+
+@Composable
+private fun Profile(navController: NavHostController, arg: String?) {
+ TODO()
+}
+
private class MyActivity
private sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
index dd6f636..3d70e48 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
@@ -47,6 +47,7 @@
import androidx.compose.runtime.savedinstancestate.savedInstanceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.viewModel
import androidx.lifecycle.LiveData
@@ -235,12 +236,18 @@
Text(text = body, Modifier.padding(top = 8.dp))
// change expanded in response to click events
IconButton( expanded = false }, modifier = Modifier.fillMaxWidth()) {
- Icon(Icons.Default.ExpandLess)
+ Icon(
+ Icons.Default.ExpandLess,
+ contentDescription = stringResource(R.string.expand_less)
+ )
}
} else {
// change expanded in response to click events
IconButton( expanded = true }, modifier = Modifier.fillMaxWidth()) {
- Icon(Icons.Default.ExpandMore)
+ Icon(
+ Icons.Default.ExpandMore,
+ contentDescription = stringResource(R.string.expand_more)
+ )
}
}
}
@@ -284,12 +291,18 @@
if (expanded) {
Spacer(Modifier.height(8.dp))
Text(body)
- IconButton( Modifier.fillMaxWidth()) {
- Icon(Icons.Default.ExpandLess)
+ IconButton( modifier = Modifier.fillMaxWidth()) {
+ Icon(
+ Icons.Default.ExpandLess,
+ contentDescription = stringResource(R.string.expand_less)
+ )
}
} else {
- IconButton( Modifier.fillMaxWidth()) {
- Icon(Icons.Default.ExpandMore)
+ IconButton( modifier = Modifier.fillMaxWidth()) {
+ Icon(
+ Icons.Default.ExpandMore,
+ contentDescription = stringResource(R.string.expand_more)
+ )
}
}
}
@@ -325,6 +338,13 @@
}
}
+private object R {
+ object string {
+ const val expand_less = 0
+ const val expand_more = 1
+ }
+}
+
private const val it = 1
private lateinit var helloViewModel: StateSnippet2.HelloViewModel
private fun computeTextFormatting(st: String): String = ""
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt
index 18a6b75..8cf64b3 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/tutorial/Tutorial.kt
@@ -159,7 +159,7 @@
Column(
modifier = Modifier.padding(16.dp)
) {
- Image(image)
+ Image(image, contentDescription = null)
Text("A day in Shark Fin Cove")
Text("Davenport, California")
@@ -179,9 +179,12 @@
.preferredHeight(180.dp)
.fillMaxWidth()
- Image(image,
+ Image(
+ image,
+ contentDescription = null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Text("A day in Shark Fin Cove")
Text("Davenport, California")
@@ -205,9 +208,12 @@
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp))
- Image(image,
+ Image(
+ image,
+ contentDescription = null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Spacer(Modifier.preferredHeight(16.dp))
Text("A day in Shark Fin Cove")
@@ -230,9 +236,12 @@
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp))
- Image(image,
+ Image(
+ image,
+ contentDescription = null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Spacer(Modifier.preferredHeight(16.dp))
Text("A day in Shark Fin Cove")
@@ -257,9 +266,12 @@
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp))
- Image(image,
+ Image(
+ image,
+ contentDescription = null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Spacer(Modifier.preferredHeight(16.dp))
Text("A day in Shark Fin Cove",
@@ -287,9 +299,12 @@
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp))
- Image(image,
+ Image(
+ image,
+ contentDescription = null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Spacer(Modifier.preferredHeight(16.dp))
Text(
@@ -321,9 +336,11 @@
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp))
- Image(image,
+ Image(
+ image, null,
modifier = imageModifier,
- contentScale = ContentScale.Crop)
+ contentScale = ContentScale.Crop
+ )
Spacer(Modifier.preferredHeight(16.dp))
Text(
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt
index 102d854..3e0c8b2 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt
@@ -96,7 +96,7 @@
}
val imageVector =
vectorResource(R.drawable.ic_pathfill_sample)
- Image(imageVector, modifier = Modifier.testTag(testTag))
+ Image(imageVector, null, modifier = Modifier.testTag(testTag))
}
rule.onNodeWithTag(testTag).captureToImage().asAndroidBitmap().apply {
diff --git a/compose/material/material-icons-core/samples/src/main/java/androidx/compose/material/icons/samples/IconSamples.kt b/compose/material/material-icons-core/samples/src/main/java/androidx/compose/material/icons/samples/IconSamples.kt
index 24bd565..36b37ac 100644
--- a/compose/material/material-icons-core/samples/src/main/java/androidx/compose/material/icons/samples/IconSamples.kt
+++ b/compose/material/material-icons-core/samples/src/main/java/androidx/compose/material/icons/samples/IconSamples.kt
@@ -38,7 +38,7 @@
@Sampled
@Composable
fun DrawIcon() {
- Icon(Icons.Rounded.Menu)
+ Icon(Icons.Rounded.Menu, contentDescription = "Localized description")
}
@Composable
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 190a613..68ce800 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -127,8 +127,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonColors {
- method public long backgroundColor-0d7_KjU(boolean enabled);
- method public long contentColor-0d7_KjU(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
}
public final class ButtonDefaults {
@@ -157,7 +157,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonElevation {
- method public float elevation-D9Ej5fM(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
}
public final class ButtonKt {
@@ -171,9 +171,9 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface CheckboxColors {
- method public long borderColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long boxColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> borderColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> boxColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> checkmarkColor(androidx.compose.ui.state.ToggleableState state);
}
public final class CheckboxDefaults {
@@ -313,7 +313,7 @@
}
public final class ElevationKt {
- method public static void animateElevation-K5VTZl0(androidx.compose.animation.core.AnimatedValue<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target);
+ method public static suspend Object? animateElevation-_EXKJ8o(androidx.compose.animation.core.Animatable<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Dp> getAmbientAbsoluteElevation();
}
@@ -344,7 +344,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
- method public float elevation-D9Ej5fM(androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(androidx.compose.foundation.InteractionState interactionState);
}
public final class FloatingActionButtonKt {
@@ -364,9 +364,9 @@
}
public final class IconKt {
- method @androidx.compose.runtime.Composable public static void Icon-1Wn-iBs(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-1eoVtfs(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-tu7MLKI(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-8NTYWNk(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-BG621w0(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-hGAziDE(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
public final class ListItemKt {
@@ -449,7 +449,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface RadioButtonColors {
- method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> radioColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
@@ -514,7 +514,7 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
+ public interface SnackbarData {
method public void dismiss();
method public String? getActionLabel();
method public androidx.compose.material.SnackbarDuration getDuration();
@@ -540,10 +540,10 @@
}
public final class SnackbarHostKt {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
}
- @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
ctor public SnackbarHostState();
method public androidx.compose.material.SnackbarData? getCurrentSnackbarData();
method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional androidx.compose.material.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material.SnackbarResult> p);
@@ -628,8 +628,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface SwitchColors {
- method public long thumbColor-0d7_KjU(boolean enabled, boolean checked);
- method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> thumbColor(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trackColor(boolean enabled, boolean checked);
}
public final class SwitchDefaults {
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 190a613..68ce800 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -127,8 +127,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonColors {
- method public long backgroundColor-0d7_KjU(boolean enabled);
- method public long contentColor-0d7_KjU(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
}
public final class ButtonDefaults {
@@ -157,7 +157,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonElevation {
- method public float elevation-D9Ej5fM(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
}
public final class ButtonKt {
@@ -171,9 +171,9 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface CheckboxColors {
- method public long borderColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long boxColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> borderColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> boxColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> checkmarkColor(androidx.compose.ui.state.ToggleableState state);
}
public final class CheckboxDefaults {
@@ -313,7 +313,7 @@
}
public final class ElevationKt {
- method public static void animateElevation-K5VTZl0(androidx.compose.animation.core.AnimatedValue<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target);
+ method public static suspend Object? animateElevation-_EXKJ8o(androidx.compose.animation.core.Animatable<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Dp> getAmbientAbsoluteElevation();
}
@@ -344,7 +344,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
- method public float elevation-D9Ej5fM(androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(androidx.compose.foundation.InteractionState interactionState);
}
public final class FloatingActionButtonKt {
@@ -364,9 +364,9 @@
}
public final class IconKt {
- method @androidx.compose.runtime.Composable public static void Icon-1Wn-iBs(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-1eoVtfs(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-tu7MLKI(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-8NTYWNk(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-BG621w0(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-hGAziDE(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
public final class ListItemKt {
@@ -449,7 +449,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface RadioButtonColors {
- method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> radioColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
@@ -514,7 +514,7 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
+ public interface SnackbarData {
method public void dismiss();
method public String? getActionLabel();
method public androidx.compose.material.SnackbarDuration getDuration();
@@ -540,10 +540,10 @@
}
public final class SnackbarHostKt {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
}
- @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
ctor public SnackbarHostState();
method public androidx.compose.material.SnackbarData? getCurrentSnackbarData();
method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional androidx.compose.material.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material.SnackbarResult> p);
@@ -628,8 +628,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface SwitchColors {
- method public long thumbColor-0d7_KjU(boolean enabled, boolean checked);
- method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> thumbColor(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trackColor(boolean enabled, boolean checked);
}
public final class SwitchDefaults {
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 190a613..68ce800 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -127,8 +127,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonColors {
- method public long backgroundColor-0d7_KjU(boolean enabled);
- method public long contentColor-0d7_KjU(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
}
public final class ButtonDefaults {
@@ -157,7 +157,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ButtonElevation {
- method public float elevation-D9Ej5fM(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(boolean enabled, androidx.compose.foundation.InteractionState interactionState);
}
public final class ButtonKt {
@@ -171,9 +171,9 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface CheckboxColors {
- method public long borderColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long boxColor-0d7_KjU(boolean enabled, androidx.compose.ui.state.ToggleableState state);
- method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> borderColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> boxColor(boolean enabled, androidx.compose.ui.state.ToggleableState state);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> checkmarkColor(androidx.compose.ui.state.ToggleableState state);
}
public final class CheckboxDefaults {
@@ -313,7 +313,7 @@
}
public final class ElevationKt {
- method public static void animateElevation-K5VTZl0(androidx.compose.animation.core.AnimatedValue<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target);
+ method public static suspend Object? animateElevation-_EXKJ8o(androidx.compose.animation.core.Animatable<androidx.compose.ui.unit.Dp,?>, optional androidx.compose.foundation.Interaction? from, optional androidx.compose.foundation.Interaction? to, float target, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Dp> getAmbientAbsoluteElevation();
}
@@ -344,7 +344,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
- method public float elevation-D9Ej5fM(androidx.compose.foundation.InteractionState interactionState);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> elevation(androidx.compose.foundation.InteractionState interactionState);
}
public final class FloatingActionButtonKt {
@@ -364,9 +364,9 @@
}
public final class IconKt {
- method @androidx.compose.runtime.Composable public static void Icon-1Wn-iBs(androidx.compose.ui.graphics.ImageBitmap bitmap, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-1eoVtfs(androidx.compose.ui.graphics.painter.Painter painter, optional androidx.compose.ui.Modifier modifier, optional long tint);
- method @androidx.compose.runtime.Composable public static void Icon-tu7MLKI(androidx.compose.ui.graphics.vector.ImageVector imageVector, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-8NTYWNk(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-BG621w0(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable public static void Icon-hGAziDE(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
public final class ListItemKt {
@@ -449,7 +449,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface RadioButtonColors {
- method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> radioColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
@@ -514,7 +514,7 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
+ public interface SnackbarData {
method public void dismiss();
method public String? getActionLabel();
method public androidx.compose.material.SnackbarDuration getDuration();
@@ -540,10 +540,10 @@
}
public final class SnackbarHostKt {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarData,kotlin.Unit> snackbar);
}
- @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
ctor public SnackbarHostState();
method public androidx.compose.material.SnackbarData? getCurrentSnackbarData();
method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional androidx.compose.material.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material.SnackbarResult> p);
@@ -628,8 +628,8 @@
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface SwitchColors {
- method public long thumbColor-0d7_KjU(boolean enabled, boolean checked);
- method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> thumbColor(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trackColor(boolean enabled, boolean checked);
}
public final class SwitchDefaults {
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
index 170084f..388b6c2 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
@@ -171,7 +171,7 @@
IconToggleButton(checked = checked, enabled = false, checked = it }) {
val tint by animateAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
- Icon(Icons.Filled.Favorite, tint = tint)
+ Icon(Icons.Filled.Favorite, contentDescription = "Favorite", tint = tint)
}
}
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
index babd9ed..8c7a242 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
@@ -115,7 +115,7 @@
)
Box(Modifier.fillMaxSize()) {
- Image(modifier = inputModifier, bitmap = colorWheel.image)
+ Image(modifier = inputModifier, contentDescription = null, bitmap = colorWheel.image)
val color = colorWheel.colorForPosition(position)
if (color.isSpecified) {
Magnifier(visible = isDragging, position = position, color = color)
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialTextField.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialTextField.kt
index fd2842e..dc48864 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialTextField.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialTextField.kt
@@ -30,7 +30,6 @@
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
@@ -175,8 +174,12 @@
"Label" + if (selectedOption == Option.Error) "*" else ""
Text(text = label)
},
- leadingIcon = { if (leadingChecked) Icon(Icons.Filled.Favorite) },
- trailingIcon = { if (trailingChecked) Icon(Icons.Filled.Info) },
+ leadingIcon = {
+ if (leadingChecked) Icon(Icons.Filled.Favorite, "Favorite")
+ },
+ trailingIcon = {
+ if (trailingChecked) Icon(Icons.Filled.Info, "Info")
+ },
isErrorValue = selectedOption == Option.Error,
modifier = Modifier.width(300.dp)
)
@@ -192,10 +195,14 @@
"Label" + if (selectedOption == Option.Error) "*" else ""
Text(text = label)
},
- leadingIcon = { if (leadingChecked) Icon(Icons.Filled.Favorite) },
- trailingIcon = { if (trailingChecked) Icon(Icons.Filled.Info) },
+ leadingIcon = {
+ if (leadingChecked) Icon(Icons.Filled.Favorite, "Favorite")
+ },
+ trailingIcon = {
+ if (trailingChecked) Icon(Icons.Filled.Info, "Info")
+ },
isErrorValue = selectedOption == Option.Error,
- modifier = Modifier.widthIn(max = 300.dp)
+ modifier = Modifier.width(300.dp)
)
}
}
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
index 009a3b3e..eee99c4 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
@@ -78,7 +78,7 @@
val iconButton = @Composable {
IconButton( expanded = true }) {
- Icon(Icons.Default.MoreVert)
+ Icon(Icons.Default.MoreVert, null)
}
}
DropdownMenu(
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/CommonUi.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/CommonUi.kt
index a2ee1ad..423fcc4 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/CommonUi.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/CommonUi.kt
@@ -100,8 +100,9 @@
Spacer(Modifier.preferredWidth(16.dp))
Icon(
Icons.Filled.ArrowForwardIos,
- tint = Color.White.copy(alpha = 0.6f),
+ contentDescription = null,
modifier = Modifier.preferredSize(12.dp).align(Alignment.CenterVertically),
+ tint = Color.White.copy(alpha = 0.6f)
)
}
RallyDivider()
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/OverviewScreen.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/OverviewScreen.kt
index 9827abd..8773f93 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/OverviewScreen.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/OverviewScreen.kt
@@ -33,6 +33,7 @@
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
+import androidx.compose.material.studies.R
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -40,6 +41,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import java.util.Locale
@@ -126,7 +128,7 @@
>
modifier = Modifier.align(Alignment.Top)
) {
- Icon(Icons.Filled.Sort)
+ Icon(Icons.Filled.Sort, contentDescription = stringResource(R.string.sort))
}
}
}
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/RallyScreenState.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/RallyScreenState.kt
index 044c093..ae6e831 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/RallyScreenState.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/RallyScreenState.kt
@@ -17,14 +17,26 @@
package androidx.compose.material.studies.rally
import androidx.compose.material.icons.Icons
+import androidx.compose.material.studies.R
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
enum class RallyScreenState(
- val icon: ImageVector,
+ val icon: ScreenIcon,
val body: @Composable () -> Unit
) {
- Overview(Icons.Filled.PieChart, { OverviewBody() }),
- Accounts(Icons.Filled.AttachMoney, { AccountsBody(UserData.accounts) }),
- Bills(Icons.Filled.MoneyOff, { BillsBody(UserData.bills) })
-}
\ No newline at end of file
+ Overview(
+ ScreenIcon(Icons.Filled.PieChart, contentDescription = R.string.overview),
+ @Composable { OverviewBody() }
+ ),
+ Accounts(
+ ScreenIcon(Icons.Filled.AttachMoney, contentDescription = R.string.account),
+ @Composable { AccountsBody(UserData.accounts) }
+ ),
+ Bills(
+ ScreenIcon(Icons.Filled.MoneyOff, contentDescription = R.string.bills),
+ @Composable { BillsBody(UserData.bills) }
+ )
+}
+
+class ScreenIcon(val icon: ImageVector, val contentDescription: Int)
\ No newline at end of file
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
index 843d7d8..b49e23f 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
@@ -38,7 +38,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import java.util.Locale
@@ -65,7 +65,7 @@
@Composable
private fun RallyTab(
text: String,
- icon: ImageVector,
+ icon: ScreenIcon,
onSelected: () -> Unit,
selected: Boolean
) {
@@ -81,7 +81,11 @@
indication = rememberRipple(bounded = false)
)
) {
- Icon(icon, tint = tabTintColor)
+ Icon(
+ imageVector = icon.icon,
+ contentDescription = stringResource(icon.contentDescription),
+ tint = tabTintColor
+ )
if (selected) {
Spacer(Modifier.preferredWidth(12.dp))
Text(text, color = tabTintColor)
diff --git a/compose/material/material/integration-tests/material-studies/src/main/res/values/strings.xml b/compose/material/material/integration-tests/material-studies/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7df5dd7b
--- /dev/null
+++ b/compose/material/material/integration-tests/material-studies/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="sort">Sort</string>
+ <string name="overview">Account overview</string>
+ <string name="account">Accounts</string>
+ <string name="bills">Bills</string>
+</resources>
\ No newline at end of file
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/AppBarSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/AppBarSamples.kt
index 1019531..2d17900 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/AppBarSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/AppBarSamples.kt
@@ -36,16 +36,16 @@
title = { Text("Simple TopAppBar") },
navigationIcon = {
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Menu)
+ Icon(Icons.Filled.Menu, contentDescription = null)
}
},
actions = {
// RowScope here, so these icons will be placed horizontally
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
}
)
@@ -56,15 +56,15 @@
fun SimpleBottomAppBar() {
BottomAppBar {
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Menu)
+ Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
// The actions should be at the end of the BottomAppBar
Spacer(Modifier.weight(1f, true))
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
index 368287d..6895b34 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
@@ -58,11 +58,11 @@
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton( scaffoldState.reveal() }) {
- Icon(Icons.Default.Menu)
+ Icon(Icons.Default.Menu, contentDescription = "Localized description")
}
} else {
IconButton( scaffoldState.conceal() }) {
- Icon(Icons.Default.Close)
+ Icon(Icons.Default.Close, contentDescription = "Localized description")
}
}
},
@@ -77,7 +77,7 @@
}
}
) {
- Icon(Icons.Default.Favorite)
+ Icon(Icons.Default.Favorite, contentDescription = "Localized description")
}
},
elevation = 0.dp,
@@ -103,7 +103,12 @@
items(50) {
ListItem(
text = { Text("Item $it") },
- icon = { Icon(Icons.Default.Favorite) }
+ icon = {
+ Icon(
+ Icons.Default.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
)
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomNavigationSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomNavigationSamples.kt
index 321d4bc..b51b529 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomNavigationSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomNavigationSamples.kt
@@ -38,7 +38,7 @@
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(item) },
selected = selectedItem == index,
selectedItem = index }
@@ -55,7 +55,7 @@
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(item) },
selected = selectedItem == index,
selectedItem = index },
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
index 8ef6b56..3a39938 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BottomSheetScaffoldSamples.kt
@@ -89,7 +89,7 @@
title = { Text("Bottom sheet scaffold") },
navigationIcon = {
IconButton( scaffoldState.drawerState.open() }) {
- Icon(Icons.Default.Menu)
+ Icon(Icons.Default.Menu, contentDescription = "Localized description")
}
}
)
@@ -104,7 +104,7 @@
}
}
) {
- Icon(Icons.Default.Favorite)
+ Icon(Icons.Default.Favorite, contentDescription = "Localized description")
}
},
floatingActionButtonPosition = FabPosition.End,
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
index 12cfe41..3539c00 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
@@ -58,7 +58,11 @@
@Composable
fun ButtonWithIconSample() {
Button( /* Do something! */ }) {
- Icon(Icons.Filled.Favorite, Modifier.size(ButtonDefaults.IconSize))
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text("Like")
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/FloatingActionButtonSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/FloatingActionButtonSamples.kt
index a90c6a5..da257ea 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/FloatingActionButtonSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/FloatingActionButtonSamples.kt
@@ -31,7 +31,7 @@
@Composable
fun SimpleFab() {
FloatingActionButton( /*do something*/ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
}
@@ -47,7 +47,7 @@
@Composable
fun SimpleExtendedFabWithIcon() {
ExtendedFloatingActionButton(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
text = { Text("ADD TO BASKET") },
/*do something*/ }
)
@@ -57,7 +57,7 @@
@Composable
fun FluidExtendedFab() {
ExtendedFloatingActionButton(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
text = { Text("FLUID FAB") },
/*do something*/ },
modifier = Modifier.fillMaxWidth()
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/IconButtonSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/IconButtonSamples.kt
index 84ac071..dde741b 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/IconButtonSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/IconButtonSamples.kt
@@ -34,7 +34,7 @@
@Composable
fun IconButtonSample() {
IconButton( /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
}
@@ -45,6 +45,6 @@
IconToggleButton(checked = checked, checked = it }) {
val tint by animateAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
- Icon(Icons.Filled.Favorite, tint = tint)
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description", tint = tint)
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
index 88f67fd..c41ca94 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
@@ -104,17 +104,35 @@
Divider()
ListItem(
text = { Text("One line list item with 24x24 icon") },
- icon = { Image(icon24x24, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon24x24,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
text = { Text("One line list item with 40x40 icon") },
- icon = { Image(icon40x40, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon40x40,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
text = { Text("One line list item with 56x56 icon") },
- icon = { Image(icon56x56, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon56x56,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
@@ -122,6 +140,7 @@
icon = {
Image(
icon56x56,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
},
@@ -130,7 +149,7 @@
Divider()
ListItem(
text = { Text("One line list item with trailing icon") },
- trailing = { Icon(vectorIcon) }
+ trailing = { Icon(vectorIcon, contentDescription = "Localized description") }
)
Divider()
ListItem(
@@ -138,10 +157,11 @@
icon = {
Image(
icon40x40,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
},
- trailing = { Icon(vectorIcon) }
+ trailing = { Icon(vectorIcon, contentDescription = "Localized description") }
)
Divider()
}
@@ -165,13 +185,25 @@
ListItem(
text = { Text("Two line list item with 24x24 icon") },
secondaryText = { Text("Secondary text") },
- icon = { Image(icon24x24, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon24x24,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
text = { Text("Two line list item with 40x40 icon") },
secondaryText = { Text("Secondary text") },
- icon = { Image(icon40x40, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon40x40,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
@@ -181,6 +213,7 @@
icon = {
Image(
icon40x40,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
}
@@ -220,7 +253,13 @@
)
},
singleLineSecondaryText = false,
- icon = { Image(icon24x24, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon24x24,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
@@ -232,7 +271,7 @@
)
},
singleLineSecondaryText = false,
- trailing = { Icon(vectorIcon) }
+ trailing = { Icon(vectorIcon, "Localized description") }
)
Divider()
ListItem(
@@ -254,12 +293,24 @@
Divider()
ListItem(
text = { Text("פריט ברשימה אחת עם תמונה.") },
- icon = { Image(icon40x40, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon40x40,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
text = { Text("One line list item with 24x24 icon") },
- icon = { Image(icon40x40, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon40x40,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
ListItem(
@@ -267,6 +318,7 @@
trailing = {
Image(
icon24x24,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
}
@@ -300,6 +352,7 @@
icon = {
Image(
icon40x40,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
}
@@ -311,6 +364,7 @@
icon = {
Image(
icon40x40,
+ contentDescription = null,
colorFilter = ColorFilter.tint(AmbientContentColor.current)
)
},
@@ -345,7 +399,13 @@
text = { Text("ثلاثة عناصر قائمة مع رمز") },
overlineText = { Text("فوق الخط") },
secondaryText = { Text("نص ثانوي") },
- icon = { Image(icon40x40, colorFilter = ColorFilter.tint(AmbientContentColor.current)) }
+ icon = {
+ Image(
+ icon40x40,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(AmbientContentColor.current)
+ )
+ }
)
Divider()
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
index d423bd0..433bfbc 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
@@ -42,7 +42,7 @@
val iconButton = @Composable {
IconButton( expanded = true }) {
- Icon(Icons.Default.MoreVert)
+ Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
}
}
DropdownMenu(
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
index 9c63c0e..40ce474 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
@@ -50,7 +50,12 @@
items(50) {
ListItem(
text = { Text("Item $it") },
- icon = { Icon(Icons.Default.Favorite) }
+ icon = {
+ Icon(
+ Icons.Default.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
)
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
index 8279439..1b95b94 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
@@ -32,7 +32,6 @@
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.BottomAppBar
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FabPosition
import androidx.compose.material.Icon
@@ -92,7 +91,7 @@
scaffoldState.drawerState.open()
}
) {
- Icon(Icons.Filled.Menu)
+ Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
)
@@ -162,7 +161,7 @@
scaffoldState.drawerState.open()
}
) {
- Icon(Icons.Filled.Menu)
+ Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
},
@@ -192,7 +191,6 @@
@Sampled
@Composable
-@OptIn(ExperimentalMaterialApi::class)
fun ScaffoldWithSimpleSnackbar() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
@@ -220,7 +218,6 @@
}
@Sampled
-@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ScaffoldWithCustomSnackbar() {
val scaffoldState = rememberScaffoldState()
@@ -258,7 +255,7 @@
}
@Sampled
-@OptIn(ExperimentalMaterialApi::class, ExperimentalCoroutinesApi::class, FlowPreview::class)
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun ScaffoldWithCoroutinesSnackbar() {
// decouple snackbar host state from scaffold state for demo purposes
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
index 4a896c2..4e651f3 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
@@ -121,7 +121,11 @@
Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
contentAlignment = alignment
) {
- Icon(icon, Modifier.scale(scale))
+ Icon(
+ icon,
+ contentDescription = "Localized description",
+ modifier = Modifier.scale(scale)
+ )
}
},
dismissContent = {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
index af55358..a61e589 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
@@ -87,7 +87,7 @@
TabRow(selectedTabIndex = state) {
icons.forEachIndexed { index, icon ->
Tab(
- icon = { Icon(icon) },
+ icon = { Icon(icon, contentDescription = "Favorite") },
selected = state == index,
state = index }
)
@@ -114,7 +114,7 @@
titlesAndIcons.forEachIndexed { index, (title, icon) ->
Tab(
text = { Text(title) },
- icon = { Icon(icon) },
+ icon = { Icon(icon, contentDescription = null) },
selected = state == index,
state = index }
)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
index 83553c1..f631a97 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
@@ -75,8 +75,8 @@
value = text,
text = it },
placeholder = { Text("placeholder") },
- leadingIcon = { Icon(Icons.Filled.Favorite) },
- trailingIcon = { Icon(Icons.Filled.Info) }
+ leadingIcon = { Icon(Icons.Filled.Favorite, contentDescription = "Localized description") },
+ trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Localized description") }
)
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
index 05b8283..39baeaf 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
@@ -198,7 +198,7 @@
*/
private val FakeIcon = @Composable { modifier: Modifier ->
IconButton( modifier = modifier) {
- Icon(ColorPainter(Color.Red))
+ Icon(ColorPainter(Color.Red), null)
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
index b32d261..c347707 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
@@ -316,18 +316,18 @@
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
BottomNavigation {
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = true,
>
interactionState = interactionState
)
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = false,
>
)
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = false,
>
)
@@ -359,7 +359,7 @@
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
BottomNavigation(backgroundColor = backgroundColor) {
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = true,
>
interactionState = interactionState,
@@ -367,14 +367,14 @@
unselectedContentColor = unselectedContentColor
)
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = false,
>
selectedContentColor = selectedContentColor,
unselectedContentColor = unselectedContentColor
)
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = false,
>
selectedContentColor = selectedContentColor,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
index 107384f..dcdef28 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
@@ -81,7 +81,7 @@
BottomNavigation {
repeat(4) { index ->
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
label = { Text("Item $index") },
selected = index == 0,
>
@@ -119,7 +119,7 @@
BottomNavigationItem(
modifier = Modifier.testTag("item"),
icon = {
- Icon(Icons.Filled.Favorite, Modifier.testTag("icon"))
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
},
label = {
Text("ItemText")
@@ -164,7 +164,7 @@
BottomNavigationItem(
modifier = Modifier.testTag("item"),
icon = {
- Icon(Icons.Filled.Favorite, Modifier.testTag("icon"))
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
},
label = {
Text("ItemText")
@@ -198,7 +198,7 @@
BottomNavigationItem(
modifier = Modifier.testTag("item"),
icon = {
- Icon(Icons.Filled.Favorite, Modifier.testTag("icon"))
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
},
label = {},
selected = false,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
index 7349c2a..9cc7ffc 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
@@ -327,7 +327,7 @@
}.testTag(fabTag),
>
) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
},
bodyContent = { Text("Content") }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
index 10d8101..c00a8b0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -65,7 +65,7 @@
rule.setMaterialContent {
Box {
FloatingActionButton(modifier = Modifier.testTag("myButton"), {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
}
}
@@ -99,7 +99,7 @@
rule
.setMaterialContentForSizeAssertions {
FloatingActionButton( {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
}
.assertIsSquareWithSize(56.dp)
@@ -111,7 +111,7 @@
ExtendedFloatingActionButton(
modifier = Modifier.testTag("FAB"),
text = { Text("Extended FAB Text") },
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
>
)
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt
index 9953c9c..dea21b2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/IconTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
@@ -41,6 +42,7 @@
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
import org.junit.runner.RunWith
@@ -58,7 +60,7 @@
val vector = Icons.Filled.Menu
rule
.setMaterialContentForSizeAssertions {
- Icon(vector)
+ Icon(vector, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -74,7 +76,7 @@
).build()
rule
.setMaterialContentForSizeAssertions {
- Icon(vector)
+ Icon(vector, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -90,7 +92,7 @@
ImageBitmap(width.toIntPx(), height.toIntPx())
}
- Icon(image)
+ Icon(image, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -107,7 +109,7 @@
ImageBitmap(width.toIntPx(), height.toIntPx())
}
- Icon(image)
+ Icon(image, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -120,7 +122,7 @@
val painter = ColorPainter(Color.Red)
rule
.setMaterialContentForSizeAssertions {
- Icon(painter)
+ Icon(painter, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -138,7 +140,7 @@
}
val imagePainter = ImagePainter(image)
- Icon(imagePainter)
+ Icon(imagePainter, null)
}
.assertWidthIsEqualTo(width)
.assertHeightIsEqualTo(height)
@@ -160,7 +162,7 @@
Color.Red
)
}
- Icon(image, modifier = Modifier.testTag(testTag), tint = Color.Unspecified)
+ Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Unspecified)
}
// With no color provided for a tint, the icon should render the original pixels
@@ -183,13 +185,31 @@
Color.Red
)
}
- Icon(image, modifier = Modifier.testTag(testTag), tint = Color.Blue)
+ Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Blue)
}
// With a tint color provided, all pixels should be blue
rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
}
+ @Test
+ fun contentDescriptionAppliedToIcon() {
+ val testTag = "TestTag"
+ rule.setContent {
+ Icon(
+ bitmap = ImageBitmap(100, 100),
+ contentDescription = "qwerty",
+ modifier = Modifier.testTag(testTag)
+ )
+ }
+
+ rule.onNodeWithTag(testTag).fetchSemanticsNode().let {
+ assertThat(it.config.contains(SemanticsProperties.ContentDescription)).isTrue()
+ assertThat(it.config[SemanticsProperties.ContentDescription])
+ .isEqualTo("qwerty")
+ }
+ }
+
private fun createBitmapWithColor(
density: Density,
width: Int,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
index 809ca2f..087e32e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
@@ -27,10 +27,13 @@
import androidx.compose.ui.node.Ref
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -68,7 +71,7 @@
.setMaterialContentForSizeAssertions {
ListItem(
text = { Text("Primary text") },
- icon = { Icon(icon24x24) }
+ icon = { Icon(icon24x24, null) }
)
}
.assertHeightIsEqualTo(expectedHeightSmallIcon)
@@ -82,7 +85,7 @@
.setMaterialContentForSizeAssertions {
ListItem(
text = { Text("Primary text") },
- icon = { Icon(icon56x56) }
+ icon = { Icon(icon56x56, null) }
)
}
.assertHeightIsEqualTo(expectedHeightLargeIcon)
@@ -112,7 +115,7 @@
ListItem(
text = { Text("Primary text") },
secondaryText = { Text("Secondary text") },
- icon = { Icon(icon24x24) }
+ icon = { Icon(icon24x24, null) }
)
}
.assertHeightIsEqualTo(expectedHeightWithIcon)
@@ -196,11 +199,12 @@
ListItem(
text = { Text("Primary text", Modifier.saveLayout(textPosition, textSize)) },
trailing = {
- Image(icon24x24, Modifier.saveLayout(trailingPosition, trailingSize))
+ Image(icon24x24, null, Modifier.saveLayout(trailingPosition, trailingSize))
}
)
}
}
+ val ds = rule.onRoot().getUnclippedBoundsInRoot()
rule.runOnIdleWithDensity {
assertThat(textPosition.value!!.x).isEqualTo(
expectedLeftPadding.toIntPx()
@@ -209,9 +213,8 @@
assertThat(textPosition.value!!.y).isEqualTo(
((listItemHeight.toIntPx() - textSize.value!!.height) / 2f).roundToInt().toFloat()
)
- val ds = rule.displaySize
assertThat(trailingPosition.value!!.x).isEqualTo(
- ds.width - trailingSize.value!!.width -
+ ds.width.toIntPx() - trailingSize.value!!.width -
expectedRightPadding.toIntPx().toFloat()
)
assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -235,7 +238,7 @@
Box {
ListItem(
text = { Text("Primary text", Modifier.saveLayout(textPosition, textSize)) },
- icon = { Image(icon24x24, Modifier.saveLayout(iconPosition, iconSize)) }
+ icon = { Image(icon24x24, null, Modifier.saveLayout(iconPosition, iconSize)) }
)
}
}
@@ -301,6 +304,7 @@
)
}
}
+ val ds = rule.onRoot().getUnclippedBoundsInRoot()
rule.runOnIdleWithDensity {
assertThat(textPosition.value!!.x).isEqualTo(
expectedLeftPadding.toIntPx().toFloat()
@@ -315,9 +319,8 @@
expectedTextBaseline.toIntPx().toFloat() +
expectedSecondaryTextBaselineOffset.toIntPx().toFloat()
)
- val ds = rule.displaySize
assertThat(trailingPosition.value!!.x).isEqualTo(
- ds.width - trailingSize.value!!.width -
+ ds.width.toIntPx() - trailingSize.value!!.width -
expectedRightPadding.toIntPx().toFloat()
)
assertThat(trailingBaseline.value!!).isEqualTo(
@@ -362,7 +365,7 @@
)
},
icon = {
- Image(icon24x24, Modifier.saveLayout(iconPosition, iconSize))
+ Image(icon24x24, null, Modifier.saveLayout(iconPosition, iconSize))
}
)
}
@@ -433,14 +436,15 @@
)
},
icon = {
- Image(icon40x40, Modifier.saveLayout(iconPosition, iconSize))
+ Image(icon40x40, null, Modifier.saveLayout(iconPosition, iconSize))
},
trailing = {
- Image(icon24x24, Modifier.saveLayout(trailingPosition, trailingSize))
+ Image(icon24x24, null, Modifier.saveLayout(trailingPosition, trailingSize))
}
)
}
}
+ val ds = rule.onRoot().getUnclippedBoundsInRoot()
rule.runOnIdleWithDensity {
assertThat(textPosition.value!!.x).isEqualTo(
expectedLeftPadding.toIntPx().toFloat() + iconSize.value!!.width +
@@ -464,9 +468,8 @@
assertThat(iconPosition.value!!.y).isEqualTo(
expectedIconTopPadding.toIntPx().toFloat()
)
- val ds = rule.displaySize
assertThat(trailingPosition.value!!.x).isEqualTo(
- ds.width - trailingSize.value!!.width -
+ ds.width.toIntPx() - trailingSize.value!!.width -
expectedRightPadding.toIntPx().toFloat()
)
assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -515,14 +518,15 @@
},
singleLineSecondaryText = false,
icon = {
- Image(icon24x24, Modifier.saveLayout(iconPosition, iconSize))
+ Image(icon24x24, null, Modifier.saveLayout(iconPosition, iconSize))
},
trailing = {
- Image(icon24x24, Modifier.saveLayout(trailingPosition, trailingSize))
+ Image(icon24x24, null, Modifier.saveLayout(trailingPosition, trailingSize))
}
)
}
}
+ val ds = rule.onRoot().getUnclippedBoundsInRoot()
rule.runOnIdleWithDensity {
assertThat(textPosition.value!!.x).isEqualTo(
expectedLeftPadding.toIntPx().toFloat() + iconSize.value!!.width +
@@ -543,9 +547,8 @@
assertThat(iconPosition.value!!.y).isEqualTo(
expectedIconTopPadding.toIntPx().toFloat()
)
- val ds = rule.displaySize
assertThat(trailingPosition.value!!.x).isEqualTo(
- ds.width - trailingSize.value!!.width.toFloat() -
+ ds.width.toIntPx() - trailingSize.value!!.width.toFloat() -
expectedRightPadding.toIntPx().toFloat()
)
assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -610,6 +613,7 @@
icon = {
Image(
icon40x40,
+ null,
Modifier.saveLayout(iconPosition, iconSize)
)
},
@@ -626,6 +630,8 @@
)
}
}
+
+ val ds = rule.onRoot().getUnclippedBoundsInRoot()
rule.runOnIdleWithDensity {
assertThat(textPosition.value!!.x).isEqualTo(
expectedLeftPadding.toIntPx().toFloat() +
@@ -660,9 +666,8 @@
assertThat(iconPosition.value!!.y).isEqualTo(
expectedIconTopPadding.toIntPx().toFloat()
)
- val ds = rule.displaySize
assertThat(trailingPosition.value!!.x).isEqualTo(
- ds.width - trailingSize.value!!.width -
+ ds.width.toIntPx() - trailingSize.value!!.width -
expectedRightPadding.toIntPx().toFloat()
)
assertThat(trailingBaseline.value!!).isEqualTo(
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
index 6cbca52..4f967de 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material
import android.os.Build
+import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
@@ -34,6 +35,7 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.width
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -248,7 +250,7 @@
private fun FilledTextFieldTestContent(colors: Colors) {
MaterialTheme(colors) {
Surface(Modifier.testTag(Tag)) {
- TextField(value = Text, >
+ TextField(value = Text, modifier = Modifier.width(280.dp))
}
}
}
@@ -257,7 +259,7 @@
private fun OutlinedTextFieldTestContent(colors: Colors) {
MaterialTheme(colors) {
Surface(Modifier.testTag(Tag)) {
- OutlinedTextField(value = Text, >
+ OutlinedTextField(value = Text, modifier = Modifier.width(280.dp))
}
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
index 0deb853..9600b81 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
@@ -48,7 +48,6 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class)
class ScaffoldScreenshotTest {
@get:Rule
@@ -602,7 +601,6 @@
* @param rtl whether to set [LayoutDirection.Rtl] as the [LayoutDirection] for this Scaffold and
* its content
*/
-@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun ScreenshotScaffold(
showTopAppBar: Boolean,
@@ -625,7 +623,7 @@
val cutoutShape = if (fabCutout) CircleShape else null
BottomAppBar(cutoutShape = cutoutShape) {
IconButton( {
- Icon(Icons.Filled.Menu)
+ Icon(Icons.Filled.Menu, null)
}
}
}
@@ -647,7 +645,7 @@
val fab = @Composable {
if (showFab) {
FloatingActionButton(
- content = { Icon(Icons.Filled.Favorite) },
+ content = { Icon(Icons.Filled.Favorite, null) },
>
)
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
index aa6181f0..d33dc24 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -285,7 +285,7 @@
},
>
) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
},
floatingActionButtonPosition = FabPosition.Center,
@@ -321,7 +321,7 @@
},
>
) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
},
floatingActionButtonPosition = FabPosition.End,
@@ -394,7 +394,7 @@
},
>
) {
- Icon(Icons.Filled.Favorite)
+ Icon(Icons.Filled.Favorite, null)
}
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
index 87ef7af..84b8fc6 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
@@ -37,7 +37,6 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@LargeTest
-@OptIn(ExperimentalMaterialApi::class)
class SnackbarHostTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt
index b8281ba..287c882 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarTest.kt
@@ -365,7 +365,6 @@
}
@Test
- @OptIn(ExperimentalMaterialApi::class)
fun defaultSnackbar_dataVersion_proxiesParameters() {
var clicked = false
val snackbarData = object : SnackbarData {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
index 65e2389..ea0bd06 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
@@ -114,7 +114,7 @@
fun iconTab_height() {
rule
.setMaterialContentForSizeAssertions {
- Tab(icon = { Icon(icon) }, selected = true, >
+ Tab(icon = { Icon(icon, null) }, selected = true, >
}
.assertHeightIsEqualTo(ExpectedSmallTabHeight)
}
@@ -126,7 +126,7 @@
Surface {
Tab(
text = { Text("Text and Icon") },
- icon = { Icon(icon) },
+ icon = { Icon(icon, null) },
selected = true,
>
)
@@ -245,7 +245,7 @@
text = {
Text(title, Modifier.testTag("text"))
},
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, null) },
selected = state == index,
state = index }
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index 285e619..690973d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -80,7 +80,8 @@
OutlinedTextField(
value = "Text",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -95,7 +96,8 @@
OutlinedTextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -110,7 +112,8 @@
OutlinedTextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -128,7 +131,8 @@
OutlinedTextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -147,7 +151,8 @@
value = "Input",
>
label = { Text("Label") },
- isErrorValue = true
+ isErrorValue = true,
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -165,7 +170,8 @@
value = "",
>
label = { Text("Label") },
- isErrorValue = true
+ isErrorValue = true,
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -180,7 +186,7 @@
OutlinedTextField(
value = "Hello, world!",
>
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
}
@@ -195,7 +201,7 @@
value = "Text",
>
label = { Text("Label") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -208,7 +214,7 @@
OutlinedTextField(
value = "Text",
>
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -223,7 +229,7 @@
>
label = { Text("Label") },
placeholder = { Text("placeholder") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -239,7 +245,7 @@
value = "",
>
placeholder = { Text("placeholder") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -255,7 +261,7 @@
value = "",
>
label = { Text("Label") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -270,7 +276,7 @@
>
singleLine = true,
label = { Text("Label") },
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
@@ -284,7 +290,7 @@
value = "Text",
>
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
@@ -300,7 +306,7 @@
placeholder = { Text("placeholder") },
label = { Text("Label") },
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
@@ -317,7 +323,7 @@
>
placeholder = { Text("placeholder") },
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
@@ -335,7 +341,7 @@
value = "",
>
label = { Text("Label") },
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp)
)
}
@@ -350,7 +356,8 @@
value = TextFieldValue("Text"),
>
singleLine = true,
- enabled = false
+ enabled = false,
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -366,7 +373,8 @@
value = TextFieldValue("Text"),
>
singleLine = true,
- enabled = false
+ enabled = false,
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -401,7 +409,7 @@
OutlinedTextField(
value = TextFieldValue("Text"),
>
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp),
enabled = true,
readOnly = true
)
@@ -416,7 +424,7 @@
OutlinedTextField(
value = TextFieldValue("Text"),
>
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.testTag(TextFieldTag).width(280.dp),
enabled = true,
readOnly = true
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index 1a96cf0..238014e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AmbientContentAlpha
import androidx.compose.material.AmbientContentColor
@@ -29,9 +30,11 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
+import androidx.compose.material.TextField
import androidx.compose.material.TextFieldPadding
import androidx.compose.material.runOnIdleWithDensity
import androidx.compose.material.setMaterialContent
+import androidx.compose.material.setMaterialContentForSizeAssertions
import androidx.compose.runtime.Providers
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -48,6 +51,7 @@
import androidx.compose.ui.platform.AmbientTextInputService
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
@@ -84,6 +88,7 @@
@OptIn(ExperimentalTestApi::class)
class OutlinedTextFieldTest {
private val ExpectedMinimumTextFieldHeight = 56.dp
+ private val ExpectedDefaultTextFieldWidth = 280.dp
private val ExpectedPadding = 16.dp
private val IconPadding = 12.dp
private val IconColorAlpha = 0.54f
@@ -93,6 +98,31 @@
val rule = createComposeRule()
@Test
+ fun testTextField_setSmallWidth() {
+ rule.setMaterialContentForSizeAssertions {
+ TextField(
+ value = "input",
+ >
+ label = {},
+ modifier = Modifier.width(40.dp)
+ )
+ }
+ .assertWidthIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun testTextField_defaultWidth() {
+ rule.setMaterialContentForSizeAssertions {
+ TextField(
+ value = "input",
+ >
+ label = {}
+ )
+ }
+ .assertWidthIsEqualTo(ExpectedDefaultTextFieldWidth)
+ }
+
+ @Test
fun testOutlinedTextFields_singleFocus() {
var textField1Focused = false
val textField1Tag = "TextField1"
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
index 96c7d79..77c1ac7 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
@@ -79,7 +79,8 @@
TextField(
value = "Text",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -94,7 +95,8 @@
TextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -109,7 +111,8 @@
TextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -127,7 +130,8 @@
TextField(
value = "",
>
- label = { Text("Label") }
+ label = { Text("Label") },
+ modifier = Modifier.width(280.dp)
)
}
}
@@ -146,7 +150,7 @@
>
label = { Text("Label") },
isErrorValue = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -163,7 +167,7 @@
>
label = { Text("Label") },
isErrorValue = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -177,7 +181,7 @@
TextField(
value = "Hello, world!",
>
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
}
@@ -192,7 +196,7 @@
value = "Text",
>
label = { Text("Label") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -205,7 +209,7 @@
TextField(
value = "Text",
>
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -220,7 +224,7 @@
>
label = { Text("Label") },
placeholder = { Text("placeholder") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -236,7 +240,7 @@
value = "",
>
placeholder = { Text("placeholder") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -252,7 +256,7 @@
value = "",
>
label = { Text("Label") },
- modifier = Modifier.height(300.dp).testTag(TextFieldTag)
+ modifier = Modifier.height(300.dp).width(280.dp).testTag(TextFieldTag)
)
}
@@ -267,7 +271,7 @@
>
singleLine = true,
label = { Text("Label") },
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -281,7 +285,7 @@
value = "Text",
>
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -297,7 +301,7 @@
placeholder = { Text("placeholder") },
label = { Text("Label") },
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -314,7 +318,7 @@
>
placeholder = { Text("placeholder") },
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -332,7 +336,7 @@
value = "",
>
label = { Text("Label") },
- modifier = Modifier.testTag(TextFieldTag)
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag)
)
}
@@ -345,7 +349,7 @@
TextField(
value = TextFieldValue("Text"),
>
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag),
singleLine = true,
enabled = false
)
@@ -361,7 +365,7 @@
value = TextFieldValue("Text"),
>
singleLine = true,
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag),
enabled = false
)
}
@@ -394,7 +398,7 @@
TextField(
value = TextFieldValue("Text"),
>
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag),
enabled = true,
readOnly = true
)
@@ -409,7 +413,7 @@
TextField(
value = TextFieldValue("Text"),
>
- modifier = Modifier.testTag(TextFieldTag),
+ modifier = Modifier.width(280.dp).testTag(TextFieldTag),
enabled = true,
readOnly = true
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index d7af753..bf96756 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -20,12 +20,15 @@
import android.os.Build
import android.view.View
import android.view.inputmethod.InputMethodManager
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AmbientContentAlpha
import androidx.compose.material.AmbientContentColor
@@ -46,8 +49,6 @@
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@@ -60,6 +61,7 @@
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
@@ -90,8 +92,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
@@ -99,7 +99,8 @@
@OptIn(ExperimentalTestApi::class)
class TextFieldTest {
- private val ExpectedMinimumTextFieldHeight = 56.dp
+ private val ExpectedDefaultTextFieldHeight = 56.dp
+ private val ExpectedDefaultTextFieldWidth = 280.dp
private val ExpectedPadding = 16.dp
private val IconPadding = 12.dp
private val ExpectedBaselineOffset = 20.dp
@@ -120,34 +121,56 @@
modifier = Modifier.preferredHeight(20.dp)
)
}
- .assertHeightIsEqualTo(ExpectedMinimumTextFieldHeight)
+ .assertHeightIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun testTextField_setSmallWidth() {
+ rule.setMaterialContentForSizeAssertions {
+ TextField(
+ value = "input",
+ >
+ label = {},
+ modifier = Modifier.width(40.dp)
+ )
+ }
+ .assertWidthIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun testTextField_defaultWidth() {
+ rule.setMaterialContentForSizeAssertions {
+ TextField(
+ value = "input",
+ >
+ label = {}
+ )
+ }
+ .assertWidthIsEqualTo(ExpectedDefaultTextFieldWidth)
}
@Test
fun testTextFields_singleFocus() {
- var textField1Focused = false
val textField1Tag = "TextField1"
-
- var textField2Focused = false
val textField2Tag = "TextField2"
+ val interactionState1 = InteractionState()
+ val interactionState2 = InteractionState()
rule.setMaterialContent {
Column {
TextField(
- modifier = Modifier
- .onFocusChanged { textField1Focused = it.isFocused }
- .testTag(textField1Tag),
+ modifier = Modifier.testTag(textField1Tag),
value = "input1",
>
- label = {}
+ label = {},
+ interactionState = interactionState1
)
TextField(
- modifier = Modifier
- .onFocusChanged { textField2Focused = it.isFocused }
- .testTag(textField2Tag),
+ modifier = Modifier.testTag(textField2Tag),
value = "input2",
>
- label = {}
+ label = {},
+ interactionState = interactionState2
)
}
}
@@ -155,32 +178,29 @@
rule.onNodeWithTag(textField1Tag).performClick()
rule.runOnIdle {
- assertThat(textField1Focused).isTrue()
- assertThat(textField2Focused).isFalse()
+ assertThat(interactionState1.contains(Interaction.Focused)).isTrue()
+ assertThat(interactionState2.contains(Interaction.Focused)).isFalse()
}
rule.onNodeWithTag(textField2Tag).performClick()
rule.runOnIdle {
- assertThat(textField1Focused).isFalse()
- assertThat(textField2Focused).isTrue()
+ assertThat(interactionState1.contains(Interaction.Focused)).isFalse()
+ assertThat(interactionState2.contains(Interaction.Focused)).isTrue()
}
}
@Test
fun testTextField_getFocus_whenClickedOnSurfaceArea() {
- var focused = false
+ val interactionState = InteractionState()
rule.setMaterialContent {
- Box {
- TextField(
- modifier = Modifier
- .onFocusChanged { focused = it.isFocused }
- .testTag(TextfieldTag),
- value = "input",
- >
- label = {}
- )
- }
+ TextField(
+ modifier = Modifier.testTag(TextfieldTag),
+ value = "input",
+ >
+ label = {},
+ interactionState = interactionState
+ )
}
// Click on (2, 2) which is Surface area and outside input area
@@ -189,7 +209,7 @@
}
rule.runOnIdleWithDensity {
- assertThat(focused).isTrue()
+ assertThat(interactionState.contains(Interaction.Focused)).isTrue()
}
}
@@ -294,7 +314,7 @@
ExpectedPadding.toIntPx().toFloat()
)
assertThat(labelPosition.value?.y).isEqualTo(
- ((ExpectedMinimumTextFieldHeight.toIntPx() - labelSize.value!!.height) / 2f)
+ ((ExpectedDefaultTextFieldHeight.toIntPx() - labelSize.value!!.height) / 2f)
.roundToInt().toFloat()
)
}
@@ -857,21 +877,20 @@
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testTextField_alphaNotApplied_toCustomBackgroundColorAndTransparentColors() {
- val latch = CountDownLatch(1)
+ val interactionState = InteractionState()
rule.setMaterialContent {
Box(Modifier.background(color = Color.White)) {
TextField(
- modifier = Modifier
- .onFocusChanged { if (it.isFocused) latch.countDown() }
- .testTag(TextfieldTag),
+ modifier = Modifier.testTag(TextfieldTag),
value = "",
>
label = {},
shape = RectangleShape,
backgroundColor = Color.Blue,
activeColor = Color.Transparent,
- inactiveColor = Color.Transparent
+ inactiveColor = Color.Transparent,
+ interactionState = interactionState
)
}
}
@@ -888,7 +907,9 @@
)
rule.onNodeWithTag(TextfieldTag).performClick()
- assert(latch.await(1, TimeUnit.SECONDS))
+ rule.runOnIdle {
+ assertThat(interactionState.contains(Interaction.Focused)).isTrue()
+ }
rule.onNodeWithTag(TextfieldTag)
.captureToImage()
@@ -953,11 +974,12 @@
}
}
- private val View.isSoftwareKeyboardShown: Boolean get() {
- val inputMethodManager =
- context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- // TODO(b/163742556): This is just a proxy for software keyboard visibility. Find a better
- // way to check if the software keyboard is shown.
- return inputMethodManager.isAcceptingText()
- }
+ private val View.isSoftwareKeyboardShown: Boolean
+ get() {
+ val inputMethodManager =
+ context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ // TODO(b/163742556): This is just a proxy for software keyboard visibility. Find a better
+ // way to check if the software keyboard is shown.
+ return inputMethodManager.isAcceptingText()
+ }
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index 9354d16..29b778a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -18,10 +18,7 @@
package androidx.compose.material
-import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.asDisposableClock
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.BorderStroke
@@ -37,15 +34,18 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Providers
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
-import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -108,13 +108,13 @@
// the ripple below the clip once http://b/157687898 is fixed and we have
// more flexibility to move the clickable modifier (see candidate approach
// aosp/1361921)
- val contentColor = colors.contentColor(enabled)
+ val contentColor by colors.contentColor(enabled)
Surface(
shape = shape,
- color = colors.backgroundColor(enabled),
+ color = colors.backgroundColor(enabled).value,
contentColor = contentColor.copy(alpha = 1f),
border = border,
- elevation = elevation?.elevation(enabled, interactionState) ?: 0.dp,
+ elevation = elevation?.elevation(enabled, interactionState)?.value ?: 0.dp,
modifier = modifier.clickable(
>
enabled = enabled,
@@ -281,7 +281,8 @@
* @param enabled whether the button is enabled
* @param interactionState the [InteractionState] for this button
*/
- fun elevation(enabled: Boolean, interactionState: InteractionState): Dp
+ @Composable
+ fun elevation(enabled: Boolean, interactionState: InteractionState): State<Dp>
}
/**
@@ -300,14 +301,16 @@
*
* @param enabled whether the button is enabled
*/
- fun backgroundColor(enabled: Boolean): Color
+ @Composable
+ fun backgroundColor(enabled: Boolean): State<Color>
/**
* Represents the content color for this button, depending on [enabled].
*
* @param enabled whether the button is enabled
*/
- fun contentColor(enabled: Boolean): Color
+ @Composable
+ fun contentColor(enabled: Boolean): State<Color>
}
/**
@@ -373,13 +376,11 @@
// hovered: Dp = 4.dp,
disabledElevation: Dp = 0.dp
): ButtonElevation {
- val clock = AmbientAnimationClock.current.asDisposableClock()
- return remember(defaultElevation, pressedElevation, disabledElevation, clock) {
+ return remember(defaultElevation, pressedElevation, disabledElevation) {
DefaultButtonElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
- disabledElevation = disabledElevation,
- clock = clock
+ disabledElevation = disabledElevation
)
}
}
@@ -492,13 +493,9 @@
private val defaultElevation: Dp,
private val pressedElevation: Dp,
private val disabledElevation: Dp,
- private val clock: AnimationClockObservable
) : ButtonElevation {
- private val lazyAnimatedElevation = LazyAnimatedValue<Dp, AnimationVector1D> { target ->
- AnimatedValueModel(initialValue = target, typeConverter = Dp.VectorConverter, clock = clock)
- }
-
- override fun elevation(enabled: Boolean, interactionState: InteractionState): Dp {
+ @Composable
+ override fun elevation(enabled: Boolean, interactionState: InteractionState): State<Dp> {
val interaction = interactionState.value.lastOrNull {
it is Interaction.Pressed
}
@@ -512,18 +509,18 @@
}
}
- val animatedElevation = lazyAnimatedElevation.animatedValueForTarget(target)
+ val animatable = remember { Animatable(target, Dp.VectorConverter) }
- if (animatedElevation.targetValue != target) {
- if (!enabled) {
- // No transition when moving to a disabled state
- animatedElevation.snapTo(target)
- } else {
- val lastInteraction = when (animatedElevation.targetValue) {
+ if (!enabled) {
+ // No transition when moving to a disabled state
+ animatable.snapTo(target)
+ } else {
+ LaunchedEffect(target) {
+ val lastInteraction = when (animatable.targetValue) {
pressedElevation -> Interaction.Pressed
else -> null
}
- animatedElevation.animateElevation(
+ animatable.animateElevation(
from = lastInteraction,
to = interaction,
target = target
@@ -531,7 +528,7 @@
}
}
- return animatedElevation.value
+ return animatable.asState()
}
}
@@ -546,12 +543,14 @@
private val contentColor: Color,
private val disabledContentColor: Color
) : ButtonColors {
- override fun backgroundColor(enabled: Boolean): Color {
- return if (enabled) backgroundColor else disabledBackgroundColor
+ @Composable
+ override fun backgroundColor(enabled: Boolean): State<Color> {
+ return rememberUpdatedState(if (enabled) backgroundColor else disabledBackgroundColor)
}
- override fun contentColor(enabled: Boolean): Color {
- return if (enabled) contentColor else disabledContentColor
+ @Composable
+ override fun contentColor(enabled: Boolean): State<Color> {
+ return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
}
override fun equals(other: Any?): Boolean {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
index d2493d0..78915f1 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
@@ -16,11 +16,7 @@
package androidx.compose.material
-import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
-import androidx.compose.animation.asDisposableClock
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.animateAsState
import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.TransitionSpec
import androidx.compose.animation.core.keyframes
@@ -39,7 +35,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
@@ -52,7 +51,6 @@
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
@@ -169,7 +167,8 @@
*
* @param state the [ToggleableState] of the checkbox
*/
- fun checkmarkColor(state: ToggleableState): Color
+ @Composable
+ fun checkmarkColor(state: ToggleableState): State<Color>
/**
* Represents the color used for the box (background) of the checkbox, depending on [enabled]
@@ -178,7 +177,8 @@
* @param enabled whether the checkbox is enabled or not
* @param state the [ToggleableState] of the checkbox
*/
- fun boxColor(enabled: Boolean, state: ToggleableState): Color
+ @Composable
+ fun boxColor(enabled: Boolean, state: ToggleableState): State<Color>
/**
* Represents the color used for the border of the checkbox, depending on [enabled] and [state].
@@ -186,7 +186,8 @@
* @param enabled whether the checkbox is enabled or not
* @param state the [ToggleableState] of the checkbox
*/
- fun borderColor(enabled: Boolean, state: ToggleableState): Color
+ @Composable
+ fun borderColor(enabled: Boolean, state: ToggleableState): State<Color>
}
/**
@@ -213,14 +214,12 @@
disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
): CheckboxColors {
- val clock = AmbientAnimationClock.current.asDisposableClock()
return remember(
checkedColor,
uncheckedColor,
checkmarkColor,
disabledColor,
disabledIndeterminateColor,
- clock
) {
DefaultCheckboxColors(
checkedBorderColor = checkedColor,
@@ -234,7 +233,6 @@
uncheckedBorderColor = uncheckedColor,
disabledBorderColor = disabledColor,
disabledIndeterminateBorderColor = disabledIndeterminateColor,
- clock = clock
)
}
}
@@ -250,11 +248,10 @@
) {
val state = transition(definition = TransitionDefinition, toState = value)
val checkCache = remember { CheckDrawingCache() }
+ val checkColor by colors.checkmarkColor(value)
+ val boxColor by colors.boxColor(enabled, value)
+ val borderColor by colors.borderColor(enabled, value)
Canvas(modifier.wrapContentSize(Alignment.Center).size(CheckboxSize)) {
- val checkColor = colors.checkmarkColor(value)
- val boxColor = colors.boxColor(enabled, value)
- val borderColor = colors.borderColor(enabled, value)
-
val strokeWidthPx = StrokeWidth.toPx()
drawBox(
boxColor = boxColor,
@@ -357,36 +354,22 @@
private val checkedBorderColor: Color,
private val uncheckedBorderColor: Color,
private val disabledBorderColor: Color,
- private val disabledIndeterminateBorderColor: Color,
- private val clock: AnimationClockObservable
+ private val disabledIndeterminateBorderColor: Color
) : CheckboxColors {
- private val lazyAnimatedCheckmarkColor = LazyAnimatedValue<Color, AnimationVector4D> { target ->
- AnimatedValueModel(target, (Color.VectorConverter)(target.colorSpace), clock)
- }
- private val lazyAnimatedBoxColor = LazyAnimatedValue<Color, AnimationVector4D> { target ->
- AnimatedValueModel(target, (Color.VectorConverter)(target.colorSpace), clock)
- }
- private val lazyAnimatedBorderColor = LazyAnimatedValue<Color, AnimationVector4D> { target ->
- AnimatedValueModel(target, (Color.VectorConverter)(target.colorSpace), clock)
- }
-
- override fun checkmarkColor(state: ToggleableState): Color {
+ @Composable
+ override fun checkmarkColor(state: ToggleableState): State<Color> {
val target = if (state == ToggleableState.Off) {
uncheckedCheckmarkColor
} else {
checkedCheckmarkColor
}
- val animatedCheckmarkColor = lazyAnimatedCheckmarkColor.animatedValueForTarget(target)
-
- if (animatedCheckmarkColor.targetValue != target) {
- val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
- animatedCheckmarkColor.animateTo(target, tween(durationMillis = duration))
- }
- return animatedCheckmarkColor.value
+ val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
+ return animateAsState(target, tween(durationMillis = duration))
}
- override fun boxColor(enabled: Boolean, state: ToggleableState): Color {
+ @Composable
+ override fun boxColor(enabled: Boolean, state: ToggleableState): State<Color> {
val target = if (enabled) {
when (state) {
ToggleableState.On, ToggleableState.Indeterminate -> checkedBoxColor
@@ -403,19 +386,15 @@
// If not enabled 'snap' to the disabled state, as there should be no animations between
// enabled / disabled.
return if (enabled) {
- val animatedBoxColor = lazyAnimatedBoxColor.animatedValueForTarget(target)
-
- if (animatedBoxColor.targetValue != target) {
- val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
- animatedBoxColor.animateTo(target, tween(durationMillis = duration))
- }
- animatedBoxColor.value
+ val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
+ animateAsState(target, tween(durationMillis = duration))
} else {
- target
+ rememberUpdatedState(target)
}
}
- override fun borderColor(enabled: Boolean, state: ToggleableState): Color {
+ @Composable
+ override fun borderColor(enabled: Boolean, state: ToggleableState): State<Color> {
val target = if (enabled) {
when (state) {
ToggleableState.On, ToggleableState.Indeterminate -> checkedBorderColor
@@ -431,15 +410,10 @@
// If not enabled 'snap' to the disabled state, as there should be no animations between
// enabled / disabled.
return if (enabled) {
- val animatedBorderColor = lazyAnimatedBorderColor.animatedValueForTarget(target)
-
- if (animatedBorderColor.targetValue != target) {
- val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
- animatedBorderColor.animateTo(target, tween(durationMillis = duration))
- }
- animatedBorderColor.value
+ val duration = if (state == ToggleableState.Off) BoxOutDuration else BoxInDuration
+ animateAsState(target, tween(durationMillis = duration))
} else {
- target
+ rememberUpdatedState(target)
}
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index 3f8da84..afb38dc 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -16,7 +16,7 @@
package androidx.compose.material
-import androidx.compose.animation.core.AnimatedValue
+import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FastOutSlowInEasing
@@ -40,7 +40,7 @@
* @param target the [Dp] target elevation for this component, corresponding to the elevation
* desired for the [to] state.
*/
-fun AnimatedValue<Dp, *>.animateElevation(
+suspend fun Animatable<Dp, *>.animateElevation(
from: Interaction? = null,
to: Interaction? = null,
target: Dp
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index 7ccbb7e..f883ac1 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -16,10 +16,7 @@
package androidx.compose.material
-import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.asDisposableClock
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.Interaction
@@ -35,14 +32,15 @@
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Providers
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -96,7 +94,7 @@
shape = shape,
color = backgroundColor,
contentColor = contentColor,
- elevation = elevation.elevation(interactionState)
+ elevation = elevation.elevation(interactionState).value
) {
Providers(AmbientContentAlpha provides contentColor.alpha) {
ProvideTextStyle(MaterialTheme.typography.button) {
@@ -201,7 +199,8 @@
*
* @param interactionState the [InteractionState] for this floating action button
*/
- fun elevation(interactionState: InteractionState): Dp
+ @Composable
+ fun elevation(interactionState: InteractionState): State<Dp>
}
/**
@@ -226,12 +225,10 @@
// focused: Dp = 8.dp,
// hovered: Dp = 8.dp,
): FloatingActionButtonElevation {
- val clock = AmbientAnimationClock.current.asDisposableClock()
- return remember(defaultElevation, pressedElevation, clock) {
+ return remember(defaultElevation, pressedElevation) {
DefaultFloatingActionButtonElevation(
defaultElevation = defaultElevation,
- pressedElevation = pressedElevation,
- clock = clock
+ pressedElevation = pressedElevation
)
}
}
@@ -245,13 +242,9 @@
private class DefaultFloatingActionButtonElevation(
private val defaultElevation: Dp,
private val pressedElevation: Dp,
- private val clock: AnimationClockObservable
) : FloatingActionButtonElevation {
- private val lazyAnimatedElevation = LazyAnimatedValue<Dp, AnimationVector1D> { target ->
- AnimatedValueModel(initialValue = target, typeConverter = Dp.VectorConverter, clock = clock)
- }
-
- override fun elevation(interactionState: InteractionState): Dp {
+ @Composable
+ override fun elevation(interactionState: InteractionState): State<Dp> {
val interaction = interactionState.value.lastOrNull {
it is Interaction.Pressed
}
@@ -261,21 +254,21 @@
else -> defaultElevation
}
- val animatedElevation = lazyAnimatedElevation.animatedValueForTarget(target)
+ val animatable = remember { Animatable(target, Dp.VectorConverter) }
- if (animatedElevation.targetValue != target) {
- val lastInteraction = when (animatedElevation.targetValue) {
+ LaunchedEffect(target) {
+ val lastInteraction = when (animatable.targetValue) {
pressedElevation -> Interaction.Pressed
else -> null
}
- animatedElevation.animateElevation(
+ animatable.animateElevation(
from = lastInteraction,
to = interaction,
target = target
)
}
- return animatedElevation.value
+ return animatable.asState()
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
index e21c901..aeb0954 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
@@ -31,6 +31,8 @@
import androidx.compose.ui.graphics.toolingGraphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
/**
@@ -38,6 +40,10 @@
* clickable icon, see [IconButton].
*
* @param imageVector [ImageVector] to draw inside this Icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier optional [Modifier] for this Icon
* @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no
* tint is applied
@@ -45,11 +51,13 @@
@Composable
fun Icon(
imageVector: ImageVector,
+ contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current.copy(alpha = AmbientContentAlpha.current)
) {
Icon(
painter = rememberVectorPainter(imageVector),
+ contentDescription = contentDescription,
modifier = modifier,
tint = tint
)
@@ -60,6 +68,10 @@
* clickable icon, see [IconButton].
*
* @param bitmap [ImageBitmap] to draw inside this Icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier optional [Modifier] for this Icon
* @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no
* tint is applied
@@ -67,12 +79,14 @@
@Composable
fun Icon(
bitmap: ImageBitmap,
+ contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current
) {
val painter = remember(bitmap) { ImagePainter(bitmap) }
Icon(
painter = painter,
+ contentDescription = contentDescription,
modifier = modifier,
tint = tint
)
@@ -83,6 +97,10 @@
* clickable icon, see [IconButton].
*
* @param painter [Painter] to draw inside this Icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
* @param modifier optional [Modifier] for this Icon
* @param tint tint to be applied to [painter]. If [Color.Unspecified] is provided, then no
* tint is applied
@@ -90,6 +108,7 @@
@Composable
fun Icon(
painter: Painter,
+ contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current.copy(alpha = AmbientContentAlpha.current)
) {
@@ -97,9 +116,15 @@
// size that this icon will be forced to take up.
// TODO: b/149735981 semantics for content description
val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
+ val semantics = if (contentDescription != null) {
+ Modifier.semantics { this.contentDescription = contentDescription }
+ } else {
+ Modifier
+ }
Box(
modifier.toolingGraphicsLayer().defaultSizeFor(painter)
.paint(painter, colorFilter = colorFilter)
+ .then(semantics)
)
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/LazyAnimatedValue.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/LazyAnimatedValue.kt
deleted file mode 100644
index 4c17027..0000000
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/LazyAnimatedValue.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material
-
-import androidx.compose.animation.core.AnimatedValue
-import androidx.compose.animation.core.AnimationVector
-
-// TODO: b/171041025 replace if/when similar functionality is added to the AnimatedValue APIs
-/**
- * A lazy wrapper around [AnimatedValue] that delays creating the [AnimatedValue] until the
- * initial value / target is known. This is similar to [androidx.compose.animation.animate], but
- * can be used outside of a Composable function.
- *
- * @property factory lazily invoked factory to create an [AnimatedValue] for the given target
- */
-internal class LazyAnimatedValue<T, V : AnimationVector>(
- private val factory: (target: T) -> AnimatedValue<T, V>
-) {
- private var animatedValue: AnimatedValue<T, V>? = null
-
- /**
- * @return a new [AnimatedValue] with an initial value equal to [targetValue], or the
- * existing [AnimatedValue] if it has already been created.
- */
- fun animatedValueForTarget(targetValue: T): AnimatedValue<T, V> {
- return animatedValue ?: factory(targetValue).also { animatedValue = it }
- }
-}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 06df31a..2f919c026 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -19,7 +19,9 @@
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.defaultMinSizeConstraints
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -70,8 +72,8 @@
* be neither editable nor focusable, the input of the text field will not be selectable,
* visually text field will appear in the disabled UI state
* @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
- * fields will not be editable but otherwise operable. Read-only text fields are usually used to
- * display the pre-filled text that user cannot edit
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle the style to be applied to the input text. The default [textStyle] uses the
* [AmbientTextStyle] defined by the theme
* @param label the optional label to be displayed inside the text field container. The default
@@ -195,8 +197,8 @@
* be neither editable nor focusable, the input of the text field will not be selectable,
* visually text field will appear in the disabled UI state
* @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
- * fields will not be editable but otherwise operable. Read-only text fields are usually used to
- * display the pre-filled text that user cannot edit
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle the style to be applied to the input text. The default [textStyle] uses the
* [AmbientTextStyle] defined by the theme
* @param label the optional label to be displayed inside the text field container. The default
@@ -295,18 +297,29 @@
@Composable
internal fun OutlinedTextFieldLayout(
- modifier: Modifier = Modifier,
- decoratedTextField: @Composable (Modifier) -> Unit,
+ modifier: Modifier,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ enabled: Boolean,
+ readOnly: Boolean,
+ keyboardOptions: KeyboardOptions,
+ textStyle: TextStyle,
+ singleLine: Boolean,
+ maxLines: Int = Int.MAX_VALUE,
+ onImeActionPerformed: (ImeAction) -> Unit = {},
+ visualTransformation: VisualTransformation,
+ onTextInputStarted: (SoftwareKeyboardController) -> Unit,
+ interactionState: InteractionState,
decoratedPlaceholder: @Composable ((Modifier) -> Unit)?,
decoratedLabel: @Composable (() -> Unit)?,
leading: @Composable (() -> Unit)?,
trailing: @Composable (() -> Unit)?,
- singleLine: Boolean,
leadingColor: Color,
trailingColor: Color,
labelProgress: Float,
indicatorWidth: Dp,
- indicatorColor: Color
+ indicatorColor: Color,
+ cursorColor: Color
) {
val outlinedBorderParams = remember {
OutlinedBorderParams(
@@ -321,24 +334,47 @@
outlinedBorderParams.borderWidth.value = indicatorWidth
}
- // places leading icon, input field, label, placeholder, trailing icon
- IconsWithTextFieldLayout(
- modifier = modifier.drawOutlinedBorder(outlinedBorderParams),
- textField = decoratedTextField,
- leading = leading,
- trailing = trailing,
+ BasicTextField(
+ value = value,
+ modifier = modifier
+ .defaultMinSizeConstraints(
+ minWidth = TextFieldMinWidth,
+ minHeight = TextFieldMinHeight + OutlinedTextFieldTopPadding,
+ )
+ .padding(top = OutlinedTextFieldTopPadding)
+ .drawOutlinedBorder(outlinedBorderParams),
+ >
+ enabled = enabled,
+ readOnly = readOnly,
+ textStyle = textStyle,
+ cursorColor = cursorColor,
+ visualTransformation = visualTransformation,
+ keyboardOptions = keyboardOptions,
+ interactionState = interactionState,
+ >
+ >
singleLine = singleLine,
- leadingColor = leadingColor,
- trailingColor = trailingColor,
- >
- val labelWidth = it * labelProgress
- if (outlinedBorderParams.labelWidth.value != labelWidth) {
- outlinedBorderParams.labelWidth.value = labelWidth
- }
- },
- animationProgress = labelProgress,
- placeholder = decoratedPlaceholder,
- label = decoratedLabel
+ maxLines = maxLines,
+ decorationBox = @Composable { coreTextField ->
+ // places leading icon, input field, label, placeholder, trailing icon
+ IconsWithTextFieldLayout(
+ textField = coreTextField,
+ leading = leading,
+ trailing = trailing,
+ singleLine = singleLine,
+ leadingColor = leadingColor,
+ trailingColor = trailingColor,
+ >
+ val labelWidth = it * labelProgress
+ if (outlinedBorderParams.labelWidth.value != labelWidth) {
+ outlinedBorderParams.labelWidth.value = labelWidth
+ }
+ },
+ animationProgress = labelProgress,
+ placeholder = decoratedPlaceholder,
+ label = decoratedLabel
+ )
+ }
)
}
@@ -350,8 +386,7 @@
\ */
@Composable
private fun IconsWithTextFieldLayout(
- modifier: Modifier = Modifier,
- textField: @Composable (Modifier) -> Unit,
+ textField: @Composable () -> Unit,
placeholder: @Composable ((Modifier) -> Unit)?,
label: @Composable (() -> Unit)?,
leading: @Composable (() -> Unit)?,
@@ -384,17 +419,14 @@
placeholder(Modifier.layoutId(PlaceholderId).padding(horizontal = TextFieldPadding))
}
- textField(
- Modifier
- .layoutId(TextFieldId)
- .padding(horizontal = TextFieldPadding)
- )
+ Box(Modifier.layoutId(TextFieldId).padding(horizontal = TextFieldPadding)) {
+ textField()
+ }
if (label != null) {
Box(modifier = Modifier.layoutId(LabelId)) { label() }
}
- },
- modifier = modifier
+ }
) { measurables, incomingConstraints ->
// used to calculate the constraints for measuring elements that will be placed in a row
var occupiedSpaceHorizontally = 0
@@ -682,4 +714,10 @@
// TODO(b/158077409) support shape in OutlinedTextField
private val OutlinedTextFieldCornerRadius = 4.dp
-private val OutlinedTextFieldInnerPadding = 4.dp
\ No newline at end of file
+private val OutlinedTextFieldInnerPadding = 4.dp
+/*
+This padding is used to allow label not overlap with the content above it. This 8.dp will work
+for default cases when developers do not override the label's font size. If they do, they will
+need to add additional padding themselves
+*/
+private val OutlinedTextFieldTopPadding = 8.dp
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
index a8855a7..97bc87e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
@@ -16,12 +16,8 @@
package androidx.compose.material
-import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
-import androidx.compose.animation.animate
-import androidx.compose.animation.asDisposableClock
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.animateAsState
+import androidx.compose.animation.core.animateAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Interaction
@@ -35,14 +31,16 @@
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -79,11 +77,11 @@
interactionState: InteractionState = remember { InteractionState() },
colors: RadioButtonColors = RadioButtonDefaults.colors()
) {
- @Suppress("Deprecation") // b/176192329
- val dotRadius = animate(
- target = if (selected) RadioButtonDotSize / 2 else 0.dp,
- animSpec = tween(durationMillis = RadioAnimationDuration)
+ val dotRadius by animateAsState(
+ targetValue = if (selected) RadioButtonDotSize / 2 else 0.dp,
+ animationSpec = tween(durationMillis = RadioAnimationDuration)
)
+ val radioColor by colors.radioColor(enabled, selected)
Canvas(
modifier
.selectable(
@@ -101,7 +99,6 @@
.padding(RadioButtonPadding)
.size(RadioButtonSize)
) {
- val radioColor = colors.radioColor(enabled, selected)
drawRadio(radioColor, dotRadius)
}
}
@@ -122,7 +119,8 @@
* @param enabled whether the [RadioButton] is enabled
* @param selected whether the [RadioButton] is selected
*/
- fun radioColor(enabled: Boolean, selected: Boolean): Color
+ @Composable
+ fun radioColor(enabled: Boolean, selected: Boolean): State<Color>
}
/**
@@ -145,14 +143,12 @@
unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
): RadioButtonColors {
- val clock = AmbientAnimationClock.current.asDisposableClock()
return remember(
selectedColor,
unselectedColor,
- disabledColor,
- clock
+ disabledColor
) {
- DefaultRadioButtonColors(selectedColor, unselectedColor, disabledColor, clock)
+ DefaultRadioButtonColors(selectedColor, unselectedColor, disabledColor)
}
}
}
@@ -173,14 +169,10 @@
private class DefaultRadioButtonColors(
private val selectedColor: Color,
private val unselectedColor: Color,
- private val disabledColor: Color,
- private val clock: AnimationClockObservable
+ private val disabledColor: Color
) : RadioButtonColors {
- private val lazyAnimatedColor = LazyAnimatedValue<Color, AnimationVector4D> { target ->
- AnimatedValueModel(target, (Color.VectorConverter)(target.colorSpace), clock)
- }
-
- override fun radioColor(enabled: Boolean, selected: Boolean): Color {
+ @Composable
+ override fun radioColor(enabled: Boolean, selected: Boolean): State<Color> {
val target = when {
!enabled -> disabledColor
!selected -> unselectedColor
@@ -190,14 +182,9 @@
// If not enabled 'snap' to the disabled state, as there should be no animations between
// enabled / disabled.
return if (enabled) {
- val animatedColor = lazyAnimatedColor.animatedValueForTarget(target)
-
- if (animatedColor.targetValue != target) {
- animatedColor.animateTo(target, tween(durationMillis = RadioAnimationDuration))
- }
- animatedColor.value
+ animateAsState(target, tween(durationMillis = RadioAnimationDuration))
} else {
- target
+ rememberUpdatedState(target)
}
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 00f6b24..8d9a5a9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -47,7 +47,6 @@
* inside of the [Scaffold]
*/
@Stable
-@OptIn(ExperimentalMaterialApi::class)
class ScaffoldState(
val drawerState: DrawerState,
val snackbarHostState: SnackbarHostState
@@ -61,7 +60,6 @@
* inside of the [Scaffold]
*/
@Composable
-@OptIn(ExperimentalMaterialApi::class)
fun rememberScaffoldState(
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
@@ -147,7 +145,6 @@
* the scroller itself.
*/
@Composable
-@OptIn(ExperimentalMaterialApi::class)
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 256a30b..210ae2a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -141,7 +141,6 @@
* of the shadow below the SnackBar
*/
@Composable
-@OptIn(ExperimentalMaterialApi::class)
fun Snackbar(
snackbarData: SnackbarData,
modifier: Modifier = Modifier,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SnackbarHost.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SnackbarHost.kt
index 63e9548..5bb7010 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SnackbarHost.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SnackbarHost.kt
@@ -53,7 +53,6 @@
* automatically, but can be decoupled from it and live separately when desired.
*/
@Stable
-@ExperimentalMaterialApi
class SnackbarHostState {
/**
@@ -107,7 +106,6 @@
}
@Stable
- @OptIn(ExperimentalMaterialApi::class)
private class SnackbarDataImpl(
override val message: String,
override val actionLabel: String?,
@@ -146,7 +144,6 @@
* appearance based on the [SnackbarData] provided as a param
*/
@Composable
-@ExperimentalMaterialApi
fun SnackbarHost(
hostState: SnackbarHostState,
modifier: Modifier = Modifier,
@@ -173,7 +170,6 @@
* @property actionLabel optional action label to show as button in the Snackbar
* @property duration duration of the snackbar
*/
-@ExperimentalMaterialApi
interface SnackbarData {
val message: String
val actionLabel: String?
@@ -235,7 +231,6 @@
// TODO: to be replaced with the public customizable implementation
// it's basically tweaked nullable version of Crossfade
@Composable
-@OptIn(ExperimentalMaterialApi::class)
private fun FadeInFadeOutWithScale(
current: SnackbarData?,
modifier: Modifier = Modifier,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
index d0d8f9c..ec65b93 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
@@ -36,7 +36,9 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.emptyContent
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -134,7 +136,8 @@
* @param enabled whether the [Switch] is enabled or not
* @param checked whether the [Switch] is checked or not
*/
- fun thumbColor(enabled: Boolean, checked: Boolean): Color
+ @Composable
+ fun thumbColor(enabled: Boolean, checked: Boolean): State<Color>
/**
* Represents the color used for the switch's track, depending on [enabled] and [checked].
@@ -142,7 +145,8 @@
* @param enabled whether the [Switch] is enabled or not
* @param checked whether the [Switch] is checked or not
*/
- fun trackColor(enabled: Boolean, checked: Boolean): Color
+ @Composable
+ fun trackColor(enabled: Boolean, checked: Boolean): State<Color>
}
@OptIn(ExperimentalMaterialApi::class)
@@ -161,11 +165,11 @@
} else {
ThumbDefaultElevation
}
+ val trackColor by colors.trackColor(enabled, checked)
Canvas(Modifier.align(Alignment.Center).fillMaxSize()) {
- val trackColor = colors.trackColor(enabled, checked)
drawTrack(trackColor, TrackWidth.toPx(), TrackStrokeWidth.toPx())
}
- val thumbColor = colors.thumbColor(enabled, checked)
+ val thumbColor by colors.thumbColor(enabled, checked)
Surface(
shape = CircleShape,
color = thumbColor,
@@ -278,20 +282,26 @@
private val disabledUncheckedThumbColor: Color,
private val disabledUncheckedTrackColor: Color
) : SwitchColors {
- override fun thumbColor(enabled: Boolean, checked: Boolean): Color {
- return if (enabled) {
- if (checked) checkedThumbColor else uncheckedThumbColor
- } else {
- if (checked) disabledCheckedThumbColor else disabledUncheckedThumbColor
- }
+ @Composable
+ override fun thumbColor(enabled: Boolean, checked: Boolean): State<Color> {
+ return rememberUpdatedState(
+ if (enabled) {
+ if (checked) checkedThumbColor else uncheckedThumbColor
+ } else {
+ if (checked) disabledCheckedThumbColor else disabledUncheckedThumbColor
+ }
+ )
}
- override fun trackColor(enabled: Boolean, checked: Boolean): Color {
- return if (enabled) {
- if (checked) checkedTrackColor else uncheckedTrackColor
- } else {
- if (checked) disabledCheckedTrackColor else disabledUncheckedTrackColor
- }
+ @Composable
+ override fun trackColor(enabled: Boolean, checked: Boolean): State<Color> {
+ return rememberUpdatedState(
+ if (enabled) {
+ if (checked) checkedTrackColor else uncheckedTrackColor
+ } else {
+ if (checked) disabledCheckedTrackColor else disabledUncheckedTrackColor
+ }
+ )
}
override fun equals(other: Any?): Boolean {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index dc4b069..6f2106a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -20,8 +20,10 @@
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.defaultMinSizeConstraints
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.ZeroCornerSize
+import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -96,9 +98,9 @@
* @param enabled controls the enabled state of the [TextField]. When `false`, the text field will
* be neither editable nor focusable, the input of the text field will not be selectable,
* visually text field will appear in the disabled UI state
- * @param readOnly controls the editable state of the [TextField]. When `true`, the text fields
- * will not be editable but otherwise operable. Read-only text fields are usually used to display
- * the pre-filled text that user cannot edit
+ * @param readOnly controls the editable state of the [TextField]. When `true`, the text
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle the style to be applied to the input text. The default [textStyle] uses the
* [AmbientTextStyle] defined by the theme
* @param label the optional label to be displayed inside the text field container. The default
@@ -228,9 +230,9 @@
* @param enabled controls the enabled state of the [TextField]. When `false`, the text field will
* be neither editable nor focusable, the input of the text field will not be selectable,
* visually text field will appear in the disabled UI state
- * @param readOnly controls the editable state of the [TextField]. When `true`, the text fields
- * will not be editable but otherwise operable. Read-only text fields are usually used to display
- * the pre-filled text that user cannot edit
+ * @param readOnly controls the editable state of the [TextField]. When `true`, the text
+ * field can not be modified, however, a user can focus it and copy text from it. Read-only text
+ * fields are usually used to display pre-filled forms that user can not edit
* @param textStyle the style to be applied to the input text. The default [textStyle] uses the
* [AmbientTextStyle] defined by the theme
* @param label the optional label to be displayed inside the text field container. The default
@@ -334,41 +336,67 @@
@Composable
internal fun TextFieldLayout(
- modifier: Modifier = Modifier,
- decoratedTextField: @Composable (Modifier) -> Unit,
+ modifier: Modifier,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ enabled: Boolean,
+ readOnly: Boolean,
+ keyboardOptions: KeyboardOptions,
+ textStyle: TextStyle,
+ singleLine: Boolean,
+ maxLines: Int = Int.MAX_VALUE,
+ onImeActionPerformed: (ImeAction) -> Unit = {},
+ visualTransformation: VisualTransformation,
+ onTextInputStarted: (SoftwareKeyboardController) -> Unit,
+ interactionState: InteractionState,
decoratedPlaceholder: @Composable ((Modifier) -> Unit)?,
decoratedLabel: @Composable (() -> Unit)?,
leading: @Composable (() -> Unit)?,
trailing: @Composable (() -> Unit)?,
- singleLine: Boolean,
leadingColor: Color,
trailingColor: Color,
labelProgress: Float,
indicatorWidth: Dp,
indicatorColor: Color,
backgroundColor: Color,
+ cursorColor: Color,
shape: Shape
) {
- // places leading icon, text field with label and placeholder, trailing icon
- IconsWithTextFieldLayout(
+ BasicTextField(
+ value = value,
modifier = modifier
- .background(
- color = backgroundColor,
- shape = shape
+ .defaultMinSizeConstraints(
+ minWidth = TextFieldMinWidth,
+ minHeight = TextFieldMinHeight
)
- .drawIndicatorLine(
- lineWidth = indicatorWidth,
- color = indicatorColor
- ),
- textField = decoratedTextField,
- placeholder = decoratedPlaceholder,
- label = decoratedLabel,
- leading = leading,
- trailing = trailing,
+ .background(color = backgroundColor, shape = shape)
+ .drawIndicatorLine(lineWidth = indicatorWidth, color = indicatorColor),
+ >
+ enabled = enabled,
+ readOnly = readOnly,
+ textStyle = textStyle,
+ cursorColor = cursorColor,
+ visualTransformation = visualTransformation,
+ keyboardOptions = keyboardOptions,
+ interactionState = interactionState,
+ >
+ >
singleLine = singleLine,
- leadingColor = leadingColor,
- trailingColor = trailingColor,
- animationProgress = labelProgress
+ maxLines = maxLines,
+ decorationBox = @Composable { coreTextField ->
+ // places leading icon, text field with label and placeholder, trailing icon
+ IconsWithTextFieldLayout(
+ textField = coreTextField,
+ placeholder = decoratedPlaceholder,
+ label = decoratedLabel,
+ leading = leading,
+ trailing = trailing,
+ singleLine = singleLine,
+ leadingColor = leadingColor,
+ trailingColor = trailingColor,
+ animationProgress = labelProgress
+ )
+ }
)
}
@@ -378,8 +406,7 @@
*/
@Composable
private fun IconsWithTextFieldLayout(
- modifier: Modifier = Modifier,
- textField: @Composable (Modifier) -> Unit,
+ textField: @Composable () -> Unit,
label: @Composable (() -> Unit)?,
placeholder: @Composable ((Modifier) -> Unit)?,
leading: @Composable (() -> Unit)?,
@@ -421,9 +448,8 @@
)
) { label() }
}
- textField(Modifier.layoutId(TextFieldId).then(padding))
- },
- modifier = modifier
+ Box(Modifier.layoutId(TextFieldId).then(padding)) { textField() }
+ }
) { measurables, incomingConstraints ->
val topBottomPadding = TextFieldPadding.toIntPx()
val baseLineOffset = FirstBaselineOffset.toIntPx()
@@ -681,7 +707,7 @@
/**
* A draw modifier that draws a bottom indicator line in [TextField]
*/
-private fun Modifier.drawIndicatorLine(lineWidth: Dp, color: Color): Modifier {
+internal fun Modifier.drawIndicatorLine(lineWidth: Dp, color: Color): Modifier {
return drawBehind {
val strokeWidth = lineWidth.value * density
val y = size.height - strokeWidth / 2
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
index 76257df..4dc2446 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
@@ -26,12 +26,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.defaultMinSizeConstraints
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.preferredSizeIn
-import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
@@ -39,8 +34,6 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
@@ -52,6 +45,7 @@
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.SoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
@@ -72,7 +66,7 @@
* Implementation of the [TextField] and [OutlinedTextField]
*/
@Composable
-@OptIn(ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class, InternalTextApi::class)
internal fun TextFieldImpl(
type: TextFieldType,
enabled: Boolean,
@@ -115,55 +109,6 @@
else -> InputPhase.UnfocusedNotEmpty
}
- val decoratedTextField: @Composable (Modifier) -> Unit = @Composable { tagModifier ->
- Decoration(
- contentColor = inactiveColor,
- typography = MaterialTheme.typography.subtitle1,
- contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
- ) {
- BasicTextField(
- value = value,
- modifier = tagModifier.defaultMinSizeConstraints(minWidth = TextFieldMinWidth),
- textStyle = mergedTextStyle,
- enabled = enabled,
- readOnly = readOnly,
- >
- cursorColor = if (isErrorValue) errorColor else activeColor,
- visualTransformation = visualTransformation,
- keyboardOptions = keyboardOptions,
- maxLines = maxLines,
- interactionState = interactionState,
- >
- onImeActionPerformed(it, keyboardController.value)
- },
- >
- keyboardController.value = it
- onTextInputStarted(it)
- },
- singleLine = singleLine
- )
- }
- }
-
- val focusRequester = FocusRequester()
- val textFieldModifier = if (enabled) {
- modifier
- .focusRequester(focusRequester)
- .clickable(interactionState = interactionState, indication = null) {
- focusRequester.requestFocus()
- // TODO(b/163109449): Showing and hiding keyboard should be handled by BaseTextField.
- // The requestFocus() call here should be enough to trigger the software keyboard.
- // Investiate why this is needed here. If it is really needed, instead of doing
- // this in the onClick callback, we should move this logic to onFocusChanged
- // so that it can show or hide the keyboard based on the focus state.
- if (!readOnly) {
- keyboardController.value?.showSoftwareKeyboard()
- }
- }
- } else {
- modifier
- }
-
TextFieldTransitionScope.Transition(
inputState = inputState,
showLabel = label != null,
@@ -221,50 +166,69 @@
}
} else null
+ val cursorColor = if (isErrorValue) errorColor else activeColor
+ val onImeActionPerformedAction: (ImeAction) -> Unit = {
+ onImeActionPerformed(it, keyboardController.value)
+ }
+ val onTextInputStartedAction: (SoftwareKeyboardController) -> Unit = {
+ keyboardController.value = it
+ onTextInputStarted(it)
+ }
when (type) {
TextFieldType.Filled -> {
TextFieldLayout(
- modifier = Modifier
- .preferredSizeIn(
- minWidth = TextFieldMinWidth,
- minHeight = TextFieldMinHeight
- )
- .then(textFieldModifier),
- decoratedTextField = decoratedTextField,
+ modifier = modifier,
+ value = value,
+ >
+ enabled = enabled,
+ readOnly = readOnly,
+ keyboardOptions = keyboardOptions,
+ textStyle = mergedTextStyle,
+ singleLine = singleLine,
+ maxLines = maxLines,
+ >
+ visualTransformation = visualTransformation,
+ >
+ interactionState = interactionState,
decoratedPlaceholder = decoratedPlaceholder,
decoratedLabel = decoratedLabel,
leading = leading,
trailing = trailing,
- singleLine = singleLine,
leadingColor = leadingColor,
trailingColor = trailingColor,
labelProgress = labelProgress,
indicatorWidth = indicatorWidth,
indicatorColor = indicatorColor,
backgroundColor = backgroundColor,
+ cursorColor = cursorColor,
shape = shape
)
}
TextFieldType.Outlined -> {
OutlinedTextFieldLayout(
- modifier = Modifier
- .preferredSizeIn(
- minWidth = TextFieldMinWidth,
- minHeight = TextFieldMinHeight + OutlinedTextFieldTopPadding
- )
- .then(textFieldModifier)
- .padding(top = OutlinedTextFieldTopPadding),
- decoratedTextField = decoratedTextField,
+ modifier = modifier,
+ value = value,
+ >
+ enabled = enabled,
+ readOnly = readOnly,
+ keyboardOptions = keyboardOptions,
+ textStyle = mergedTextStyle,
+ singleLine = singleLine,
+ maxLines = maxLines,
+ >
+ visualTransformation = visualTransformation,
+ >
+ interactionState = interactionState,
decoratedPlaceholder = decoratedPlaceholder,
decoratedLabel = decoratedLabel,
leading = leading,
trailing = trailing,
- singleLine = singleLine,
leadingColor = leadingColor,
trailingColor = trailingColor,
labelProgress = labelProgress,
indicatorWidth = indicatorWidth,
- indicatorColor = indicatorColor
+ indicatorColor = indicatorColor,
+ cursorColor = cursorColor
)
}
}
@@ -461,17 +425,10 @@
private val IndicatorUnfocusedWidth = 1.dp
private val IndicatorFocusedWidth = 2.dp
private const val TrailingLeadingAlpha = 0.54f
-private val TextFieldMinHeight = 56.dp
-private val TextFieldMinWidth = 280.dp
+internal val TextFieldMinHeight = 56.dp
+internal val TextFieldMinWidth = 280.dp
internal val TextFieldPadding = 16.dp
internal val HorizontalIconPadding = 12.dp
// Filled text field uses 42% opacity to meet the contrast requirements for accessibility reasons
private const val IndicatorInactiveAlpha = 0.42f
-
-/*
-This padding is used to allow label not overlap with the content above it. This 8.dp will work
-for default cases when developers do not override the label's font size. If they do, they will
-need to add additional padding themselves
-*/
-private val OutlinedTextFieldTopPadding = 8.dp
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
index f32860a..c53a78e 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
@@ -147,8 +147,16 @@
/**
* Measures the time of the first composition of the given compose test case.
+ *
+ * @param assertNoPendingRecompositions whether the benchmark will fail if there are pending
+ * recompositions after the first composition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have an initial animation after being composed this can
+ * be turned off to benchmark just the first composition without any pending animations.
*/
-fun ComposeBenchmarkRule.benchmarkFirstCompose(caseFactory: () -> ComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstCompose(
+ caseFactory: () -> ComposeTestCase,
+ assertNoPendingRecompositions: Boolean = true
+) {
runBenchmarkFor(caseFactory) {
measureRepeated {
runWithTimingDisabled {
@@ -158,7 +166,9 @@
emitContent()
runWithTimingDisabled {
- assertNoPendingChanges()
+ if (assertNoPendingRecompositions) {
+ assertNoPendingChanges()
+ }
disposeContent()
}
}
@@ -311,9 +321,15 @@
/**
* Measures recomposition time of the hierarchy after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first recomposition without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkRecompose(
- caseFactory: () -> T
+ caseFactory: () -> T,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -323,17 +339,25 @@
getTestCase().toggleState()
}
recomposeAssertHadChanges()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
}
}
/**
* Measures measure time of the hierarchy after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first remeasure without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasure(
caseFactory: () -> T,
- toggleCausesRecompose: Boolean = true
+ toggleCausesRecompose: Boolean = true,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -345,20 +369,30 @@
recomposeAssertHadChanges()
}
requestLayout()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
measure()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
}
}
/**
* Measures layout time of the hierarchy after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkLayout(
caseFactory: () -> T,
- toggleCausesRecompose: Boolean = true
+ toggleCausesRecompose: Boolean = true,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -371,20 +405,30 @@
}
requestLayout()
measure()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
layout()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
}
}
/**
* Measures draw time of the hierarchy after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first redraw without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkDraw(
caseFactory: () -> T,
- toggleCausesRecompose: Boolean = true
+ toggleCausesRecompose: Boolean = true,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -395,7 +439,9 @@
if (toggleCausesRecompose) {
recomposeAssertHadChanges()
}
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
requestLayout()
measure()
layout()
@@ -472,9 +518,16 @@
/**
* Measures recompose, measure and layout time after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first recompose, remeasure and relayout without any pending
+ * animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
- caseFactory: () -> T
+ caseFactory: () -> T,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -482,7 +535,9 @@
measureRepeated {
getTestCase().toggleState()
recomposeAssertHadChanges()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
measure()
layout()
runWithTimingDisabled {
@@ -496,9 +551,15 @@
/**
* Measures measure and layout time after changing a state.
+ *
+ * @param assertOneRecomposition whether the benchmark will fail if there are pending
+ * recompositions after the first recomposition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have animations after being recomposed this can
+ * be turned off to benchmark just the first remeasure and relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasureLayout(
- caseFactory: () -> T
+ caseFactory: () -> T,
+ assertOneRecomposition: Boolean = true
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
doFramesUntilNoChangesPending()
@@ -506,10 +567,14 @@
measureRepeated {
runWithTimingDisabled {
getTestCase().toggleState()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
measure()
- assertNoPendingChanges()
+ if (assertOneRecomposition) {
+ assertNoPendingChanges()
+ }
}
}
}
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
new file mode 100644
index 0000000..9c4b9f5
--- /dev/null
+++ b/compose/ui/ui-inspection/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+ id("androidx.inspection")
+}
+
+dependencies {
+ implementation("androidx.annotation:annotation:1.1.0")
+ implementation(KOTLIN_STDLIB)
+ compileOnly(projectOrArtifact(":inspection:inspection"))
+ compileOnly(project(":compose:runtime:runtime"))
+ compileOnly(project(":compose:ui:ui"))
+ compileOnly(project(":compose:ui:ui-tooling"))
+}
+
+androidx {
+ name = "Android Compose Layout Inspector"
+ type = LibraryType.IDE_PLUGIN
+ mavenGroup = LibraryGroups.Compose.UI
+ inceptionYear = "2021"
+ description = "Compose layout inspector. Exposes information to our tools for better IDE support."
+}
+
+android {
+ defaultConfig {
+ // layout inspection supported starting on Android Q
+ minSdkVersion 29
+ }
+
+ sourceSets {
+ main.resources.srcDirs += "src/main/proto"
+ }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ useIR = true
+ freeCompilerArgs += [
+ "-Xopt-in=kotlin.RequiresOptIn",
+ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
+ ]
+ }
+}
diff --git a/compose/ui/ui-inspection/lint-baseline.xml b/compose/ui/ui-inspection/lint-baseline.xml
new file mode 100644
index 0000000..8f1aa4b
--- /dev/null
+++ b/compose/ui/ui-inspection/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+
+</issues>
diff --git a/compose/ui/ui-inspection/src/main/AndroidManifest.xml b/compose/ui/ui-inspection/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5f27d96
--- /dev/null
+++ b/compose/ui/ui-inspection/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="androidx.compose.ui.inspection" />
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
new file mode 100644
index 0000000..133e045
--- /dev/null
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.inspection
+
+import androidx.inspection.Connection
+import androidx.inspection.Inspector
+import androidx.inspection.InspectorEnvironment
+import androidx.inspection.InspectorFactory
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol
+
+private const val LAYOUT_INSPECTION_ID = "layoutinspector.compose.inspection"
+
+// created by java.util.ServiceLoader
+class ComposeLayoutInspectorFactory :
+ InspectorFactory<ComposeLayoutInspector>(LAYOUT_INSPECTION_ID) {
+ override fun createInspector(
+ connection: Connection,
+ environment: InspectorEnvironment
+ ): ComposeLayoutInspector {
+ return ComposeLayoutInspector(connection)
+ }
+}
+
+class ComposeLayoutInspector(
+ connection: Connection,
+) : Inspector(connection) {
+
+ override fun onReceiveCommand(data: ByteArray, callback: CommandCallback) {
+ // TODO: Actually reply with a real response
+ callback.reply(LayoutInspectorComposeProtocol.Response.getDefaultInstance().toByteArray())
+ }
+}
\ No newline at end of file
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
similarity index 60%
copy from navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
copy to compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
index f9f0fbb..c6df549 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
+++ b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,11 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+syntax = "proto3";
+package layoutinspector.compose.inspection;
+option java_package = "layoutinspector.compose.inspection";
+option java_outer_classname = "LayoutInspectorComposeProtocol";
-package androidx.navigation;
+// ======= MESSAGES =======
-/**
- * An interface marking generated Args classes.
- */
-public interface NavArgs {
+// ======= COMMANDS, RESPONSES, AND EVENTS =======
+
+message Command {
+}
+
+message Response {
}
diff --git a/compose/ui/ui-inspection/src/main/resources/META-INF/services/androidx.inspection.InspectorFactory b/compose/ui/ui-inspection/src/main/resources/META-INF/services/androidx.inspection.InspectorFactory
new file mode 100644
index 0000000..e13fa9c
--- /dev/null
+++ b/compose/ui/ui-inspection/src/main/resources/META-INF/services/androidx.inspection.InspectorFactory
@@ -0,0 +1 @@
+androidx.compose.ui.inspection.ComposeLayoutInspectorFactory
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index 04f1c78..614612d 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -12,7 +12,7 @@
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
@@ -26,7 +26,7 @@
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public androidx.compose.ui.unit.Density density;
- property public long displaySize;
+ property @Deprecated public long displaySize;
property public androidx.compose.ui.test.MainTestClock mainClock;
}
@@ -61,7 +61,7 @@
method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method @Deprecated public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
@@ -72,7 +72,7 @@
method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
property @Deprecated public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
- property public abstract long displaySize;
+ property @Deprecated public abstract long displaySize;
property public abstract androidx.compose.ui.test.MainTestClock mainClock;
}
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index 04f1c78..614612d 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -12,7 +12,7 @@
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
@@ -26,7 +26,7 @@
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public androidx.compose.ui.unit.Density density;
- property public long displaySize;
+ property @Deprecated public long displaySize;
property public androidx.compose.ui.test.MainTestClock mainClock;
}
@@ -61,7 +61,7 @@
method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method @Deprecated public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
@@ -72,7 +72,7 @@
method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
property @Deprecated public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
- property public abstract long displaySize;
+ property @Deprecated public abstract long displaySize;
property public abstract androidx.compose.ui.test.MainTestClock mainClock;
}
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index 04f1c78..614612d 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -12,7 +12,7 @@
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
@@ -26,7 +26,7 @@
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public androidx.compose.ui.unit.Density density;
- property public long displaySize;
+ property @Deprecated public long displaySize;
property public androidx.compose.ui.test.MainTestClock mainClock;
}
@@ -61,7 +61,7 @@
method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method @Deprecated public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
- method public long getDisplaySize-YbymL2g();
+ method @Deprecated public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.MainTestClock getMainClock();
method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
@@ -72,7 +72,7 @@
method public void waitUntil(optional long timeoutMillis, kotlin.jvm.functions.Function0<java.lang.Boolean> condition);
property @Deprecated public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
- property public abstract long displaySize;
+ property @Deprecated public abstract long displaySize;
property public abstract androidx.compose.ui.test.MainTestClock mainClock;
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index e860083..6ccf5cc 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -24,7 +24,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Recomposer
import androidx.compose.ui.InternalComposeUiApi
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.platform.WindowRecomposerFactory
import androidx.compose.ui.platform.WindowRecomposerPolicy
@@ -318,6 +318,10 @@
Density(ApplicationProvider.getApplicationContext())
}
+ @Deprecated(
+ "This utility was deprecated without replacement. It is recommend to use " +
+ "the root size for any assertions."
+ )
override val displaySize by lazy {
ApplicationProvider.getApplicationContext<Context>().resources.displayMetrics.let {
IntSize(it.widthPixels, it.heightPixels)
@@ -526,7 +530,7 @@
@SuppressLint("DocumentExceptions")
override fun sendTextInputCommand(node: SemanticsNode, command: List<EditCommand>) {
- val owner = node.owner as ViewRootForTest
+ val owner = node.root as ViewRootForTest
@Suppress("DEPRECATION")
runOnUiThread {
@@ -539,7 +543,7 @@
@SuppressLint("DocumentExceptions")
override fun sendImeAction(node: SemanticsNode, actionSpecified: ImeAction) {
- val owner = node.owner as ViewRootForTest
+ val owner = node.root as ViewRootForTest
@Suppress("DEPRECATION")
runOnUiThread {
@@ -555,7 +559,7 @@
return androidx.compose.ui.test.junit4.runOnUiThread(action)
}
- override fun getOwners(): Set<Owner> {
+ override fun getRoots(): Set<RootForTest> {
// TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
// structure. In case of multiple AndroidOwners, add a fake root
waitForIdle()
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index 6d89edb5..c4fe79d 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -20,7 +20,7 @@
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.DesktopOwner
import androidx.compose.ui.platform.DesktopOwners
import androidx.compose.ui.platform.setContent
@@ -69,7 +69,12 @@
override val mainClock: MainTestClock
get() = TODO()
- override val displaySize: IntSize get() = IntSize(1024, 768)
+ @Deprecated(
+ "This utility was deprecated without replacement. It is recommend to use " +
+ "the root size for any assertions."
+ )
+ override val displaySize: IntSize get() = testDisplaySize
+ internal val testDisplaySize: IntSize get() = IntSize(1024, 768)
val executionQueue = LinkedList<() -> Unit>()
@@ -167,14 +172,14 @@
}
private fun performSetContent(composable: @Composable() () -> Unit) {
- val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)!!
+ val surface = Surface.makeRasterN32Premul(testDisplaySize.width, testDisplaySize.height)!!
val canvas = surface.canvas
val owners = DesktopOwners(invalidate = {}).also {
owners = it
}
val owner = DesktopOwner(owners)
owner.setContent(content = composable)
- owner.setSize(displaySize.width, displaySize.height)
+ owner.setSize(testDisplaySize.width, testDisplaySize.height)
owner.measureAndLayout()
owner.draw(canvas)
this.owner = owner
@@ -207,7 +212,7 @@
return rule.runOnUiThread(action)
}
- override fun getOwners(): Set<Owner> {
+ override fun getRoots(): Set<RootForTest> {
return rule.owners!!.list
}
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
index 51977f1..bd2f38f 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
@@ -43,6 +43,10 @@
/**
* Current device display's size.
*/
+ @Deprecated(
+ "This utility was deprecated without replacement. It is recommend to use " +
+ "the root size for any assertions."
+ )
val displaySize: IntSize get
/**
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 6641d7a..28ad28a 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -316,7 +316,7 @@
@androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public void advanceTimeBy(long millis);
- method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
+ method public java.util.Set<androidx.compose.ui.node.RootForTest> getRoots();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
method public void sendTextInputCommand(androidx.compose.ui.semantics.SemanticsNode node, java.util.List<? extends androidx.compose.ui.text.input.EditCommand> command);
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 6641d7a..28ad28a 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -316,7 +316,7 @@
@androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public void advanceTimeBy(long millis);
- method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
+ method public java.util.Set<androidx.compose.ui.node.RootForTest> getRoots();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
method public void sendTextInputCommand(androidx.compose.ui.semantics.SemanticsNode node, java.util.List<? extends androidx.compose.ui.text.input.EditCommand> command);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 6641d7a..28ad28a 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -316,7 +316,7 @@
@androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public void advanceTimeBy(long millis);
- method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
+ method public java.util.Set<androidx.compose.ui.node.RootForTest> getRoots();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
method public void sendTextInputCommand(androidx.compose.ui.semantics.SemanticsNode node, java.util.List<? extends androidx.compose.ui.text.input.EditCommand> command);
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
index 47c42c1..f96f634 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
@@ -37,7 +37,7 @@
return false
}
- (node.owner as? ViewRootForTest)?.let {
+ (node.root as? ViewRootForTest)?.let {
if (!ViewMatchers.isDisplayed().matches(it.view)) {
return false
}
@@ -53,7 +53,7 @@
}
internal actual fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
- val composeView = (owner as ViewRootForTest).view
+ val composeView = (root as ViewRootForTest).view
val rootLocationInWindow = intArrayOf(0, 0).let {
composeView.getLocationInWindow(it)
Offset(it[0].toFloat(), it[1].toFloat())
@@ -62,7 +62,7 @@
}
internal actual fun SemanticsNode.isInScreenBounds(): Boolean {
- val composeView = (owner as ViewRootForTest).view
+ val composeView = (root as ViewRootForTest).view
// Window relative bounds of our node
val nodeBoundsInWindow = clippedNodeBoundsInWindow()
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
index e9de46c..7c82733 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
@@ -53,7 +53,7 @@
)
}
- val view = (node.owner as ViewRootForTest).view
+ val view = (node.root as ViewRootForTest).view
// If we are in dialog use its window to capture the bitmap
val dialogParentNodeMaybe = node.findClosestParentNode(includeSelf = true) {
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
index 578cf62..0dde1da 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
@@ -27,7 +27,7 @@
import android.view.MotionEvent.ACTION_UP
import androidx.compose.runtime.dispatch.AndroidUiDispatcher
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.ViewRootForTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
@@ -35,20 +35,23 @@
import kotlinx.coroutines.withContext
import kotlin.math.max
-internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
- require(owner is ViewRootForTest) {
+internal actual fun createInputDispatcher(
+ testContext: TestContext,
+ root: RootForTest
+): InputDispatcher {
+ require(root is ViewRootForTest) {
"InputDispatcher currently only supports dispatching to ViewRootForTest, not to " +
- owner::class.java.simpleName
+ root::class.java.simpleName
}
- val view = owner.view
- return AndroidInputDispatcher(testContext, owner) { view.dispatchTouchEvent(it) }
+ val view = root.view
+ return AndroidInputDispatcher(testContext, root) { view.dispatchTouchEvent(it) }
}
internal class AndroidInputDispatcher(
private val testContext: TestContext,
- composeRoot: ViewRootForTest?,
+ root: ViewRootForTest?,
private val sendEvent: (MotionEvent) -> Unit
-) : InputDispatcher(testContext, composeRoot) {
+) : InputDispatcher(testContext, root) {
private val batchLock = Any()
// Batched events are generated just-in-time, given the "lateness" of the dispatching (see
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
index 6b35b6d..621627c 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
@@ -101,12 +101,9 @@
"Can't query SemanticsNode, (Partial)GestureScope has already been disposed"
}
- // Convenience property
- private val owner get() = semanticsNode.owner
-
// TODO(b/133217292): Better error: explain which gesture couldn't be performed
private var _inputDispatcher: InputDispatcher? =
- createInputDispatcher(testContext, checkNotNull(owner))
+ createInputDispatcher(testContext, checkNotNull(semanticsNode.root))
internal val inputDispatcher
get() = checkNotNull(_inputDispatcher) {
"Can't send gesture, (Partial)GestureScope has already been disposed"
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
index 8da027d..c3b6dde 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
@@ -17,11 +17,14 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import kotlin.math.max
import kotlin.math.roundToInt
-internal expect fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
+internal expect fun createInputDispatcher(
+ testContext: TestContext,
+ root: RootForTest
+): InputDispatcher
/**
* Dispatcher to inject full and partial gestures. An [InputDispatcher] is created at the
@@ -52,7 +55,7 @@
*/
internal abstract class InputDispatcher(
private val testContext: TestContext,
- private val owner: Owner?
+ private val root: RootForTest?
) {
companion object {
/**
@@ -122,7 +125,7 @@
protected abstract val now: Long
init {
- val state = testContext.states.remove(owner)
+ val state = testContext.states.remove(root)
if (state?.partialGesture != null) {
nextDownTime = state.nextDownTime
gestureLateness = state.gestureLateness
@@ -130,9 +133,9 @@
}
}
- protected open fun saveState(owner: Owner?) {
- if (owner != null) {
- testContext.states[owner] =
+ protected open fun saveState(root: RootForTest?) {
+ if (root != null) {
+ testContext.states[root] =
InputDispatcherState(
nextDownTime,
gestureLateness,
@@ -567,7 +570,7 @@
* Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
*/
fun dispose() {
- saveState(owner)
+ saveState(root)
onDispose()
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index 2e0e7e3..b28a86b 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
@@ -26,8 +26,8 @@
*/
fun SemanticsNodeInteraction.performKeyPress(keyEvent: KeyEvent): Boolean {
val semanticsNode = fetchSemanticsNode("Failed to send key Event (${keyEvent.key})")
- val owner = semanticsNode.owner
- requireNotNull(owner) { "Failed to find owner" }
+ val root = semanticsNode.root
+ requireNotNull(root) { "Failed to find owner" }
@OptIn(InternalTestApi::class)
- return testContext.testOwner.runOnUiThread { owner.sendKeyEvent(keyEvent) }
+ return testContext.testOwner.runOnUiThread { root.sendKeyEvent(keyEvent) }
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index 74a2c6d..06abef4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
@@ -248,7 +248,7 @@
operation: Density.(SemanticsNode) -> R
): R {
val node = fetchSemanticsNode("Failed to retrieve density for the node.")
- val density = node.owner!!.density
+ val density = node.root!!.density
return operation.invoke(density, node)
}
@@ -256,7 +256,7 @@
assertion: Density.(Rect) -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
- val density = node.owner!!.density
+ val density = node.root!!.density
assertion.invoke(density, node.unclippedBoundsInRoot)
return this
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index 5f0afda..d44f775 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -16,7 +16,7 @@
package androidx.compose.ui.test
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.getAllSemanticsNodes
import androidx.compose.ui.text.input.EditCommand
@@ -48,14 +48,14 @@
fun <T> runOnUiThread(action: () -> T): T
/**
- * Collects all [Owner]s from all compose hierarchies.
+ * Collects all [RootForTest]s from all compose hierarchies.
*
* This is a blocking call. Returns only after compose is idle.
*
* Can crash in case it hits time out. This is not supposed to be handled as it
* surfaces only in incorrect tests.
*/
- fun getOwners(): Set<Owner>
+ fun getRoots(): Set<RootForTest>
/**
* Advances time if and only if this [TestOwner] uses a [MainTestClock]
@@ -74,7 +74,7 @@
*/
@OptIn(InternalTestApi::class)
internal fun TestOwner.getAllSemanticsNodes(useUnmergedTree: Boolean): List<SemanticsNode> {
- return getOwners().flatMap { it.semanticsOwner.getAllSemanticsNodes(useUnmergedTree) }
+ return getRoots().flatMap { it.semanticsOwner.getAllSemanticsNodes(useUnmergedTree) }
}
@InternalTestApi
@@ -86,10 +86,10 @@
class TestContext internal constructor(internal val testOwner: TestOwner) {
/**
- * Stores the [InputDispatcherState] of each [Owner]. The state will be restored in an
+ * Stores the [InputDispatcherState] of each [RootForTest]. The state will be restored in an
* [InputDispatcher] when it is created for an owner that has a state stored.
*/
- internal val states = mutableMapOf<Owner, InputDispatcherState>()
+ internal val states = mutableMapOf<RootForTest, InputDispatcherState>()
internal fun getAllSemanticsNodes(mergingEnabled: Boolean) =
testOwner.getAllSemanticsNodes(mergingEnabled)
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
index 2d1f927..4aaddd3 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
@@ -18,17 +18,20 @@
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.TestPointerInputEventData
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.DesktopOwner
-internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
- return DesktopInputDispatcher(testContext, owner as DesktopOwner)
+internal actual fun createInputDispatcher(
+ testContext: TestContext,
+ root: RootForTest
+): InputDispatcher {
+ return DesktopInputDispatcher(testContext, root as DesktopOwner)
}
internal class DesktopInputDispatcher(
testContext: TestContext,
- val owner: DesktopOwner
-) : InputDispatcher(testContext, owner) {
+ val root: DesktopOwner
+) : InputDispatcher(testContext, root) {
companion object {
var gesturePointerId = 0L
}
@@ -86,7 +89,7 @@
Thread.sleep(delayMs)
}
}
- owner.processPointerInput(eventTime, it)
+ root.processPointerInput(eventTime, it)
}
}
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/TestAnimationPreview.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/TestAnimationPreview.kt
index 232a23f..17e63cf 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/TestAnimationPreview.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/TestAnimationPreview.kt
@@ -71,6 +71,6 @@
shape = MaterialTheme.shapes.large.copy(topLeft = CornerSize(state[CheckBoxCorner])),
modifier = Modifier.toggleable(value = selected, >
) {
- Icon(imageVector = Icons.Filled.Done)
+ Icon(imageVector = Icons.Filled.Done, contentDescription = null)
}
}
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 2c25f26..eeb8a85 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -93,7 +93,7 @@
Inspectable(slotTableRecord) {
Column {
Text(text = "Hello World", color = Color.Green)
- Icon(Icons.Filled.FavoriteBorder)
+ Icon(Icons.Filled.FavoriteBorder, null)
Surface {
Button( { Text(text = "OK") }
}
@@ -309,7 +309,7 @@
Column {
Text(text = "Hello World", color = Color.Green)
Spacer(Modifier.preferredHeight(16.dp))
- Image(Icons.Filled.Call)
+ Image(Icons.Filled.Call, null)
}
}
}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 90634da..863dbec 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2086,64 +2086,6 @@
property public abstract long layerId;
}
- public interface Owner {
- method public long calculatePosition-nOcc-ac();
- method public long calculatePositionInWindow-nOcc-ac();
- method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
- method public androidx.compose.ui.platform.ClipboardManager getClipboardManager();
- method public androidx.compose.ui.unit.Density getDensity();
- method public androidx.compose.ui.focus.FocusManager getFocusManager();
- method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
- method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public long getMeasureIteration();
- method public androidx.compose.ui.node.LayoutNode getRoot();
- method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
- method public boolean getShowLayoutBounds();
- method public androidx.compose.ui.node.OwnerSnapshotObserver getSnapshotObserver();
- method public androidx.compose.ui.text.input.TextInputService getTextInputService();
- method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
- method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
- method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
- method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
- method public void measureAndLayout();
- method public void onAttach(androidx.compose.ui.node.LayoutNode node);
- method public void onDetach(androidx.compose.ui.node.LayoutNode node);
- method public void onLayoutChange(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestMeasure(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onSemanticsChange();
- method public boolean requestFocus();
- method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.Autofill? autofill;
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
- property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
- property public abstract androidx.compose.ui.unit.Density density;
- property public abstract androidx.compose.ui.focus.FocusManager focusManager;
- property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
- property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public abstract long measureIteration;
- property public abstract androidx.compose.ui.node.LayoutNode root;
- property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
- property public abstract boolean showLayoutBounds;
- property public abstract androidx.compose.ui.node.OwnerSnapshotObserver snapshotObserver;
- property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
- property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
- property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
- property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
- property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
- field public static final androidx.compose.ui.node.Owner.Companion Companion;
- }
-
- public static final class Owner.Companion {
- method public boolean getEnableExtraAssertions();
- method public void setEnableExtraAssertions(boolean p);
- property public final boolean enableExtraAssertions;
- }
-
public interface OwnerScope {
method public boolean isValid();
property public abstract boolean isValid;
@@ -2160,6 +2102,16 @@
property public final T? value;
}
+ public interface RootForTest {
+ method public androidx.compose.ui.unit.Density getDensity();
+ method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
+ method public androidx.compose.ui.text.input.TextInputService getTextInputService();
+ method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
+ property public abstract androidx.compose.ui.unit.Density density;
+ property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
+ property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
+ }
+
public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
method public void insertBottomUp(int index, Object instance);
@@ -2401,7 +2353,7 @@
property public abstract float touchSlop;
}
- @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.RootForTest {
method public boolean getHasPendingMeasureOrLayout();
method public android.view.View getView();
method public void invalidateDescendants();
@@ -2754,9 +2706,9 @@
method public int getId();
method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
- method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
+ method public androidx.compose.ui.node.RootForTest? getRoot();
method public long getSize-YbymL2g();
method public boolean isRoot();
property public final androidx.compose.ui.geometry.Rect boundsInRoot;
@@ -2768,9 +2720,9 @@
property public final boolean isRoot;
property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
- property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
+ property public final androidx.compose.ui.node.RootForTest? root;
property public final long size;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 90634da..863dbec 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2086,64 +2086,6 @@
property public abstract long layerId;
}
- public interface Owner {
- method public long calculatePosition-nOcc-ac();
- method public long calculatePositionInWindow-nOcc-ac();
- method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
- method public androidx.compose.ui.platform.ClipboardManager getClipboardManager();
- method public androidx.compose.ui.unit.Density getDensity();
- method public androidx.compose.ui.focus.FocusManager getFocusManager();
- method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
- method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public long getMeasureIteration();
- method public androidx.compose.ui.node.LayoutNode getRoot();
- method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
- method public boolean getShowLayoutBounds();
- method public androidx.compose.ui.node.OwnerSnapshotObserver getSnapshotObserver();
- method public androidx.compose.ui.text.input.TextInputService getTextInputService();
- method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
- method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
- method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
- method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
- method public void measureAndLayout();
- method public void onAttach(androidx.compose.ui.node.LayoutNode node);
- method public void onDetach(androidx.compose.ui.node.LayoutNode node);
- method public void onLayoutChange(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestMeasure(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onSemanticsChange();
- method public boolean requestFocus();
- method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.Autofill? autofill;
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
- property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
- property public abstract androidx.compose.ui.unit.Density density;
- property public abstract androidx.compose.ui.focus.FocusManager focusManager;
- property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
- property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public abstract long measureIteration;
- property public abstract androidx.compose.ui.node.LayoutNode root;
- property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
- property public abstract boolean showLayoutBounds;
- property public abstract androidx.compose.ui.node.OwnerSnapshotObserver snapshotObserver;
- property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
- property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
- property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
- property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
- property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
- field public static final androidx.compose.ui.node.Owner.Companion Companion;
- }
-
- public static final class Owner.Companion {
- method public boolean getEnableExtraAssertions();
- method public void setEnableExtraAssertions(boolean p);
- property public final boolean enableExtraAssertions;
- }
-
public interface OwnerScope {
method public boolean isValid();
property public abstract boolean isValid;
@@ -2160,6 +2102,16 @@
property public final T? value;
}
+ public interface RootForTest {
+ method public androidx.compose.ui.unit.Density getDensity();
+ method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
+ method public androidx.compose.ui.text.input.TextInputService getTextInputService();
+ method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
+ property public abstract androidx.compose.ui.unit.Density density;
+ property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
+ property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
+ }
+
public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
method public void insertBottomUp(int index, Object instance);
@@ -2401,7 +2353,7 @@
property public abstract float touchSlop;
}
- @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.RootForTest {
method public boolean getHasPendingMeasureOrLayout();
method public android.view.View getView();
method public void invalidateDescendants();
@@ -2754,9 +2706,9 @@
method public int getId();
method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
- method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
+ method public androidx.compose.ui.node.RootForTest? getRoot();
method public long getSize-YbymL2g();
method public boolean isRoot();
property public final androidx.compose.ui.geometry.Rect boundsInRoot;
@@ -2768,9 +2720,9 @@
property public final boolean isRoot;
property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
- property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
+ property public final androidx.compose.ui.node.RootForTest? root;
property public final long size;
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index e6be08b..9ba676f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2148,64 +2148,6 @@
property public abstract long layerId;
}
- public interface Owner {
- method public long calculatePosition-nOcc-ac();
- method public long calculatePositionInWindow-nOcc-ac();
- method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
- method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
- method public androidx.compose.ui.platform.ClipboardManager getClipboardManager();
- method public androidx.compose.ui.unit.Density getDensity();
- method public androidx.compose.ui.focus.FocusManager getFocusManager();
- method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
- method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public long getMeasureIteration();
- method public androidx.compose.ui.node.LayoutNode getRoot();
- method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
- method public boolean getShowLayoutBounds();
- method public androidx.compose.ui.node.OwnerSnapshotObserver getSnapshotObserver();
- method public androidx.compose.ui.text.input.TextInputService getTextInputService();
- method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
- method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
- method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
- method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
- method public void measureAndLayout();
- method public void onAttach(androidx.compose.ui.node.LayoutNode node);
- method public void onDetach(androidx.compose.ui.node.LayoutNode node);
- method public void onLayoutChange(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestMeasure(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
- method public void onSemanticsChange();
- method public boolean requestFocus();
- method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.Autofill? autofill;
- property @androidx.compose.ui.ExperimentalComposeUiApi public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
- property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
- property public abstract androidx.compose.ui.unit.Density density;
- property public abstract androidx.compose.ui.focus.FocusManager focusManager;
- property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
- property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public abstract long measureIteration;
- property public abstract androidx.compose.ui.node.LayoutNode root;
- property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
- property public abstract boolean showLayoutBounds;
- property public abstract androidx.compose.ui.node.OwnerSnapshotObserver snapshotObserver;
- property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
- property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
- property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
- property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
- property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
- field public static final androidx.compose.ui.node.Owner.Companion Companion;
- }
-
- public static final class Owner.Companion {
- method public boolean getEnableExtraAssertions();
- method public void setEnableExtraAssertions(boolean p);
- property public final boolean enableExtraAssertions;
- }
-
public interface OwnerScope {
method public boolean isValid();
property public abstract boolean isValid;
@@ -2222,6 +2164,16 @@
property public final T? value;
}
+ public interface RootForTest {
+ method public androidx.compose.ui.unit.Density getDensity();
+ method public androidx.compose.ui.semantics.SemanticsOwner getSemanticsOwner();
+ method public androidx.compose.ui.text.input.TextInputService getTextInputService();
+ method public boolean sendKeyEvent-ZmokQxo(android.view.KeyEvent keyEvent);
+ property public abstract androidx.compose.ui.unit.Density density;
+ property public abstract androidx.compose.ui.semantics.SemanticsOwner semanticsOwner;
+ property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
+ }
+
public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
method public void insertBottomUp(int index, Object instance);
@@ -2463,7 +2415,7 @@
property public abstract float touchSlop;
}
- @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.RootForTest {
method public boolean getHasPendingMeasureOrLayout();
method public android.view.View getView();
method public void invalidateDescendants();
@@ -2816,9 +2768,9 @@
method public int getId();
method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
- method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
+ method public androidx.compose.ui.node.RootForTest? getRoot();
method public long getSize-YbymL2g();
method public boolean isRoot();
property public final androidx.compose.ui.geometry.Rect boundsInRoot;
@@ -2830,9 +2782,9 @@
property public final boolean isRoot;
property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
- property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
+ property public final androidx.compose.ui.node.RootForTest? root;
property public final long size;
}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 952edf6..108a403 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -21,6 +21,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static androidx.build.dependencies.DependenciesKt.*
+import static androidx.inspection.gradle.InspectionPluginKt.packageInspector
plugins {
id("AndroidXPlugin")
@@ -109,6 +110,8 @@
}
}
+packageInspector(project, project(":compose:ui:ui-inspection"))
+
if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
kotlin {
android()
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
index 8465693..5686f60 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
@@ -51,6 +51,7 @@
imageVector.resource.resource?.let {
Image(
imageVector = it,
+ contentDescription = "Crane",
modifier = Modifier.preferredSize(200.dp, 200.dp),
contentScale = ContentScale.Inside
)
@@ -60,6 +61,7 @@
complexImageVector.resource.resource?.let {
Image(
imageVector = it,
+ contentDescription = "Hourglass",
modifier = Modifier.preferredSize(64.dp, 64.dp),
contentScale = ContentScale.Fit
)
@@ -67,6 +69,7 @@
Image(
painter = vectorShape(120.dp, 120.dp),
+ contentDescription = null,
modifier = Modifier.preferredSize(200.dp, 150.dp)
)
}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
index 15c1dbd..cb5f8ad 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
@@ -111,6 +111,7 @@
}
Image(
painter = vectorPainter,
+ contentDescription = null,
modifier = Modifier.size(120.dp).drawWithCache {
val gradient = Brush.linearGradient(
colors = listOf(Color.Red, Color.Blue),
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
index dfce282..48f7a8e 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
@@ -66,6 +66,7 @@
// in the landscape orientation based on the res/drawable and res/drawable-land-hdpi folders
Image(
painterResource(R.drawable.ic_vector_or_png),
+ contentDescription = null,
modifier = Modifier.size(50.dp)
)
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 66d65f3..3847526 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -233,13 +233,14 @@
val clickState = remember { mutableStateOf(false) }
Image(
imageVector = if (clickState.value) icon1 else icon2,
+ contentDescription = null,
modifier = Modifier
.testTag(testTag)
.preferredSize(icon1.defaultWidth, icon1.defaultHeight)
.background(Color.Red)
.clickable { clickState.value = !clickState.value },
- contentScale = ContentScale.FillHeight,
- alignment = Alignment.TopStart
+ alignment = Alignment.TopStart,
+ contentScale = ContentScale.FillHeight
)
}
@@ -292,6 +293,7 @@
}
Image(
painter = vectorPainter,
+ contentDescription = null,
modifier = Modifier
.testTag(testTag)
.preferredSize(defaultWidth * 7, defaultHeight * 3)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 9538232..c28b85c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -24,7 +24,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
@@ -34,11 +33,11 @@
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.Constraints
@@ -3037,9 +3036,10 @@
override fun calculatePositionInWindow(): IntOffset = position
override fun requestFocus(): Boolean = false
- override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
override val root: LayoutNode
get() = targetRoot
+ override val rootForTest: RootForTest
+ get() = TODO("Not yet implemented")
override val hapticFeedBack: HapticFeedback
get() = TODO("Not yet implemented")
override val clipboardManager: ClipboardManager
@@ -3052,8 +3052,6 @@
get() = null
override val density: Density
get() = Density(1f)
- override val semanticsOwner: SemanticsOwner
- get() = TODO("Not yet implemented")
override val textInputService: TextInputService
get() = TODO("Not yet implemented")
override val focusManager: FocusManager
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
index 73e2715..186a2b0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
@@ -92,6 +92,43 @@
}
@Test
+ @SmallTest
+ fun internalSizeChange() {
+ var latch = CountDownLatch(1)
+ var changedSize = IntSize.Zero
+ var sizePx by mutableStateOf(10)
+
+ rule.runOnUiThread {
+ activity.setContent {
+ with(AmbientDensity.current) {
+ Box(
+ Modifier.padding(10.toDp())
+ .onSizeChanged {
+ changedSize = it
+ latch.countDown()
+ }.padding(sizePx.toDp())
+ ) {
+ Box(Modifier.size(10.toDp()))
+ }
+ }
+ }
+ }
+
+ // Initial setting will call onSizeChanged
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(30, changedSize.height)
+ assertEquals(30, changedSize.width)
+
+ latch = CountDownLatch(1)
+ sizePx = 20
+
+ // We've changed the size of the contents, so we should receive a onSizeChanged call
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(50, changedSize.height)
+ assertEquals(50, changedSize.width)
+ }
+
+ @Test
fun onlyInnerSizeChange() {
var latch = CountDownLatch(1)
var changedSize = IntSize.Zero
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
index dcdc42e..0a89378 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
@@ -124,7 +124,7 @@
private fun isSecureFlagEnabledForDialog(): Boolean {
val owner = rule
.onNode(isDialog())
- .fetchSemanticsNode("").owner as View
+ .fetchSemanticsNode("").root as View
return (owner.rootView.layoutParams as WindowManager.LayoutParams).flags and
WindowManager.LayoutParams.FLAG_SECURE != 0
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
index 8b464cd..285f48d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
@@ -23,9 +23,13 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.height
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
@@ -117,7 +121,9 @@
// Click outside the dialog to dismiss it
val outsideX = 0
- val outsideY = rule.displaySize.height / 2
+ val outsideY = with(rule.density) {
+ rule.onAllNodes(isRoot()).onFirst().getUnclippedBoundsInRoot().height.toIntPx() / 2
+ }
UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
rule.onNodeWithText(defaultText).assertDoesNotExist()
@@ -139,7 +145,9 @@
// Click outside the dialog to try to dismiss it
val outsideX = 0
- val outsideY = rule.displaySize.height / 2
+ val outsideY = with(rule.density) {
+ rule.onAllNodes(isRoot()).onFirst().getUnclippedBoundsInRoot().height.toIntPx() / 2
+ }
UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
// The Dialog should still be visible
@@ -167,7 +175,9 @@
// Click outside the dialog to try to dismiss it
val outsideX = 0
- val outsideY = rule.displaySize.height / 2
+ val outsideY = with(rule.density) {
+ rule.onAllNodes(isRoot()).onFirst().getUnclippedBoundsInRoot().height.toIntPx() / 2
+ }
UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
// The Dialog should still be visible
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index da1cd3d..2ace3e0 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -64,6 +64,7 @@
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.InternalTextApi
@@ -130,6 +131,8 @@
.then(keyInputModifier)
}
+ override val rootForTest: RootForTest = this
+
override val semanticsOwner: SemanticsOwner = SemanticsOwner(root)
private val accessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
index 461e12a..77445b1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
@@ -18,15 +18,14 @@
import android.view.View
import androidx.annotation.VisibleForTesting
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
/**
* The marker interface to be implemented by the [View] backing the composition.
* To be used in tests.
*/
@VisibleForTesting
-// TODO(b/174747742) Introduce RootForTest and extend it instead of Owner
-interface ViewRootForTest : Owner {
+interface ViewRootForTest : RootForTest {
/**
* The view backing this Owner.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
index 338fcaf..90d7e8b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
@@ -23,14 +23,17 @@
import androidx.compose.ui.unit.IntSize
/**
- * Invoke [onSizeChanged] with the size of the content after it has been measured.
- * This will only be invoked either the first time measurement happens or when the content
+ * Invoke [onSizeChanged] when the size of the modifier immediately after it has changed. If
+ * there is no modifier following [onSizeChanged], the content size of the layout is reported.
+ *
+ * [onSizeChanged] will only be invoked during the first time measurement or when the
* size has changed.
*
- * Use [Layout] or [SubcomposeLayout] to have the size of one component to affect the size
+ * Use [Layout] or [SubcomposeLayout] to have the size of one component affect the size
* of another component. Using the size received from the [onSizeChanged] callback in a
* [MutableState] to affect layout will cause the new value to be recomposed and read only in the
- * following frame, causing a one frame lag.
+ * following frame, causing a one frame lag. You can use [onSizeChanged] to affect
+ * drawing operations.
*
* Example usage:
* @sample androidx.compose.ui.samples.OnSizeChangedSample
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 7f1d771..c4b79fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -60,7 +60,6 @@
import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.deleteAt
import kotlin.math.roundToInt
@@ -642,7 +641,6 @@
}
val addedCallback = hasNewPositioningCallback()
onPositionedCallbacks.clear()
- onRemeasuredCallbacks.clear()
// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers
// when possible.
@@ -651,9 +649,6 @@
if (mod is OnGloballyPositionedModifier) {
onPositionedCallbacks += mod
}
- if (mod is OnRemeasuredModifier) {
- onRemeasuredCallbacks += mod
- }
if (mod is RemeasurementModifier) {
mod.onRemeasurementAvailable(this)
}
@@ -699,6 +694,9 @@
if (mod is SemanticsModifier) {
wrapper = SemanticsWrapper(wrapper, mod).assignChained(toWrap)
}
+ if (mod is OnRemeasuredModifier) {
+ wrapper = RemeasureModifierWrapper(wrapper, mod).assignChained(toWrap)
+ }
}
wrapper
}
@@ -770,11 +768,6 @@
private val >
/**
- * List of all OnSizeChangedModifiers in the modifier chain.
- */
- private val >
-
- /**
* Flag used by [OnPositionedDispatcher] to identify LayoutNodes that have already
* had their [OnGloballyPositionedModifier]'s dispatch called so that they aren't called
* multiple times.
@@ -1070,16 +1063,6 @@
innerLayoutNodeWrapper.measureResult = measureResult
this.providedAlignmentLines.clear()
this.providedAlignmentLines += measureResult.alignmentLines
-
- if (onRemeasuredCallbacks.isNotEmpty()) {
- val invokeRemeasureCallbacks = {
- val content = innerLayoutNodeWrapper
- val size = IntSize(content.measuredWidth, content.measuredHeight)
- onRemeasuredCallbacks.forEach { it.onRemeasured(size) }
- }
- owner?.snapshotObserver?.pauseSnapshotReadObservation(invokeRemeasureCallbacks)
- ?: invokeRemeasureCallbacks.invoke()
- }
}
/**
@@ -1389,4 +1372,4 @@
wrapper.isChained = true
}
return this
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 9ec0253..64f47eb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -21,12 +21,10 @@
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.Density
@@ -38,13 +36,15 @@
* to Android [views][android.view.View] and all layout, draw, input, and accessibility is hooked
* through them.
*/
-interface Owner {
+internal interface Owner {
/**
* The root layout node in the component tree.
*/
val root: LayoutNode
+ val rootForTest: RootForTest
+
/**
* Provide haptic feedback to the user. Use the Android version of haptic feedback.
*/
@@ -79,8 +79,6 @@
val density: Density
- val semanticsOwner: SemanticsOwner
-
val textInputService: TextInputService
/**
@@ -158,13 +156,6 @@
fun requestFocus(): Boolean
/**
- * Send this [KeyEvent] to the focused component in this [Owner].
- *
- * @return true if the event was consumed. False otherwise.
- */
- fun sendKeyEvent(keyEvent: KeyEvent): Boolean
-
- /**
* Iterates through all LayoutNodes that have requested layout and measures and lays them out
*/
fun measureAndLayout()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
new file mode 100644
index 0000000..6d25308
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Wrapper around the [OnRemeasuredModifier] to notify whenever a remeasurement happens.
+ */
+internal class RemeasureModifierWrapper(
+ wrapped: LayoutNodeWrapper,
+ modifier: OnRemeasuredModifier
+) : DelegatingLayoutNodeWrapper<OnRemeasuredModifier>(wrapped, modifier) {
+ override fun performMeasure(constraints: Constraints): Placeable {
+ val placeable = super.performMeasure(constraints)
+ val invokeRemeasureCallbacks = {
+ modifier.onRemeasured(measuredSize)
+ }
+ layoutNode.owner?.snapshotObserver?.pauseSnapshotReadObservation(invokeRemeasureCallbacks)
+ ?: invokeRemeasureCallbacks.invoke()
+ return placeable
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RootForTest.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RootForTest.kt
new file mode 100644
index 0000000..cd72eb2
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RootForTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Density
+
+/**
+ * The marker interface to be implemented by the root backing the composition.
+ * To be used in tests.
+ */
+interface RootForTest {
+ /**
+ * Current device density.
+ */
+ val density: Density
+
+ /**
+ * Semantics owner for this root. Manages all the semantics nodes.
+ */
+ val semanticsOwner: SemanticsOwner
+
+ /**
+ * The service handling text input.
+ */
+ val textInputService: TextInputService
+
+ /**
+ * Send this [KeyEvent] to the focused component in this [Owner].
+ *
+ * @return true if the event was consumed. False otherwise.
+ */
+ fun sendKeyEvent(keyEvent: KeyEvent): Boolean
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 7b5b0f3..87b4899b2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNodeWrapper
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -73,10 +73,9 @@
val layoutInfo: LayoutInfo = layoutNodeWrapper.layoutNode
/**
- * The [Owner] this node is attached to.
+ * The [root][RootForTest] this node is attached to.
*/
- // TODO(b/174747742) Stop using Owner in tests and use RootForTest instead
- val owner: Owner? get() = layoutNode.owner
+ val root: RootForTest? get() = layoutNode.owner?.rootForTest
/**
* The [LayoutNode] that this is associated with.
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 1a72190..8e9fbd0 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -46,6 +46,7 @@
import androidx.compose.ui.node.MeasureAndLayoutDelegate
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.input.TextInputService
@@ -64,7 +65,7 @@
class DesktopOwner(
val container: DesktopOwners,
density: Density = Density(1f, 1f)
-) : Owner {
+) : Owner, RootForTest {
internal var size by mutableStateOf(IntSize(0, 0))
override var density by mutableStateOf(density)
@@ -97,6 +98,8 @@
.then(keyInputModifier)
}
+ override val rootForTest = this
+
override val snapshotObserver = OwnerSnapshotObserver { command ->
command()
}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 09dfdc4..b262eeb 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -30,7 +30,6 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.layout.LayoutModifier
@@ -44,7 +43,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.Constraints
@@ -1720,6 +1718,8 @@
val >
var layoutChangeCount = 0
+ override val rootForTest: RootForTest
+ get() = TODO("Not yet implemented")
override val hapticFeedBack: HapticFeedback
get() = TODO("Not yet implemented")
override val clipboardManager: ClipboardManager
@@ -1734,8 +1734,6 @@
get() = TODO("Not yet implemented")
override val density: Density
get() = Density(1f)
- override val semanticsOwner: SemanticsOwner
- get() = TODO("Not yet implemented")
override val textInputService: TextInputService
get() = TODO("Not yet implemented")
override val focusManager: FocusManager
@@ -1771,8 +1769,6 @@
override fun requestFocus(): Boolean = false
- override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
override fun measureAndLayout() {
}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 8a6e871..072e510 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -420,6 +420,9 @@
WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+ \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+\-alpha[0-9]+\.jar\) to field com\.sun\.tools\.javac\.code\.Symbol\$ClassSymbol\.classfile
WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+
# > Task :wear:wear-watchface-complications-rendering:compileDebugUnitTestJavaWithJavac
+# > Task :wear:wear-watchface:testDebugUnitTest
+System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'dispose\' not called
+System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'release\' not called
# > Task :benchmark:benchmark-perfetto:mergeDebugAndroidTestJavaResource
More than one file was found with OS independent path '.*'\. This version of the Android Gradle Plugin chooses the file from the app or dynamic\-feature module, but this can cause unexpected behavior or errors at runtime\. Future versions of the Android Gradle Plugin will throw an error in this case\.
# > Task :docs-runner:dokkaJavaTipOfTreeDocs
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 146b6ca..e134e44 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -35,6 +35,7 @@
docs(project(":camera:camera-lifecycle"))
docs(project(":camera:camera-view"))
docs(project(":car:app:app"))
+ docs(project(":car:app:app-aaos"))
docs(project(":cardview:cardview"))
docs(project(":collection:collection"))
docs(project(":collection:collection-ktx"))
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
index 04ce980..0008abd 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
@@ -230,9 +230,9 @@
Animation anim = Preconditions.checkNotNull(
Preconditions.checkNotNull(animationInfo.getAnimation(context)).animation);
Operation.State finalState = operation.getFinalState();
- if (finalState == Operation.State.VISIBLE) {
- // If we've moving to VISIBLE, we can't use a AnimationSet
- // due that causing the introduction of visual artifacts (b/163084315).
+ if (finalState != Operation.State.REMOVED) {
+ // If the operation does not remove the view, we can't use a
+ // AnimationSet due that causing the introduction of visual artifacts (b/163084315).
viewToAnimate.startAnimation(anim);
// This means we can't use setAnimationListener() without overriding
// any listener that the Fragment has set themselves, so we
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
index 8cccb82..e370f53 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
@@ -95,6 +95,10 @@
private static final String SAVED_CANCELABLE = "android:cancelable";
private static final String SAVED_SHOWS_DIALOG = "android:showsDialog";
private static final String SAVED_BACK_STACK_ID = "android:backStackId";
+ /**
+ * Copied from {@link Dialog}.
+ */
+ private static final String SAVED_INTERNAL_DIALOG_SHOWING = "android:dialogShowing";
private Handler mHandler;
private Runnable mDismissRunnable = new Runnable() {
@@ -691,6 +695,7 @@
super.onSaveInstanceState(outState);
if (mDialog != null) {
Bundle dialogState = mDialog.onSaveInstanceState();
+ dialogState.putBoolean(SAVED_INTERNAL_DIALOG_SHOWING, false);
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
}
if (mStyle != STYLE_NORMAL) {
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/GenerateProguardDetectionFileTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/GenerateProguardDetectionFileTask.kt
index ac34dbb..08cd184 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/GenerateProguardDetectionFileTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/GenerateProguardDetectionFileTask.kt
@@ -114,5 +114,7 @@
val strippedArtifact = mavenArtifact.removePrefix(mavenGroup.split('.').last())
.removePrefix("-").replace('-', '.')
val group = mavenGroup.removePrefix("androidx.")
- return "androidx.inspection.$group.$strippedArtifact"
+ // It's possible for strippedArtifact to be empty, e.g. "compose.ui/ui" has no hyphen
+ return "androidx.inspection.$group" +
+ if (strippedArtifact.isNotEmpty()) ".$strippedArtifact" else ""
}
\ No newline at end of file
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index e5a1eba..81df245 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -124,10 +124,12 @@
inspectorProject.project.plugins.withType(InspectionPlugin::class.java) { inspectionPlugin ->
val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
libExtension.libraryVariants.all { variant ->
- variant.packageLibraryProvider.configure { zip ->
- val outputFile = inspectionPlugin.dexTask.get().outputFile
- zip.from(outputFile)
- zip.rename(outputFile.asFile.get().name, "inspector.jar")
+ inspectorProject.afterEvaluate {
+ variant.packageLibraryProvider.configure { zip ->
+ val outputFile = inspectionPlugin.dexTask.get().outputFile
+ zip.from(outputFile)
+ zip.rename(outputFile.asFile.get().name, "inspector.jar")
+ }
}
}
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java b/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.kt
similarity index 91%
rename from navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
rename to navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.kt
index f9f0fbb..167dcb1 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.java
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavArgs.kt
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.navigation;
+package androidx.navigation
/**
* An interface marking generated Args classes.
*/
-public interface NavArgs {
-}
+public interface NavArgs
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.java b/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.java
deleted file mode 100644
index a4ed438..0000000
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.navigation;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-/**
- * A {@link Navigator} that only supports creating destinations.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@Navigator.Name("NoOp")
-public class NoOpNavigator extends Navigator<NavDestination> {
- @NonNull
- @Override
- public NavDestination createDestination() {
- return new NavDestination(this);
- }
-
- @Nullable
- @Override
- public NavDestination navigate(@NonNull NavDestination destination, @Nullable Bundle args,
- @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
- return destination;
- }
-
- @Override
- public boolean popBackStack() {
- return true;
- }
-}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.kt
new file mode 100644
index 0000000..044db7a
--- /dev/null
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NoOpNavigator.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.navigation
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+
+/**
+ * A [Navigator] that only supports creating destinations.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Navigator.Name("NoOp")
+public class NoOpNavigator : Navigator<NavDestination>() {
+ override fun createDestination(): NavDestination = NavDestination(this)
+
+ override fun navigate(
+ destination: NavDestination,
+ args: Bundle?,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ): NavDestination? = destination
+
+ override fun popBackStack(): Boolean = true
+}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
index 5367b9f..7c3f3159c 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
@@ -53,7 +53,7 @@
val entryRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
items.forEach { (name, route) ->
BottomNavigationItem(
- icon = { Icon(Icons.Filled.Favorite) },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(name) },
selected = entryRoute == route,
>
diff --git a/settings.gradle b/settings.gradle
index a7f2b13..cae4b3a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -159,6 +159,7 @@
includeProject(":benchmark:integration-tests:startup-benchmark", "benchmark/integration-tests/startup-benchmark", [BuildType.MAIN])
includeProject(":biometric:biometric", "biometric/biometric", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx", "biometric/biometric-ktx", [BuildType.MAIN])
+includeProject(":biometric:biometric-ktx-samples", "biometric/biometric-ktx/samples", [BuildType.MAIN])
includeProject(":biometric:integration-tests:testapp", "biometric/integration-tests/testapp", [BuildType.MAIN])
includeProject(":browser:browser", "browser/browser", [BuildType.MAIN])
includeProject(":buildSrc-tests", "buildSrc-tests", [BuildType.MAIN])
@@ -186,6 +187,7 @@
includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testlib-extensions", "camera/integration-tests/extensionstestlib", [BuildType.MAIN])
includeProject(":car:app:app", "car/app/app", [BuildType.MAIN])
+includeProject(":car:app:app-aaos", "car/app/app-aaos", [BuildType.MAIN])
includeProject(":cardview:cardview", "cardview/cardview", [BuildType.MAIN])
includeProject(":collection:collection", "collection/collection", [BuildType.MAIN])
includeProject(":collection:collection-benchmark", "collection/collection-benchmark", [BuildType.MAIN])
@@ -255,6 +257,7 @@
includeProject(":compose:ui:ui-geometry", "compose/ui/ui-geometry", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-graphics", "compose/ui/ui-graphics", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-graphics:ui-graphics-samples", "compose/ui/ui-graphics/samples", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-inspection", "compose/ui/ui-inspection", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-lint", "compose/ui/ui-lint", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test", "compose/ui/ui-test", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test-font", "compose/ui/ui-test-font", [BuildType.COMPOSE])
@@ -326,7 +329,7 @@
includeProject(":hilt:hilt-work", "hilt/hilt-work", [BuildType.MAIN])
includeProject(":hilt:integration-tests:hilt-testapp-viewmodel", "hilt/integration-tests/viewmodelapp", [BuildType.MAIN])
includeProject(":hilt:integration-tests:hilt-testapp-worker", "hilt/integration-tests/workerapp", [BuildType.MAIN])
-includeProject(":inspection:inspection", "inspection/inspection", [BuildType.MAIN])
+includeProject(":inspection:inspection", "inspection/inspection", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":inspection:inspection-gradle-plugin", "inspection/inspection-gradle-plugin", [BuildType.MAIN])
includeProject(":inspection:inspection-testing", "inspection/inspection-testing", [BuildType.MAIN])
includeProject(":interpolator:interpolator", "interpolator/interpolator", [BuildType.MAIN])
@@ -360,11 +363,11 @@
includeProject(":lifecycle:lifecycle-runtime", "lifecycle/lifecycle-runtime", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-runtime-ktx", "lifecycle/lifecycle-runtime-ktx", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-runtime-ktx-lint", "lifecycle/lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-runtime-testing", "lifecycle/lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.FLAN])
+includeProject(":lifecycle:lifecycle-runtime-testing", "lifecycle/lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-service", "lifecycle/lifecycle-service", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.FLAN])
+includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lint-checks", "lint-checks")
includeProject(":lint-checks:tests", "lint-checks/tests", [BuildType.MAIN])
includeProject(":lint-demos:lint-demo-appcompat", "lint-demos/lint-demo-appcompat", [BuildType.MAIN])
diff --git a/slices/view/build.gradle b/slices/view/build.gradle
index 5d8e730..29aa15a 100644
--- a/slices/view/build.gradle
+++ b/slices/view/build.gradle
@@ -27,7 +27,7 @@
dependencies {
implementation(project(":slice-core"))
implementation project(":appcompat:appcompat")
- implementation("androidx.recyclerview:recyclerview:1.1.0")
+ implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.collection:collection:1.1.0")
api("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceContent.java b/slices/view/src/main/java/androidx/slice/widget/SliceContent.java
index 6061e64..6dc5983 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceContent.java
@@ -256,7 +256,8 @@
}
if (actionItem == null) {
Intent intent = new Intent();
- PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
+ PendingIntent pi = PendingIntent.getActivity(context, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
actionItem = new SliceItem(pi, null, FORMAT_ACTION, null, null);
}
if (shortcutAction != null && shortcutIcon != null && actionItem != null) {
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index 8eae4e5..bf00bec 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -31,7 +31,6 @@
id("AndroidXPlugin")
id("com.android.application")
id("org.jetbrains.kotlin.android")
- id('kotlin-android-extensions')
}
dependencies {
diff --git a/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt b/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
index ed7adc78..fdb6534 100644
--- a/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
+++ b/wear/wear-complications-data/src/androidTest/java/androidx/wear/complications/ComplicationHelperActivityTest.kt
@@ -16,38 +16,99 @@
package androidx.wear.complications
import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import androidx.wear.complications.data.ComplicationType
-import com.google.common.truth.Truth
+import androidx.wear.complications.data.ComplicationType.LONG_TEXT
+import androidx.wear.complications.data.ComplicationType.MONOCHROMATIC_IMAGE
+import androidx.wear.complications.data.ComplicationType.SHORT_TEXT
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
public class ComplicationHelperActivityTest {
+
@Test
- public fun createProviderChooserHelperIntent() {
- val complicationId = 1234
- val watchFaceComponentName = ComponentName("test.package", "test.class")
- val complicationTypes = listOf(ComplicationType.SHORT_TEXT, ComplicationType.LONG_TEXT)
- val intent = ComplicationHelperActivity.createProviderChooserHelperIntent(
- ApplicationProvider.getApplicationContext(),
- watchFaceComponentName,
- complicationId,
- complicationTypes
- )
- val expectedSupportedTypes = intArrayOf(
- ComplicationType.SHORT_TEXT.asWireComplicationType(),
- ComplicationType.LONG_TEXT.asWireComplicationType()
- )
- val actualComponentName = intent.getParcelableExtra<ComponentName>(
- ProviderChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME
- )
- Truth.assertThat(actualComponentName).isEqualTo(watchFaceComponentName)
- Truth.assertThat(intent.getIntExtra(ProviderChooserIntent.EXTRA_COMPLICATION_ID, -1))
- .isEqualTo(complicationId)
- Truth.assertThat(intent.getIntArrayExtra(ProviderChooserIntent.EXTRA_SUPPORTED_TYPES))
- .isEqualTo(expectedSupportedTypes)
- Truth.assertThat(intent.action)
+ public fun createProviderChooserHelperIntent_action() {
+ assertThat(createIntent().action)
.isEqualTo(ComplicationHelperActivity.ACTION_START_PROVIDER_CHOOSER)
- Truth.assertThat(intent.component!!.className)
- .isEqualTo(ComplicationHelperActivity::class.java.name)
+ }
+
+ @Test
+ public fun createProviderChooserHelperIntent_component() {
+ assertThat(createIntent().component)
+ .isEqualTo(ComponentName(context, ComplicationHelperActivity::class.java))
+ }
+
+ @Test
+ public fun createProviderChooserHelperIntent_watchFaceComponentName() {
+ ComponentName("package-name", "watch-face-service-name").let {
+ assertThat(createIntent(watchFaceComponentName = it).watchFaceComponentName)
+ .isEqualTo(it)
+ }
+ ComponentName(context, "service-name").let {
+ assertThat(createIntent(watchFaceComponentName = it).watchFaceComponentName)
+ .isEqualTo(it)
+ }
+ }
+
+ @Test
+ public fun createProviderChooserHelperIntent_complicationId() {
+ assertThat(createIntent(complicationId = -1).complicationId).isEqualTo(-1)
+ assertThat(createIntent(complicationId = 1234).complicationId).isEqualTo(1234)
+ assertThat(createIntent(complicationId = 30000).complicationId).isEqualTo(30000)
+ }
+
+ @Test
+ public fun createProviderChooserHelperIntent_supportedTypes() {
+ arrayOf<ComplicationType>().let {
+ assertThat(createIntent(supportedTypes = it).supportedTypes).isEqualTo(it)
+ }
+ arrayOf(LONG_TEXT).let {
+ assertThat(createIntent(supportedTypes = it).supportedTypes).isEqualTo(it)
+ }
+ arrayOf(SHORT_TEXT, LONG_TEXT, MONOCHROMATIC_IMAGE).let {
+ assertThat(createIntent(supportedTypes = it).supportedTypes).isEqualTo(it)
+ }
+ }
+
+ /** Creates an intent with default values for unspecified parameters. */
+ private fun createIntent(
+ watchFaceComponentName: ComponentName = defaultWatchFaceComponentName,
+ complicationId: Int = defaultComplicationId,
+ vararg supportedTypes: ComplicationType = defaultSupportedTypes
+ ) = ComplicationHelperActivity.createProviderChooserHelperIntent(
+ context,
+ watchFaceComponentName,
+ complicationId,
+ supportedTypes.asList()
+ )
+
+ private companion object {
+ /** The context to be used in the various tests. */
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ /** The default watch face component name used in the test. */
+ private val defaultWatchFaceComponentName = ComponentName("test.package", "test.class")
+
+ /** The default complication ID used in the test. */
+ private const val defaultComplicationId = 1234
+
+ /** The default supported types used in the test. */
+ private val defaultSupportedTypes = arrayOf(SHORT_TEXT, LONG_TEXT)
}
}
+
+/** The watch face component name encoded in the intent. */
+private val Intent.watchFaceComponentName
+ get() = getParcelableExtra<ComponentName>(ProviderChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME)
+
+/** The complication ID encoded in the intent. */
+private val Intent.complicationId
+ get() = getIntExtra(ProviderChooserIntent.EXTRA_COMPLICATION_ID, -1)
+
+/** The support types encoded in the intent. */
+private val Intent.supportedTypes
+ get() = ComplicationType.fromWireTypes(
+ getIntArrayExtra(ProviderChooserIntent.EXTRA_SUPPORTED_TYPES)!!
+ )
\ No newline at end of file
diff --git a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index cdb4705..764f5cb 100644
--- a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -51,7 +51,6 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.wear.complications.ComplicationHelperActivity;
import androidx.wear.watchface.CanvasType;
-import androidx.wear.watchface.ComplicationsManager;
import androidx.wear.watchface.Renderer;
import androidx.wear.watchface.WatchFace;
import androidx.wear.watchface.WatchFaceService;
@@ -724,7 +723,6 @@
return new WatchFace(
WatchFaceType.ANALOG,
userStyleRepository,
- new ComplicationsManager(new ArrayList<>(), userStyleRepository),
new Renderer.CanvasRenderer(
surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
16L) {
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 7319806..99cc7f6 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -371,12 +371,12 @@
)
}
- override var userStyle: UserStyle
- // We return a deep copy of the style because assigning to it can otherwise have unexpected
- // side effects.
- get() = UserStyle(editorDelegate.userStyleRepository.userStyle)
+ // We make a deep copy of the style because assigning to it can otherwise have unexpected
+ // side effects (it would apply to the active watch face).
+ override var userStyle = UserStyle(editorDelegate.userStyleRepository.userStyle)
set(value) {
- editorDelegate.userStyleRepository.userStyle = value
+ field = value
+ editorDelegate.userStyleRepository.userStyle = UserStyle(value)
}
override fun takeWatchFaceScreenshot(
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 5592cb3..b9bf6f7 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -37,6 +37,7 @@
method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
method public int getBoundsType();
method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
+ method public android.os.Bundle? getComplicationConfigExtras();
method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
@@ -50,6 +51,7 @@
method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
property public final int boundsType;
property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
+ property public final android.os.Bundle? complicationConfigExtras;
property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
@@ -61,6 +63,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
+ method public androidx.wear.watchface.Complication.Builder setComplicationConfigExtras(android.os.Bundle? extras);
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
@@ -200,7 +203,8 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method public Long? getOverridePreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 5592cb3..b9bf6f7 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -37,6 +37,7 @@
method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
method public int getBoundsType();
method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
+ method public android.os.Bundle? getComplicationConfigExtras();
method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
@@ -50,6 +51,7 @@
method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
property public final int boundsType;
property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
+ property public final android.os.Bundle? complicationConfigExtras;
property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
@@ -61,6 +63,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
+ method public androidx.wear.watchface.Complication.Builder setComplicationConfigExtras(android.os.Bundle? extras);
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
@@ -200,7 +203,8 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method public Long? getOverridePreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 505f3b1..f43ccbf 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -37,6 +37,7 @@
method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
method public int getBoundsType();
method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
+ method public android.os.Bundle? getComplicationConfigExtras();
method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
@@ -50,6 +51,7 @@
method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
property public final int boundsType;
property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
+ property public final android.os.Bundle? complicationConfigExtras;
property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
@@ -61,6 +63,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
+ method public androidx.wear.watchface.Complication.Builder setComplicationConfigExtras(android.os.Bundle? extras);
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
@@ -230,7 +233,8 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.watchface.WatchFace.EditorDelegate? getEditorDelegate(android.content.ComponentName componentName);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method public Long? getOverridePreviewReferenceTimeMillis();
@@ -292,9 +296,7 @@
method public androidx.wear.watchface.style.data.UserStyleWireFormat? getInitialUserStyle();
method @UiThread public void invalidate();
method public void setActiveComplications(int[] watchFaceComplicationIds);
- method public void setComplicationDetails(int complicationId, android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, int[] types);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void setContentDescriptionLabels(android.support.wearable.watchface.accessibility.ContentDescriptionLabel![] labels);
- method public void setCurrentUserStyle(androidx.wear.watchface.style.data.UserStyleWireFormat userStyle);
method public void setDefaultComplicationProviderWithFallbacks(int watchFaceComplicationId, java.util.List<android.content.ComponentName>? providers, @androidx.wear.complications.SystemProviders.ProviderId int fallbackSystemProvider, int type);
}
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 1324b1f..19e14a1 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -262,8 +262,8 @@
return WatchFace(
WatchFaceType.ANALOG,
userStyleRepository,
- complicationsManager,
- renderer
+ renderer,
+ complicationsManager
)
}
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index cedac02..bd084a8 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -623,8 +623,8 @@
return WatchFace(
WatchFaceType.ANALOG,
userStyleRepository,
- complicationsManager,
- renderer
+ renderer,
+ complicationsManager
)
}
}
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 6861812..d5ec784 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -139,8 +139,8 @@
return WatchFace(
WatchFaceType.ANALOG,
userStyleRepository,
- complicationsManager,
- renderer
+ renderer,
+ complicationsManager
).setLegacyWatchFaceStyle(
WatchFace.LegacyWatchFaceOverlayStyle(
0,
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index ab10e84..7b4bd98d 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -171,8 +171,8 @@
return WatchFace(
WatchFaceType.ANALOG,
userStyleRepository,
- complicationSlots,
- renderer
+ renderer,
+ complicationSlots
)
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 06a5fa1..1922e79 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -22,6 +22,7 @@
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.icu.util.Calendar
+import android.os.Bundle
import android.support.wearable.complications.ComplicationData
import androidx.annotation.ColorInt
import androidx.annotation.UiThread
@@ -34,6 +35,7 @@
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
/** Interface for rendering complications onto a [Canvas]. */
public interface CanvasComplication {
@@ -247,7 +249,10 @@
* The initial state of the complication. Note complications can be enabled / disabled by
* [UserStyleSetting.ComplicationsUserStyleSetting].
*/
- initiallyEnabled: Boolean
+ initiallyEnabled: Boolean,
+
+ /** Extras to be merged into the Intent sent when invoking the provider chooser activity. */
+ public val complicationConfigExtras: Bundle?
) {
public companion object {
internal val unitSquare = RectF(0f, 0f, 1f, 1f)
@@ -350,6 +355,7 @@
) {
private var defaultProviderType = ComplicationType.NOT_CONFIGURED
private var initiallyEnabled = true
+ private var complicationConfigExtras: Bundle? = null
/**
* Sets the initial [ComplicationType] to use with the initial complication provider.
@@ -363,11 +369,24 @@
return this
}
+ /**
+ * Whether the complication is initially enabled or not (by default its enabled). This can
+ * be overridden by [ComplicationsUserStyleSetting].
+ */
public fun setEnabled(enabled: Boolean): Builder {
this.initiallyEnabled = enabled
return this
}
+ /**
+ * Sets optional extras to be merged into the Intent sent when invoking the provider chooser
+ * activity.
+ */
+ public fun setComplicationConfigExtras(extras: Bundle?): Builder {
+ this.complicationConfigExtras = extras
+ return this
+ }
+
/** Constructs the [Complication]. */
public fun build(): Complication = Complication(
id,
@@ -377,7 +396,8 @@
supportedTypes,
defaultProviderPolicy,
defaultProviderType,
- initiallyEnabled
+ initiallyEnabled,
+ complicationConfigExtras
)
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index c895684..93b37af 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -21,6 +21,7 @@
import android.content.Context
import android.content.Intent
import android.icu.util.Calendar
+import android.os.Bundle
import android.support.wearable.watchface.accessibility.AccessibilityUtils
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import androidx.annotation.UiThread
@@ -279,12 +280,14 @@
* Called when new complication data is received.
*
* @param watchFaceComplicationId The id of the complication that the data relates to. This
- * will be an id that was previously sent in a call to [setActiveComplications].
+ * will be an id that was previously sent in a call to [setActiveComplications]. If this id
+ * is unrecognized the call will be a NOP, the only circumstance when that happens is if
+ * the watch face changes it's complication config between runs e.g. during development.
* @param data The [ComplicationData] that should be displayed in the complication.
*/
@UiThread
internal fun onComplicationDataUpdate(watchFaceComplicationId: Int, data: ComplicationData) {
- val complication = complications[watchFaceComplicationId]!!
+ val complication = complications[watchFaceComplicationId] ?: return
complication.dataDirty =
complication.dataDirty || (complication.renderer.idAndData?.complicationData != data)
complication.renderer.idAndData = IdAndComplicationData(watchFaceComplicationId, data)
@@ -394,7 +397,11 @@
getComponentName(watchFaceHostApi.getContext()),
complicationId,
complication.supportedTypes
- ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).apply {
+ complication.complicationConfigExtras?.let {
+ replaceExtras(Bundle(it).apply { putAll(extras!!) })
+ }
+ }
)
for (complicationListener in complicationListeners) {
complicationListener.onComplicationDoubleTapped(complicationId)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 9c62c0d..d2a98e8 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -105,7 +105,7 @@
* rates can also help preserve battery life, e.g. if a watch face has a short animation once
* per second it can adjust the frame rate inorder to sleep when not animating.
*/
- @IntRange(from = 0, to = 10000)
+ @IntRange(from = 0, to = 60000)
public var interactiveDrawModeUpdateDelayMillis: Long,
) {
internal lateinit var watchFaceHostApi: WatchFaceHostApi
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index f5a9ac1..0ab75a3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -113,7 +113,7 @@
* The return value of [WatchFaceService.createWatchFace] which brings together rendering, styling,
* complications and state observers.
*/
-public class WatchFace(
+public class WatchFace @JvmOverloads constructor(
/**
* The type of watch face, whether it's digital or analog. Used to determine the
* default time for editor preview screenshots.
@@ -123,11 +123,12 @@
/** The [UserStyleRepository] for this WatchFace. */
public val userStyleRepository: UserStyleRepository,
- /** The [ComplicationsManager] for this WatchFace. */
- internal var complicationsManager: ComplicationsManager,
-
/** The [Renderer] for this WatchFace. */
- internal val renderer: Renderer
+ internal val renderer: Renderer,
+
+ /** The [ComplicationsManager] for this WatchFace. */
+ internal var complicationsManager: ComplicationsManager =
+ ComplicationsManager(emptyList(), userStyleRepository)
) {
internal var tapListener: TapListener? = null
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
index b2e1896..dad0402 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
@@ -18,14 +18,12 @@
import android.content.ComponentName
import android.content.Context
-import android.graphics.Rect
import android.os.Handler
import android.support.wearable.complications.ComplicationData
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.wear.complications.SystemProviders
-import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.style.data.UserStyleWireFormat
/**
@@ -40,20 +38,9 @@
/** Returns the main thread [Handler]. */
public fun getHandler(): Handler
- /** Registers the watch face's current user style with the system. */
- public fun setCurrentUserStyle(userStyle: UserStyleWireFormat)
-
/** Returns the initial user style stored by the system if there is one or null otherwise. */
public fun getInitialUserStyle(): UserStyleWireFormat?
- /** Registers details of the complications with the system. */
- public fun setComplicationDetails(
- complicationId: Int,
- bounds: Rect,
- @ComplicationBoundsType boundsType: Int,
- types: IntArray
- )
-
/**
* Sets ContentDescriptionLabels for text-to-speech screen readers to make your
* complications, buttons, and any other text on your watchface accessible.
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index bb0f91f..d497ee6 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -60,7 +60,6 @@
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
-import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.ComplicationStateWireFormat
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
@@ -299,7 +298,7 @@
private val invalidateRunnable = Runnable(this::invalidate)
- // TODO(alexclarke): Figure out if we can remove this.
+ // State to support the old WSL style initialzation flow.
private var pendingBackgroundAction: Bundle? = null
private var pendingProperties: Bundle? = null
private var pendingSetWatchFaceStyle = false
@@ -1075,19 +1074,6 @@
Log.e(TAG, "Failed to set accessibility labels: ", e)
}
}
-
- override fun setCurrentUserStyle(userStyle: UserStyleWireFormat) {
- // TODO(alexclarke): Report programmatic style changes to WCS.
- }
-
- override fun setComplicationDetails(
- complicationId: Int,
- bounds: Rect,
- @ComplicationBoundsType boundsType: Int,
- types: IntArray
- ) {
- // TODO(alexclarke): Report programmatic complication details changes to WCS.
- }
}
}
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 7b3158c..ae550c7 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -95,8 +95,8 @@
) = WatchFace(
watchFaceType,
userStyleRepository,
- complicationsManager,
- renderer
+ renderer,
+ complicationsManager
).setSystemTimeProvider(object : WatchFace.SystemTimeProvider {
override fun getSystemTimeMillis(): Long {
return mockSystemTimeMillis
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 5520c4a..2805d19 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -1884,6 +1884,39 @@
}
@Test
+ fun updateInvalidCOmpliationIdDoesNotCrash() {
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ "interactiveInstanceId",
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ SystemState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null
+ )
+ )
+
+ // Send a complication with an invalid id - this should get ignored.
+ interactiveWatchFaceInstanceWCS.updateComplicationData(
+ listOf(
+ IdAndComplicationDataWireFormat(
+ RIGHT_COMPLICATION_ID,
+ ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+ .build()
+ )
+ )
+ )
+ }
+
+ @Test
fun invalidateRendererBeforeFullInit() {
renderer = TestRenderer(
surfaceHolder,
diff --git a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
index 448df50..3706add 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
@@ -142,7 +142,7 @@
mContext = context;
setSwipeDismissible(WearableNavigationHelper.isSwipeToDismissEnabled(context));
- setBackButtonDismissible(true);
+ setBackButtonDismissible(false);
}
/** Registers a callback for dismissal. */
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/ProxyControllerTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/ProxyControllerTest.java
index 571b9c4..c5009bf 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/ProxyControllerTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/ProxyControllerTest.java
@@ -147,6 +147,31 @@
/**
* This test should have an equivalent in CTS when this is implemented in the framework.
+ */
+ @Test
+ public void testReverseBypass() throws Exception {
+ WebkitUtils.checkFeature(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS);
+
+ final String contentUrl = "http://www.example.com";
+ final String bypassUrl = "www.example.com";
+ int proxyServerRequestCount = mProxyServer.getRequestCount();
+
+ // Set proxy override with reverse bypass and load content url
+ // The content url (in the bypass list) should use proxy settings.
+ setProxyOverrideSync(new ProxyConfig.Builder()
+ .addProxyRule(mProxyServer.getHostName() + ":" + mProxyServer.getPort())
+ .addBypassRule(bypassUrl)
+ .setReverseBypass(true)
+ .build());
+ mWebViewOnUiThread.loadUrl(contentUrl);
+
+ proxyServerRequestCount++;
+ assertNotNull(mProxyServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(proxyServerRequestCount, mProxyServer.getRequestCount());
+ }
+
+ /**
+ * This test should have an equivalent in CTS when this is implemented in the framework.
*
* Enumerates valid patterns to check they are supported.
*/
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ProxyConfig.java b/webkit/webkit/src/main/java/androidx/webkit/ProxyConfig.java
index 1b62fc9..3fc5e78 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ProxyConfig.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ProxyConfig.java
@@ -17,6 +17,7 @@
package androidx.webkit;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
@@ -68,14 +69,17 @@
private List<ProxyRule> mProxyRules;
private List<String> mBypassRules;
+ private boolean mReverseBypass;
/**
* @hide Internal use only
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public ProxyConfig(@NonNull List<ProxyRule> proxyRules, @NonNull List<String> bypassRules) {
+ public ProxyConfig(@NonNull List<ProxyRule> proxyRules, @NonNull List<String> bypassRules,
+ boolean reverseBypass) {
mProxyRules = proxyRules;
mBypassRules = bypassRules;
+ mReverseBypass = reverseBypass;
}
/**
@@ -106,6 +110,17 @@
}
/**
+ * @return reverseBypass
+ *
+ * TODO(laisminchillo): unhide this when we're ready to expose this
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public boolean isReverseBypass() {
+ return mReverseBypass;
+ }
+
+ /**
* Class that holds a scheme filter and a proxy URL.
*/
public static final class ProxyRule {
@@ -162,6 +177,7 @@
public static final class Builder {
private List<ProxyRule> mProxyRules;
private List<String> mBypassRules;
+ private boolean mReverseBypass = false;
/**
* Create an empty ProxyConfig Builder.
@@ -177,6 +193,7 @@
public Builder(@NonNull ProxyConfig proxyConfig) {
mProxyRules = proxyConfig.getProxyRules();
mBypassRules = proxyConfig.getBypassRules();
+ mReverseBypass = proxyConfig.isReverseBypass();
}
/**
@@ -186,7 +203,7 @@
*/
@NonNull
public ProxyConfig build() {
- return new ProxyConfig(proxyRules(), bypassRules());
+ return new ProxyConfig(proxyRules(), bypassRules(), reverseBypass());
}
/**
@@ -316,6 +333,28 @@
return addBypassRule(BYPASS_RULE_REMOVE_IMPLICIT);
}
+ /**
+ * Reverse the bypass list, so only URLs in the bypass list will use these proxy settings.
+ *
+ * <p>
+ * This method should only be called if
+ * {@link WebViewFeature#isFeatureSupported(String)}
+ * returns {@code true} for {@link WebViewFeature#PROXY_OVERRIDE_REVERSE_BYPASS}.
+ *
+ * @return This Builder object
+ *
+ * TODO(laisminchillo): unhide this when we're ready to expose this
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresFeature(name = WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ @NonNull
+ public Builder setReverseBypass(boolean reverseBypass) {
+ mReverseBypass = reverseBypass;
+ return this;
+ }
+
@NonNull
private List<ProxyRule> proxyRules() {
return mProxyRules;
@@ -325,5 +364,9 @@
private List<String> bypassRules() {
return mBypassRules;
}
+
+ private boolean reverseBypass() {
+ return mReverseBypass;
+ }
}
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 55916b9..1bb99540 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -93,6 +93,7 @@
FORCE_DARK_STRATEGY,
WEB_MESSAGE_LISTENER,
DOCUMENT_START_SCRIPT,
+ PROXY_OVERRIDE_REVERSE_BYPASS,
})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -454,6 +455,16 @@
public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
/**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers {@link androidx.webkit.ProxyConfig.Builder.setReverseBypass(boolean)}
+ *
+ * TODO(laisminchillo): unhide when ready.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
+
+ /**
* Return whether a feature is supported at run-time. On devices running Android version {@link
* android.os.Build.VERSION_CODES#LOLLIPOP} and higher, this will check whether a feature is
* supported, depending on the combination of the desired feature, the Android version of
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProxyControllerImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProxyControllerImpl.java
index 36b5074a..5d7174f 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProxyControllerImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProxyControllerImpl.java
@@ -36,13 +36,23 @@
@Override
public void setProxyOverride(@NonNull ProxyConfig proxyConfig, @NonNull Executor executor,
@NonNull Runnable listener) {
- WebViewFeatureInternal feature = WebViewFeatureInternal.PROXY_OVERRIDE;
- if (feature.isSupportedByWebView()) {
- // A 2D String array representation is required by reflection
- String[][] proxyRuleArray = proxyRulesToStringArray(proxyConfig.getProxyRules());
- String[] bypassRuleArray = proxyConfig.getBypassRules().toArray(new String[0]);
+ WebViewFeatureInternal proxyOverride = WebViewFeatureInternal.PROXY_OVERRIDE;
+ WebViewFeatureInternal reverseBypass = WebViewFeatureInternal.PROXY_OVERRIDE_REVERSE_BYPASS;
+
+ // A 2D String array representation is required by reflection
+ String[][] proxyRuleArray = proxyRulesToStringArray(proxyConfig.getProxyRules());
+ String[] bypassRuleArray = proxyConfig.getBypassRules().toArray(new String[0]);
+
+ if (proxyOverride.isSupportedByWebView() && !proxyConfig.isReverseBypass()) {
getBoundaryInterface().setProxyOverride(
proxyRuleArray, bypassRuleArray, listener, executor);
+ } else if (proxyOverride.isSupportedByWebView() && reverseBypass.isSupportedByWebView()) {
+ getBoundaryInterface().setProxyOverride(
+ proxyRuleArray,
+ bypassRuleArray,
+ listener,
+ executor,
+ proxyConfig.isReverseBypass());
} else {
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index 2bec18d..7ff7120 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -402,6 +402,12 @@
*/
DOCUMENT_START_SCRIPT(WebViewFeature.DOCUMENT_START_SCRIPT, Features.DOCUMENT_START_SCRIPT),
+ /**
+ * This feature covers {@link androidx.webkit.ProxyConfig.Builder.setReverseBypass(boolean)}
+ */
+ PROXY_OVERRIDE_REVERSE_BYPASS(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS,
+ Features.PROXY_OVERRIDE_REVERSE_BYPASS),
+
; // This semicolon ends the enum. Add new features with a trailing comma above this line.
private static final int NOT_SUPPORTED_BY_FRAMEWORK = -1;