Merge "Adding functionality to handle proguard when storing enums in databases." into androidx-master-dev
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 7fe96b8..4175155 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -30,16 +30,16 @@
api("androidx.core:core-ktx:1.1.0") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01") {
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
- api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+ api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
api(KOTLIN_STDLIB)
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(JUNIT)
androidTestImplementation(TRUTH)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b0d8662..e683bb6 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,12 +23,12 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api("androidx.core:core:1.1.0")
- api("androidx.lifecycle:lifecycle-runtime:2.3.0-beta01")
- api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
- api("androidx.savedstate:savedstate:1.1.0-beta01")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(projectOrArtifact(":savedstate:savedstate"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(KOTLIN_STDLIB)
androidTestImplementation(LEAKCANARY)
androidTestImplementation(LEAKCANARY_INSTRUMENTATION)
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -98,6 +98,7 @@
public interface AppSearchSession {
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -98,6 +98,7 @@
public interface AppSearchSession {
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -98,6 +98,7 @@
public interface AppSearchSession {
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
new file mode 100644
index 0000000..99ac18e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.appsearch.app;
+
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.annotation.AppSearchDocument;
+
+import org.junit.Test;
+
+public class SetSchemaRequestTest {
+
+ @AppSearchDocument
+ static class Card {
+ @AppSearchDocument.Uri
+ String mUri;
+ @AppSearchDocument.Property
+ (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+ String mString;
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AnnotationProcessorTest.Card)) {
+ return false;
+ }
+ AnnotationProcessorTest.Card otherCard = (AnnotationProcessorTest.Card) other;
+ assertThat(otherCard.mUri).isEqualTo(this.mUri);
+ return true;
+ }
+ }
+
+ @Test
+ public void testInvalidSchemaReferences() {
+ IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+ () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(false,
+ "InvalidSchema").build());
+ assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForSystemUi_Visible() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ // By default, the schema is visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchema(schema).build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+
+ request =
+ new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
+ true,
+ "Schema").build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForSystemUi_NotVisible() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
+ false,
+ "Schema").build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Schema");
+ }
+
+ @Test
+ public void testDataClassVisibilityForSystemUi_Visible() throws Exception {
+ // By default, the schema is visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+
+ request =
+ new SetSchemaRequest.Builder().addDataClass(
+ Card.class).setDataClassVisibilityForSystemUi(
+ true,
+ Card.class).build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+ }
+
+ @Test
+ public void testDataClassVisibilityForSystemUi_NotVisible() throws Exception {
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(
+ Card.class).setDataClassVisibilityForSystemUi(
+ false,
+ Card.class).build();
+ assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Card");
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
index ba04b58..7a646b0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
@@ -72,4 +72,105 @@
.build()));
assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
}
+
+ @Test
+ public void testEquals_identical() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ assertThat(schema1).isEqualTo(schema2);
+ assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+ }
+
+ @Test
+ public void testEquals_differentOrder() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build()
+ ).build();
+ assertThat(schema1).isEqualTo(schema2);
+ assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+ }
+
+ @Test
+ public void testEquals_failure() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS) // Different
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ assertThat(schema1).isNotEqualTo(schema2);
+ assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+ }
+
+ @Test
+ public void testEquals_failure_differentOrder() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new PropertyConfig.Builder("body")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ // Order of 'body' and 'subject' has been switched
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+ .addProperty(new PropertyConfig.Builder("body")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ assertThat(schema1).isNotEqualTo(schema2);
+ assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 9586204..91af24c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -99,6 +99,52 @@
}
@Test
+ public void testGetSchema() throws Exception {
+ AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS) // Different
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+ .setDataType(PropertyConfig.DATA_TYPE_STRING)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS) // Different
+ .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ SetSchemaRequest request1 = new SetSchemaRequest.Builder()
+ .addSchema(emailSchema1).addDataClass(EmailDataClass.class).build();
+ SetSchemaRequest request2 = new SetSchemaRequest.Builder()
+ .addSchema(emailSchema2).addDataClass(EmailDataClass.class).build();
+
+ checkIsResultSuccess(mDb1.setSchema(request1));
+ checkIsResultSuccess(mDb2.setSchema(request2));
+
+ Set<AppSearchSchema> actual1 = checkIsResultSuccess(mDb1.getSchema());
+ Set<AppSearchSchema> actual2 = checkIsResultSuccess(mDb2.getSchema());
+
+ assertThat(actual1).isEqualTo(request1.getSchemas());
+ assertThat(actual2).isEqualTo(request2.getSchemas());
+ }
+
+ @Test
public void testPutDocuments() throws Exception {
// Schema registration
checkIsResultSuccess(mDb1.setSchema(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
new file mode 100644
index 0000000..3830b9d
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.appsearch.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.util.Size;
+import android.util.SizeF;
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.UUID;
+
+public class BundleUtilTest {
+ @Test
+ public void testDeepEquals_self() {
+ Bundle one = new Bundle();
+ one.putString("a", "a");
+ assertThat(one).isEqualTo(one);
+ assertThat(BundleUtil.deepEquals(one, one)).isTrue();
+ }
+
+ @Test
+ public void testDeepEquals_simple() {
+ Bundle one = new Bundle();
+ one.putString("a", "a");
+
+ Bundle two = new Bundle();
+ two.putString("a", "a");
+
+ assertThat(one).isNotEqualTo(two);
+ assertThat(BundleUtil.deepEquals(one, two)).isTrue();
+ }
+
+ @Test
+ public void testDeepEquals_keyMismatch() {
+ Bundle one = new Bundle();
+ one.putString("a", "a");
+
+ Bundle two = new Bundle();
+ two.putString("a", "a");
+ two.putString("b", "b");
+ assertThat(BundleUtil.deepEquals(one, two)).isFalse();
+ }
+
+ @Test
+ public void testDeepEquals_thorough_equal() {
+ Bundle[] inputs = new Bundle[2];
+ for (int i = 0; i < 2; i++) {
+ inputs[i] = createThoroughBundle();
+ }
+ assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+ assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isTrue();
+ }
+
+ @Test
+ public void testDeepEquals_thorough_notEqual() {
+ Bundle[] inputs = new Bundle[2];
+ for (int i = 0; i < 2; i++) {
+ Bundle b = createThoroughBundle();
+ // Create a difference
+ assertThat(b.containsKey("doubleArray")).isTrue();
+ b.putDoubleArray("doubleArray", new double[]{18., i});
+ inputs[i] = b;
+ }
+ assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+ assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isFalse();
+ }
+
+ @Test
+ public void testDeepEquals_nestedNotEquals() {
+ Bundle one = new Bundle();
+ one.putString("a", "a");
+ Bundle two = new Bundle();
+ two.putBundle("b", one);
+ Bundle twoClone = new Bundle();
+ twoClone.putBundle("b", one);
+ Bundle three = new Bundle();
+ three.putBundle("b", two);
+
+ ArrayList<Bundle> listOne = new ArrayList<>(ImmutableList.of(one, two, three));
+ ArrayList<Bundle> listOneClone = new ArrayList<>(ImmutableList.of(one, twoClone, three));
+ ArrayList<Bundle> listTwo = new ArrayList<>(ImmutableList.of(one, three, two));
+ Bundle b1 = new Bundle();
+ b1.putParcelableArrayList("key", listOne);
+ Bundle b1Clone = new Bundle();
+ b1Clone.putParcelableArrayList("key", listOneClone);
+ Bundle b2 = new Bundle();
+ b2.putParcelableArrayList("key", listTwo);
+
+ assertThat(b1).isNotEqualTo(b1Clone);
+ assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+ assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+ assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+ }
+
+ @Test
+ public void testDeepEquals_sparseArray() {
+ Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+ Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+ Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+ SparseArray<Parcelable> array1 = new SparseArray<>();
+ array1.put(1, parcelable1);
+ array1.put(10, parcelable2);
+
+ SparseArray<Parcelable> array1Clone = new SparseArray<>();
+ array1Clone.put(1, parcelable1);
+ array1Clone.put(10, parcelable2);
+
+ SparseArray<Parcelable> array2 = new SparseArray<>();
+ array2.put(1, parcelable1);
+ array2.put(10, parcelable3); // Different
+
+ Bundle b1 = new Bundle();
+ b1.putSparseParcelableArray("array1", array1);
+ Bundle b1Clone = new Bundle();
+ b1Clone.putSparseParcelableArray("array1", array1Clone);
+ Bundle b2 = new Bundle();
+ b2.putSparseParcelableArray("array1", array2);
+
+ assertThat(b1).isNotEqualTo(b1Clone);
+ assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+ assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+ assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+ }
+
+ @Test
+ public void testDeepHashCode_same() {
+ Bundle[] inputs = new Bundle[2];
+ for (int i = 0; i < 2; i++) {
+ inputs[i] = createThoroughBundle();
+ }
+ assertThat(BundleUtil.deepHashCode(inputs[0]))
+ .isEqualTo(BundleUtil.deepHashCode(inputs[1]));
+ }
+
+ @Test
+ public void testDeepHashCode_different() {
+ Bundle[] inputs = new Bundle[2];
+ for (int i = 0; i < 2; i++) {
+ Bundle b = createThoroughBundle();
+ // Create a difference
+ assertThat(b.containsKey("doubleArray")).isTrue();
+ b.putDoubleArray("doubleArray", new double[]{18., i});
+ inputs[i] = b;
+ }
+ assertThat(BundleUtil.deepHashCode(inputs[0]))
+ .isNotEqualTo(BundleUtil.deepHashCode(inputs[1]));
+ }
+
+ @Test
+ public void testHashCode_sparseArray() {
+ Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+ Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+ Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+ SparseArray<Parcelable> array1 = new SparseArray<>();
+ array1.put(1, parcelable1);
+ array1.put(10, parcelable2);
+
+ SparseArray<Parcelable> array1Clone = new SparseArray<>();
+ array1Clone.put(1, parcelable1);
+ array1Clone.put(10, parcelable2);
+
+ SparseArray<Parcelable> array2 = new SparseArray<>();
+ array2.put(1, parcelable1);
+ array2.put(10, parcelable3); // Different
+
+ Bundle b1 = new Bundle();
+ b1.putSparseParcelableArray("array1", array1);
+ Bundle b1Clone = new Bundle();
+ b1Clone.putSparseParcelableArray("array1", array1Clone);
+ Bundle b2 = new Bundle();
+ b2.putSparseParcelableArray("array1", array2);
+
+ assertThat(b1.hashCode()).isNotEqualTo(b1Clone.hashCode());
+ assertThat(BundleUtil.deepHashCode(b1)).isEqualTo(BundleUtil.deepHashCode(b1Clone));
+ assertThat(BundleUtil.deepHashCode(b1)).isNotEqualTo(BundleUtil.deepHashCode(b2));
+ }
+
+ private static Bundle createThoroughBundle() {
+ Bundle toy1 = new Bundle();
+ toy1.putString("a", "a");
+ Bundle toy2 = new Bundle();
+ toy2.putInt("b", 2);
+
+ Bundle b = new Bundle();
+ // BaseBundle stuff
+ b.putBoolean("boolean", true);
+ b.putByte("byte", (byte) 1);
+ b.putChar("char", 'a');
+ b.putShort("short", (short) 2);
+ b.putInt("int", 3);
+ b.putLong("long", 4L);
+ b.putFloat("float", 5f);
+ b.putDouble("double", 6f);
+ b.putString("string", "b");
+ b.putCharSequence("charSequence", "c");
+ b.putIntegerArrayList("integerArrayList", new ArrayList<>(ImmutableList.of(7, 8)));
+ b.putStringArrayList("stringArrayList", new ArrayList<>(ImmutableList.of("d", "e")));
+ b.putCharSequenceArrayList(
+ "charSequenceArrayList", new ArrayList<>(ImmutableList.of("f", "g")));
+ b.putSerializable("serializable", new BigDecimal(9));
+ b.putBooleanArray("booleanArray", new boolean[]{true, false, true});
+ b.putByteArray("byteArray", new byte[]{(byte) 10, (byte) 11});
+ b.putShortArray("shortArray", new short[]{(short) 12, (short) 13});
+ b.putCharArray("charArray", new char[]{'h', 'i'});
+ b.putLongArray("longArray", new long[]{14L, 15L});
+ b.putFloatArray("floatArray", new float[]{16f, 17f});
+ b.putDoubleArray("doubleArray", new double[]{18., 19.});
+ b.putStringArray("stringArray", new String[]{"j", "k"});
+ b.putCharSequenceArray("charSequenceArrayList", new CharSequence[]{"l", "m"});
+
+ // Bundle stuff
+ b.putParcelable("parcelable", toy1);
+ if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ b.putSize("size", new Size(20, 21));
+ b.putSizeF("sizeF", new SizeF(22f, 23f));
+ }
+ b.putParcelableArray("parcelableArray", new Parcelable[]{toy1, toy2});
+ b.putParcelableArrayList(
+ "parcelableArrayList", new ArrayList<>(ImmutableList.of(toy1, toy2)));
+ SparseArray<Parcelable> sparseArray = new SparseArray<>();
+ sparseArray.put(24, toy1);
+ sparseArray.put(1025, toy2);
+ b.putSparseParcelableArray("sparceParcelableArray", sparseArray);
+ b.putBundle("bundle", toy1);
+
+ return b;
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 9d0460a..11b587b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -24,7 +24,9 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.appsearch.util.BundleUtil;
import androidx.collection.ArraySet;
+import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
@@ -96,17 +98,37 @@
return ret;
}
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AppSearchSchema)) {
+ return false;
+ }
+ AppSearchSchema otherSchema = (AppSearchSchema) other;
+ if (!getSchemaType().equals(otherSchema.getSchemaType())) {
+ return false;
+ }
+ return getProperties().equals(otherSchema.getProperties());
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(getSchemaType(), getProperties());
+ }
+
/** Builder for {@link AppSearchSchema objects}. */
public static final class Builder {
- private final String mTypeName;
+ private final String mSchemaType;
private final ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
private final Set<String> mPropertyNames = new ArraySet<>();
private boolean mBuilt = false;
/** Creates a new {@link AppSearchSchema.Builder}. */
- public Builder(@NonNull String typeName) {
- Preconditions.checkNotNull(typeName);
- mTypeName = typeName;
+ public Builder(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ mSchemaType = schemaType;
}
/** Adds a property to the given type. */
@@ -135,7 +157,7 @@
public AppSearchSchema build() {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Bundle bundle = new Bundle();
- bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mTypeName);
+ bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
mBuilt = true;
return new AppSearchSchema(bundle);
@@ -273,6 +295,9 @@
final Bundle mBundle;
+ @Nullable
+ private Integer mHashCode;
+
PropertyConfig(@NonNull Bundle bundle) {
mBundle = Preconditions.checkNotNull(bundle);
}
@@ -321,6 +346,26 @@
return mBundle.getInt(TOKENIZER_TYPE_FIELD);
}
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PropertyConfig)) {
+ return false;
+ }
+ PropertyConfig otherProperty = (PropertyConfig) other;
+ return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = BundleUtil.deepHashCode(mBundle);
+ }
+ return mHashCode;
+ }
+
/**
* Builder for {@link PropertyConfig}.
*
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 671aa1d..311b515 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -16,11 +16,14 @@
// @exportToFramework:skipFile()
package androidx.appsearch.app;
+import android.annotation.SuppressLint;
+
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Set;
+
/**
* Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be
* placed and queried.
@@ -74,37 +77,29 @@
* <p>It is a no-op to set the same schema as has been previously set; this is handled
* efficiently.
*
+ * <p>By default, documents are visible on platform surfaces. To opt out, call {@code
+ * SetSchemaRequest.Builder#setPlatformSurfaceable} with {@code surfaceable} as false. Any
+ * visibility settings apply only to the schemas that are included in the {@code request}.
+ * Visibility settings for a schema type do not apply or persist across
+ * {@link SetSchemaRequest}s.
+ *
* @param request The schema update request.
* @return The pending result of performing this operation.
*/
+ // TODO(b/169883602): Change @code references to @link when setPlatformSurfaceable APIs are
+ // exposed.
@NonNull
ListenableFuture<AppSearchResult<Void>> setSchema(@NonNull SetSchemaRequest request);
/**
- * Sets visibility settings for documents in AppSearch.
+ * Retrieves the schema most recently successfully provided to {@link #setSchema}.
*
- * <p>Visibility settings are not carried over from previous {@code SetVisibilityRequest}s.
- * The entire set of visibility settings must be specified on each {@code SetVisibilityRequest}.
- *
- * <p>The visibility settings apply to the schema instance that currently exists. If a schema
- * is deleted and then re-added, the visibility setting will no longer apply to the new
- * instance of the schema.
- *
- * <p>An {@link AppSearchResult#RESULT_NOT_FOUND} will be returned if a specified schema
- * doesn't exist.
- *
- * <p>The default visibility settings are that all documents can be shown on platform
- * surfaces. Documents can be opted out of being shown on platform surfaces by specifying
- * their schema type in {@link SetVisibilityRequest.Builder#addHiddenFromPlatformSurfaces}.
- *
- * @param request The visibility settings request
* @return The pending result of performing this operation.
- * @hide
*/
- // TODO(b/169883602): Add integration tests for this API in the platform.
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ @SuppressLint("KotlinPropertyAccess")
@NonNull
- ListenableFuture<AppSearchResult<Void>> setVisibility(@NonNull SetVisibilityRequest request);
+ ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema();
/**
* Indexes documents into AppSearch.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index ca34d32..28357af 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -25,6 +25,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.util.BundleUtil;
import androidx.core.util.Preconditions;
import java.lang.reflect.Array;
@@ -43,7 +44,7 @@
* @see AppSearchSession#query
*/
public class GenericDocument {
- private static final String TAG = "GenericDocument";
+ private static final String TAG = "AppSearchGenericDocumen";
/** The default empty namespace.*/
public static final String DEFAULT_NAMESPACE = "";
@@ -480,142 +481,17 @@
return false;
}
GenericDocument otherDocument = (GenericDocument) other;
- return bundleEquals(this.mBundle, otherDocument.mBundle);
- }
-
- /**
- * Deeply checks whether two bundles are equal.
- * <p>Two bundles will be considered equal if they contain the same content.
- */
- @SuppressWarnings("unchecked")
- private static boolean bundleEquals(Bundle one, Bundle two) {
- if (one.size() != two.size()) {
- return false;
- }
- Set<String> keySetOne = one.keySet();
- Object valueOne;
- Object valueTwo;
- // Bundle inherit its equals() from Object.java, which only compare their memory address.
- // We should iterate all keys and check their presents and values in both bundle.
- for (String key : keySetOne) {
- valueOne = one.get(key);
- valueTwo = two.get(key);
- if (valueOne instanceof Bundle
- && valueTwo instanceof Bundle
- && !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) {
- return false;
- } else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) {
- // If we call bundle.get(key) when the 'key' doesn't actually exist in the
- // bundle, we'll get back a null. So make sure that both values are null and
- // both keys exist in the bundle.
- return false;
- } else if (valueOne instanceof boolean[]) {
- if (!(valueTwo instanceof boolean[])
- || !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) {
- return false;
- }
- } else if (valueOne instanceof long[]) {
- if (!(valueTwo instanceof long[])
- || !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) {
- return false;
- }
- } else if (valueOne instanceof double[]) {
- if (!(valueTwo instanceof double[])
- || !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) {
- return false;
- }
- } else if (valueOne instanceof Bundle[]) {
- if (!(valueTwo instanceof Bundle[])) {
- return false;
- }
- Bundle[] bundlesOne = (Bundle[]) valueOne;
- Bundle[] bundlesTwo = (Bundle[]) valueTwo;
- if (bundlesOne.length != bundlesTwo.length) {
- return false;
- }
- for (int i = 0; i < bundlesOne.length; i++) {
- if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) {
- return false;
- }
- }
- } else if (valueOne instanceof ArrayList) {
- if (!(valueTwo instanceof ArrayList)) {
- return false;
- }
- ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne;
- ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo;
- if (bundlesOne.size() != bundlesTwo.size()) {
- return false;
- }
- for (int i = 0; i < bundlesOne.size(); i++) {
- if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) {
- return false;
- }
- }
- } else if (valueOne instanceof Object[]) {
- if (!(valueTwo instanceof Object[])
- || !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) {
- return false;
- }
- }
- }
- return true;
+ return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
}
@Override
public int hashCode() {
if (mHashCode == null) {
- mHashCode = bundleHashCode(mBundle);
+ mHashCode = BundleUtil.deepHashCode(mBundle);
}
return mHashCode;
}
- /**
- * Calculates the hash code for a bundle.
- * <p> The hash code is only effected by the contents in the bundle. Bundles will get
- * consistent hash code if they have same contents.
- */
- @SuppressWarnings("unchecked")
- private static int bundleHashCode(Bundle bundle) {
- int[] hashCodes = new int[bundle.size()];
- int i = 0;
- // Bundle inherit its hashCode() from Object.java, which only relative to their memory
- // address. Bundle doesn't have an order, so we should iterate all keys and combine
- // their value's hashcode into an array. And use the hashcode of the array to be
- // the hashcode of the bundle.
- for (String key : bundle.keySet()) {
- Object value = bundle.get(key);
- if (value instanceof boolean[]) {
- hashCodes[i++] = Arrays.hashCode((boolean[]) value);
- } else if (value instanceof long[]) {
- hashCodes[i++] = Arrays.hashCode((long[]) value);
- } else if (value instanceof double[]) {
- hashCodes[i++] = Arrays.hashCode((double[]) value);
- } else if (value instanceof String[]) {
- hashCodes[i++] = Arrays.hashCode((Object[]) value);
- } else if (value instanceof Bundle) {
- hashCodes[i++] = bundleHashCode((Bundle) value);
- } else if (value instanceof Bundle[]) {
- Bundle[] bundles = (Bundle[]) value;
- int[] innerHashCodes = new int[bundles.length];
- for (int j = 0; j < innerHashCodes.length; j++) {
- innerHashCodes[j] = bundleHashCode(bundles[j]);
- }
- hashCodes[i++] = Arrays.hashCode(innerHashCodes);
- } else if (value instanceof ArrayList) {
- ArrayList<Bundle> bundles = (ArrayList<Bundle>) value;
- int[] innerHashCodes = new int[bundles.size()];
- for (int j = 0; j < innerHashCodes.length; j++) {
- innerHashCodes[j] = bundleHashCode(bundles.get(j));
- }
- hashCodes[i++] = Arrays.hashCode(innerHashCodes);
- } else {
- hashCodes[i++] = value.hashCode();
- }
- }
- return Arrays.hashCode(hashCodes);
- }
-
@Override
@NonNull
public String toString() {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 2f8337c..a918106 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -19,6 +19,7 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -26,6 +27,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -36,17 +38,31 @@
*/
public final class SetSchemaRequest {
private final Set<AppSearchSchema> mSchemas;
+ private final Set<String> mSchemasNotPlatformSurfaceable;
private final boolean mForceOverride;
- SetSchemaRequest(Set<AppSearchSchema> schemas, boolean forceOverride) {
- mSchemas = schemas;
+ SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
+ @NonNull Set<String> schemasNotPlatformSurfaceable, boolean forceOverride) {
+ mSchemas = Preconditions.checkNotNull(schemas);
+ mSchemasNotPlatformSurfaceable = Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
mForceOverride = forceOverride;
}
/** Returns the schemas that are part of this request. */
@NonNull
public Set<AppSearchSchema> getSchemas() {
- return mSchemas;
+ return Collections.unmodifiableSet(mSchemas);
+ }
+
+ /**
+ * Returns the set of schema types that have opted out of being visible on system UI surfaces.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Set<String> getSchemasNotPlatformSurfaceable() {
+ return Collections.unmodifiableSet(mSchemasNotPlatformSurfaceable);
}
/** Returns whether this request will force the schema to be overridden. */
@@ -57,17 +73,26 @@
/** Builder for {@link SetSchemaRequest} objects. */
public static final class Builder {
private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
+ private final Set<String> mSchemasNotPlatformSurfaceable = new ArraySet<>();
private boolean mForceOverride = false;
private boolean mBuilt = false;
- /** Adds one or more types to the schema. */
+ /**
+ * Adds one or more types to the schema.
+ *
+ * <p>Any documents of these types will be visible on system UI surfaces by default.
+ */
@NonNull
public Builder addSchema(@NonNull AppSearchSchema... schemas) {
Preconditions.checkNotNull(schemas);
return addSchema(Arrays.asList(schemas));
}
- /** Adds one or more types to the schema. */
+ /**
+ * Adds one or more types to the schema.
+ *
+ * <p>Any documents of these types will be visible on system UI surfaces by default.
+ */
@NonNull
public Builder addSchema(@NonNull Collection<AppSearchSchema> schemas) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
@@ -79,6 +104,8 @@
/**
* Adds one or more types to the schema.
*
+ * <p>Any documents of these types will be visible on system UI surfaces by default.
+ *
* @param dataClasses classes annotated with
* {@link androidx.appsearch.annotation.AppSearchDocument}.
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
@@ -95,6 +122,8 @@
/**
* Adds one or more types to the schema.
*
+ * <p>Any documents of these types will be visible on system UI surfaces by default.
+ *
* @param dataClasses classes annotated with
* {@link androidx.appsearch.annotation.AppSearchDocument}.
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
@@ -116,6 +145,78 @@
}
/**
+ * Sets visibility on system UI surfaces for schema types.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
+ @NonNull String... schemaTypes) {
+ Preconditions.checkNotNull(schemaTypes);
+ return this.setSchemaTypeVisibilityForSystemUi(visible, Arrays.asList(schemaTypes));
+ }
+
+ /**
+ * Sets visibility on system UI surfaces for schema types.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
+ @NonNull Collection<String> schemaTypes) {
+ Preconditions.checkState(!mBuilt, "Builder has already been used");
+ Preconditions.checkNotNull(schemaTypes);
+ if (visible) {
+ mSchemasNotPlatformSurfaceable.removeAll(schemaTypes);
+ } else {
+ mSchemasNotPlatformSurfaceable.addAll(schemaTypes);
+ }
+ return this;
+ }
+
+ /**
+ * Sets visibility on system UI surfaces for schema types.
+ *
+ * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+ * has not generated a schema for the given data classes.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setDataClassVisibilityForSystemUi(boolean visible,
+ @NonNull Class<?>... dataClasses) throws AppSearchException {
+ Preconditions.checkNotNull(dataClasses);
+ return setDataClassVisibilityForSystemUi(visible, Arrays.asList(dataClasses));
+ }
+
+ /**
+ * Sets visibility on system UI surfaces for schema types.
+ *
+ * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+ * has not generated a schema for the given data classes.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setDataClassVisibilityForSystemUi(boolean visible,
+ @NonNull Collection<Class<?>> dataClasses) throws AppSearchException {
+ Preconditions.checkState(!mBuilt, "Builder has already been used");
+ Preconditions.checkNotNull(dataClasses);
+ DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+ for (Class<?> dataClass : dataClasses) {
+ DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+ if (visible) {
+ mSchemasNotPlatformSurfaceable.remove(factory.getSchemaType());
+ } else {
+ mSchemasNotPlatformSurfaceable.add(factory.getSchemaType());
+ }
+ }
+ return this;
+ }
+
+ /**
* Configures the {@link SetSchemaRequest} to delete any existing documents that don't
* follow the new schema.
*
@@ -130,12 +231,34 @@
return this;
}
- /** Builds a new {@link SetSchemaRequest}. */
+ /**
+ * Builds a new {@link SetSchemaRequest}.
+ *
+ * @throws IllegalArgumentException If schema types were referenced, but the
+ * corresponding {@link AppSearchSchema} was never added.
+ */
@NonNull
public SetSchemaRequest build() {
Preconditions.checkState(!mBuilt, "Builder has already been used");
mBuilt = true;
- return new SetSchemaRequest(mSchemas, mForceOverride);
+
+ // Verify that any schema types with visibility settings refer to a real schema.
+ // Create a copy because we're going to remove from the set for verification purposes.
+ Set<String> schemasNotPlatformSurfaceableCopy = new ArraySet<>(
+ mSchemasNotPlatformSurfaceable);
+ for (AppSearchSchema schema : mSchemas) {
+ schemasNotPlatformSurfaceableCopy.remove(schema.getSchemaType());
+ }
+ if (!schemasNotPlatformSurfaceableCopy.isEmpty()) {
+ // We still have schema types that weren't seen in our mSchemas set. This means
+ // there wasn't a corresponding AppSearchSchema.
+ throw new IllegalArgumentException(
+ "Schema types " + schemasNotPlatformSurfaceableCopy
+ + " referenced, but were not added.");
+ }
+
+ return new SetSchemaRequest(mSchemas, mSchemasNotPlatformSurfaceable,
+ mForceOverride);
}
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java
deleted file mode 100644
index 291a735..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java
+++ /dev/null
@@ -1,78 +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.appsearch.app;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Set;
-
-/**
- * Encapsulates a request to update the visibility settings of an {@link AppSearchSession} database.
- *
- * @see AppSearchSession#setVisibility
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class SetVisibilityRequest {
- private final Set<AppSearchSchema> mSchemasHiddenFromPlatformSurfaces;
-
- SetVisibilityRequest(Set<AppSearchSchema> schemasHiddenFromPlatformSurfaces) {
- mSchemasHiddenFromPlatformSurfaces = schemasHiddenFromPlatformSurfaces;
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Set<AppSearchSchema> getSchemasHiddenFromPlatformSurfaces() {
- return mSchemasHiddenFromPlatformSurfaces;
- }
-
- /** Builder for {@link SetVisibilityRequest} objects. */
- public static final class Builder {
- private final Set<AppSearchSchema> mSchemasHiddenFromPlatformSurfaces = new ArraySet<>();
- private boolean mBuilt = false;
-
- /** Set documents of type {@code schemas} to be hidden from platform surfaces. */
- @NonNull
- public Builder addHiddenFromPlatformSurfaces(@NonNull AppSearchSchema... schemas) {
- Preconditions.checkNotNull(schemas);
- return addHiddenFromPlatformSurfaces(Arrays.asList(schemas));
- }
-
- /** Set documents of type {@code schemas} to be hidden from platform surfaces. */
- @NonNull
- public Builder addHiddenFromPlatformSurfaces(@NonNull Collection<AppSearchSchema> schemas) {
- Preconditions.checkState(!mBuilt, "Builder has already been used");
- Preconditions.checkNotNull(schemas);
- mSchemasHiddenFromPlatformSurfaces.addAll(schemas);
- return this;
- }
-
- /** Builds a new {@link SetVisibilityRequest}. */
- @NonNull
- public SetVisibilityRequest build() {
- Preconditions.checkState(!mBuilt, "Builder has already been used");
- mBuilt = true;
- return new SetVisibilityRequest(mSchemasHiddenFromPlatformSurfaces);
- }
- }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
new file mode 100644
index 0000000..86de233
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -0,0 +1,222 @@
+/*
+ * 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.appsearch.util;
+
+import android.os.Bundle;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Utilities for working with {@link android.os.Bundle}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class BundleUtil {
+ private BundleUtil() {}
+
+ /**
+ * Deeply checks two bundles are equal or not.
+ *
+ * <p>Two bundles will be considered equal if they contain the same keys, and each value is also
+ * equal. Bundle values are compared using deepEquals.
+ */
+ public static boolean deepEquals(@Nullable Bundle one, @Nullable Bundle two) {
+ if (one == null && two == null) {
+ return true;
+ }
+ if (one == null || two == null) {
+ return false;
+ }
+ if (one.size() != two.size()) {
+ return false;
+ }
+ if (!one.keySet().equals(two.keySet())) {
+ return false;
+ }
+ // Bundle inherit its equals() from Object.java, which only compare their memory address.
+ // We should iterate all keys and check their presents and values in both bundle.
+ for (String key : one.keySet()) {
+ if (!bundleValueEquals(one.get(key), two.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Deeply checks whether two values in a Bundle are equal or not.
+ *
+ * <p>Values of type Bundle are compared using {@link #deepEquals}.
+ */
+ private static boolean bundleValueEquals(@Nullable Object one, @Nullable Object two) {
+ if (one == null && two == null) {
+ return true;
+ }
+ if (one == null || two == null) {
+ return false;
+ }
+ if (one.equals(two)) {
+ return true;
+ }
+ if (one instanceof Bundle && two instanceof Bundle) {
+ return deepEquals((Bundle) one, (Bundle) two);
+ } else if (one instanceof int[] && two instanceof int[]) {
+ return Arrays.equals((int[]) one, (int[]) two);
+ } else if (one instanceof byte[] && two instanceof byte[]) {
+ return Arrays.equals((byte[]) one, (byte[]) two);
+ } else if (one instanceof char[] && two instanceof char[]) {
+ return Arrays.equals((char[]) one, (char[]) two);
+ } else if (one instanceof long[] && two instanceof long[]) {
+ return Arrays.equals((long[]) one, (long[]) two);
+ } else if (one instanceof float[] && two instanceof float[]) {
+ return Arrays.equals((float[]) one, (float[]) two);
+ } else if (one instanceof short[] && two instanceof short[]) {
+ return Arrays.equals((short[]) one, (short[]) two);
+ } else if (one instanceof double[] && two instanceof double[]) {
+ return Arrays.equals((double[]) one, (double[]) two);
+ } else if (one instanceof boolean[] && two instanceof boolean[]) {
+ return Arrays.equals((boolean[]) one, (boolean[]) two);
+ } else if (one instanceof Object[] && two instanceof Object[]) {
+ Object[] arrayOne = (Object[]) one;
+ Object[] arrayTwo = (Object[]) two;
+ if (arrayOne.length != arrayTwo.length) {
+ return false;
+ }
+ if (Arrays.equals(arrayOne, arrayTwo)) {
+ return true;
+ }
+ for (int i = 0; i < arrayOne.length; i++) {
+ if (!bundleValueEquals(arrayOne[i], arrayTwo[i])) {
+ return false;
+ }
+ }
+ return true;
+ } else if (one instanceof ArrayList && two instanceof ArrayList) {
+ ArrayList<?> listOne = (ArrayList<?>) one;
+ ArrayList<?> listTwo = (ArrayList<?>) two;
+ if (listOne.size() != listTwo.size()) {
+ return false;
+ }
+ for (int i = 0; i < listOne.size(); i++) {
+ if (!bundleValueEquals(listOne.get(i), listTwo.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ } else if (one instanceof SparseArray && two instanceof SparseArray) {
+ SparseArray<?> arrayOne = (SparseArray<?>) one;
+ SparseArray<?> arrayTwo = (SparseArray<?>) two;
+ if (arrayOne.size() != arrayTwo.size()) {
+ return false;
+ }
+ for (int i = 0; i < arrayOne.size(); i++) {
+ if (arrayOne.keyAt(i) != arrayTwo.keyAt(i)
+ || !bundleValueEquals(arrayOne.valueAt(i), arrayTwo.valueAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the hash code for a bundle.
+ * <p> The hash code is only effected by the contents in the bundle. Bundles will get
+ * consistent hash code if they have same contents.
+ */
+ public static int deepHashCode(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return 0;
+ }
+ int[] hashCodes = new int[bundle.size()];
+ int i = 0;
+ // Bundle inherit its hashCode() from Object.java, which only relative to their memory
+ // address. Bundle doesn't have an order, so we should iterate all keys and combine
+ // their value's hashcode into an array. And use the hashcode of the array to be
+ // the hashcode of the bundle.
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ if (value instanceof Bundle) {
+ hashCodes[i++] = deepHashCode((Bundle) value);
+ } else if (value instanceof int[]) {
+ hashCodes[i++] = Arrays.hashCode((int[]) value);
+ } else if (value instanceof byte[]) {
+ hashCodes[i++] = Arrays.hashCode((byte[]) value);
+ } else if (value instanceof char[]) {
+ hashCodes[i++] = Arrays.hashCode((char[]) value);
+ } else if (value instanceof long[]) {
+ hashCodes[i++] = Arrays.hashCode((long[]) value);
+ } else if (value instanceof float[]) {
+ hashCodes[i++] = Arrays.hashCode((float[]) value);
+ } else if (value instanceof short[]) {
+ hashCodes[i++] = Arrays.hashCode((short[]) value);
+ } else if (value instanceof double[]) {
+ hashCodes[i++] = Arrays.hashCode((double[]) value);
+ } else if (value instanceof boolean[]) {
+ hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+ } else if (value instanceof String[]) {
+ // Optimization to avoid Object[] handler creating an inner array for common cases
+ hashCodes[i++] = Arrays.hashCode((String[]) value);
+ } else if (value instanceof Object[]) {
+ Object[] array = (Object[]) value;
+ int[] innerHashCodes = new int[array.length];
+ for (int j = 0; j < array.length; j++) {
+ if (array[j] instanceof Bundle) {
+ innerHashCodes[j] = deepHashCode((Bundle) array[j]);
+ } else if (array[j] != null) {
+ innerHashCodes[j] = array[j].hashCode();
+ }
+ }
+ hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+ } else if (value instanceof ArrayList) {
+ ArrayList<?> list = (ArrayList<?>) value;
+ int[] innerHashCodes = new int[list.size()];
+ for (int j = 0; j < innerHashCodes.length; j++) {
+ Object item = list.get(j);
+ if (item instanceof Bundle) {
+ innerHashCodes[j] = deepHashCode((Bundle) item);
+ } else if (item != null) {
+ innerHashCodes[j] = item.hashCode();
+ }
+ }
+ hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+ } else if (value instanceof SparseArray) {
+ SparseArray<?> array = (SparseArray<?>) value;
+ int[] innerHashCodes = new int[array.size() * 2];
+ for (int j = 0; j < array.size(); j++) {
+ innerHashCodes[j * 2] = array.keyAt(j);
+ Object item = array.valueAt(j);
+ if (item instanceof Bundle) {
+ innerHashCodes[j * 2 + 1] = deepHashCode((Bundle) item);
+ } else if (item != null) {
+ innerHashCodes[j * 2 + 1] = item.hashCode();
+ }
+ }
+ hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+ } else {
+ hashCodes[i++] = value.hashCode();
+ }
+ }
+ return Arrays.hashCode(hashCodes);
+ }
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 3f4c746..cad539b 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -64,12 +64,13 @@
VisibilityStore.DATABASE_NAME + AppSearchImpl.DATABASE_DELIMITER
+ VisibilityStore.SCHEMA_TYPE)
.addProperty(new AppSearchSchema.PropertyConfig.Builder(
- VisibilityStore.PLATFORM_HIDDEN_PROPERTY)
+ VisibilityStore.NOT_PLATFORM_SURFACEABLE_PROPERTY)
.setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.build())
.build();
- mVisibilitySchemaProto = SchemaToProtoConverter.convert(visibilityAppSearchSchema);
+ mVisibilitySchemaProto =
+ SchemaToProtoConverter.toSchemaTypeConfigProto(visibilityAppSearchSchema);
}
/**
@@ -280,7 +281,8 @@
// Insert schema
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("type").build());
- mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Insert enough documents.
for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
@@ -327,7 +329,8 @@
// Insert schema
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("type").build());
- mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Insert document
GenericDocument document = new GenericDocument.Builder("uri", "type").setNamespace(
@@ -351,8 +354,10 @@
Set<AppSearchSchema> schemas = Set.of(
new AppSearchSchema.Builder("typeA").build(),
new AppSearchSchema.Builder("typeB").build());
- mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
- mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
+ mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Insert documents
GenericDocument document1 = new GenericDocument.Builder("uri", "typeA").setNamespace(
@@ -415,7 +420,8 @@
Set<AppSearchSchema> schemas =
Collections.singleton(new AppSearchSchema.Builder("Email").build());
// Set schema Email to AppSearch database1
- mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -432,21 +438,22 @@
@Test
public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "schema1").build()), /*forceOverride=*/false);
- mAppSearchImpl.setVisibility("database", Set.of("schema1"));
+ "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.singleton("schema1"), /*forceOverride=*/ false);
// "schema1" is platform hidden now
- assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/schema1");
// Add a new schema, and include the already-existing "schema1"
mAppSearchImpl.setSchema("database", Set.of(new AppSearchSchema.Builder(
- "schema1").build(), new AppSearchSchema.Builder(
- "schema2").build()), /*forceOverride=*/false);
+ "schema1").build(), new AppSearchSchema.Builder(
+ "schema2").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.singleton("schema1"), /*forceOverride=*/ false);
// Check that "schema1" is still platform hidden, but "schema2" is the default platform
// visible.
- assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/schema1");
}
@@ -456,7 +463,8 @@
schemas.add(new AppSearchSchema.Builder("Email").build());
schemas.add(new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1
- mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -475,12 +483,15 @@
"Email").build());
// Check the incompatible error has been thrown.
AppSearchException e = assertThrows(AppSearchException.class, () ->
- mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/false));
+ mAppSearchImpl.setSchema("database1",
+ finalSchemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false));
assertThat(e).hasMessageThat().contains("Schema is incompatible");
assertThat(e).hasMessageThat().contains("Deleted types: [database1/Document]");
// ForceOverride to delete.
- mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/true);
+ mAppSearchImpl.setSchema("database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ true);
// Check Document schema is removed.
expectedProto = SchemaProto.newBuilder()
@@ -502,8 +513,10 @@
schemas.add(new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1 and 2
- mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
- mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
+ mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -522,7 +535,8 @@
// Save only Email to database1 this time.
schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build());
- mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/true);
+ mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ true);
// Create expected schemaType list, database 1 should only contain Email but database 2
// remains in same.
@@ -544,67 +558,60 @@
@Test
public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "schema1").build()), /*forceOverride=*/false);
- mAppSearchImpl.setVisibility("database", Set.of("schema1"));
+ "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.singleton("schema1"), /*forceOverride=*/ false);
// "schema1" is platform hidden now
- assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/schema1");
// Remove "schema1" by force overriding
- mAppSearchImpl.setSchema("database", Collections.emptySet(), /*forceOverride=*/true);
+ mAppSearchImpl.setSchema("database",
+ Collections.emptySet(), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ true);
// Check that "schema1" is no longer considered platform hidden
assertThat(
- mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).isEmpty();
// Add "schema1" back, it gets default visibility settings which means it's not platform
// hidden.
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "schema1").build()), /*forceOverride=*/false);
+ "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
assertThat(
- mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).isEmpty();
}
@Test
- public void testSetVisibility_defaultPlatformVisible() throws Exception {
+ public void testSetSchema_defaultPlatformVisible() throws Exception {
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "Schema").build()), /*forceOverride=*/false);
+ "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
assertThat(
- mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).isEmpty();
}
@Test
- public void testSetVisibility_platformHidden() throws Exception {
+ public void testSetSchema_platformHidden() throws Exception {
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "Schema").build()), /*forceOverride=*/false);
- mAppSearchImpl.setVisibility("database", Set.of("Schema"));
- assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+ "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.singleton("Schema"), /*forceOverride=*/ false);
+ assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/Schema");
}
@Test
- public void testSetVisibility_unknownSchema() throws Exception {
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "Schema").build()), /*forceOverride=*/false);
-
- // We'll throw an exception if a client tries to set visibility on a schema we don't know
- // about.
- AppSearchException e = assertThrows(AppSearchException.class,
- () -> mAppSearchImpl.setVisibility("database", Set.of("UnknownSchema")));
- assertThat(e).hasMessageThat().contains("Unknown schema(s)");
- }
-
- @Test
public void testHasSchemaType() throws Exception {
// Nothing exists yet
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isFalse();
mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
- "Schema").build()), /*forceOverride=*/false);
+ "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isTrue();
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "UnknownSchema")).isFalse();
@@ -618,13 +625,15 @@
// Has database1
mAppSearchImpl.setSchema("database1", Collections.singleton(new AppSearchSchema.Builder(
- "schema").build()), /*forceOverride=*/false);
+ "schema").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
VisibilityStore.DATABASE_NAME, "database1");
// Has both databases
mAppSearchImpl.setSchema("database2", Collections.singleton(new AppSearchSchema.Builder(
- "schema").build()), /*forceOverride=*/false);
+ "schema").build()), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptySet(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
VisibilityStore.DATABASE_NAME, "database1", "database2");
}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
index 2113859..d4a30f4 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
@@ -42,35 +42,19 @@
@Test
public void testSetVisibility() throws Exception {
mVisibilityStore.setVisibility(
- "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2"));
- assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
+ "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema2"));
+ assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
.containsExactly("schema1", "schema2");
// New .setVisibility() call completely overrides previous visibility settings. So
// "schema1" isn't preserved.
mVisibilityStore.setVisibility(
- "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema3"));
- assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
+ "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema3"));
+ assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
.containsExactly("schema1", "schema3");
mVisibilityStore.setVisibility(
- "database", /*platformHiddenSchemas=*/ Collections.emptySet());
- assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty();
+ "database", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
+ assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database")).isEmpty();
}
-
- @Test
- public void testRemoveSchemas() throws Exception {
- mVisibilityStore.setVisibility(
- "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2"));
-
- // Removed just schema1
- mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema1"));
- assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
- .containsExactly("schema2");
-
- // Removed everything now
- mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema2"));
- assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty();
- }
-
}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 4fa010f..1245262 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -84,21 +84,20 @@
.addBytesValues(ByteString.copyFrom(BYTE_ARRAY_1))
.addBytesValues(ByteString.copyFrom(BYTE_ARRAY_2)));
propertyProtoMap.put("documentKey1",
- PropertyProto.newBuilder().setName("documentKey1")
- .addDocumentValues(
- GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_1)));
+ PropertyProto.newBuilder().setName("documentKey1").addDocumentValues(
+ GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_1)));
propertyProtoMap.put("documentKey2",
- PropertyProto.newBuilder().setName("documentKey2")
- .addDocumentValues(
- GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_2)));
+ PropertyProto.newBuilder().setName("documentKey2").addDocumentValues(
+ GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_2)));
List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
Collections.sort(sortedKey);
for (String key : sortedKey) {
documentProtoBuilder.addProperties(propertyProtoMap.get(key));
}
DocumentProto documentProto = documentProtoBuilder.build();
- assertThat(GenericDocumentToProtoConverter.convert(document))
+ assertThat(GenericDocumentToProtoConverter.toDocumentProto(document))
.isEqualTo(documentProto);
- assertThat(document).isEqualTo(GenericDocumentToProtoConverter.convert(documentProto));
+ assertThat(document)
+ .isEqualTo(GenericDocumentToProtoConverter.toGenericDocument(documentProto));
}
}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 0fb39db..889b0c7 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -69,7 +69,10 @@
)
).build();
- assertThat(SchemaToProtoConverter.convert(emailSchema)).isEqualTo(expectedEmailProto);
+ assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema))
+ .isEqualTo(expectedEmailProto);
+ assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+ .isEqualTo(emailSchema);
}
@Test
@@ -113,7 +116,9 @@
)
).build();
- assertThat(SchemaToProtoConverter.convert(musicRecordingSchema))
+ assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(musicRecordingSchema))
.isEqualTo(expectedMusicRecordingProto);
+ assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
+ .isEqualTo(musicRecordingSchema);
}
}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index d7ecf7b..9ffdeb9 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -77,7 +77,7 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage =
- SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+ SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
SearchResult.MatchInfo match = result.getMatches().get(0);
assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
@@ -124,7 +124,7 @@
.build();
SearchResultPage searchResultPage =
- SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+ SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
assertThat(result.getMatches()).isEmpty();
}
@@ -186,7 +186,7 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage =
- SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+ SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
for (SearchResult result : searchResultPage.getResults()) {
SearchResult.MatchInfo match1 = result.getMatches().get(0);
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 545b0d0..2ec2d82 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
@@ -223,13 +223,17 @@
*
* <p>This method belongs to mutate group.
*
- * @param databaseName The name of the database where this schema lives.
- * @param schemas Schemas to set for this app.
- * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
- * which do not comply with the new schema will be deleted.
+ * @param databaseName The name of the database where this schema lives.
+ * @param schemas Schemas to set for this app.
+ * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform
+ * surfaces.
+ * @param forceOverride Whether to force-apply the schema even if it is
+ * incompatible. Documents
+ * which do not comply with the new schema will be deleted.
* @throws AppSearchException on IcingSearchEngine error.
*/
public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas,
+ @NonNull Set<String> schemasNotPlatformSurfaceable,
boolean forceOverride) throws AppSearchException {
mReadWriteLock.writeLock().lock();
try {
@@ -237,7 +241,8 @@
SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
for (AppSearchSchema schema : schemas) {
- SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema);
+ SchemaTypeConfigProto schemaTypeProto =
+ SchemaToProtoConverter.toSchemaTypeConfigProto(schema);
newSchemaBuilder.addTypes(schemaTypeProto);
}
@@ -272,8 +277,15 @@
// Update derived data structures.
mSchemaMapLocked.put(databaseName, rewrittenSchemaResults.mRewrittenQualifiedTypes);
- mVisibilityStoreLocked.updateSchemas(databaseName,
- rewrittenSchemaResults.mDeletedQualifiedTypes);
+
+ String databasePrefix = getDatabasePrefix(databaseName);
+ Set<String> qualifiedSchemasNotPlatformSurfaceable =
+ new ArraySet<>(schemasNotPlatformSurfaceable.size());
+ for (String schema : schemasNotPlatformSurfaceable) {
+ qualifiedSchemasNotPlatformSurfaceable.add(databasePrefix + schema);
+ }
+ mVisibilityStoreLocked.setVisibility(databaseName,
+ qualifiedSchemasNotPlatformSurfaceable);
// Determine whether to schedule an immediate optimize.
if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
@@ -290,38 +302,53 @@
}
/**
- * Update the visibility settings for this app.
+ * Retrieves the AppSearch schema for this database.
*
- * <p>This method belongs to the mutate group
+ * <p>This method belongs to query group.
*
- * @param databaseName The name of the database where the
- * visibility settings will apply.
- * @param schemasHiddenFromPlatformSurfaces Schemas that should be hidden from platform
- * surfaces
- * @throws AppSearchException on IcingSearchEngine error
+ * @param databaseName The name of the database where this schema lives.
+ * @throws AppSearchException on IcingSearchEngine error.
*/
- public void setVisibility(@NonNull String databaseName,
- @NonNull Set<String> schemasHiddenFromPlatformSurfaces)
- throws AppSearchException {
- mReadWriteLock.writeLock().lock();
+ @NonNull
+ public Set<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
+ SchemaProto fullSchema;
+ mReadWriteLock.readLock().lock();
try {
- String databasePrefix = getDatabasePrefix(databaseName);
- Set<String> qualifiedSchemasHiddenFromPlatformSurface =
- new ArraySet<>(schemasHiddenFromPlatformSurfaces.size());
- for (String schema : schemasHiddenFromPlatformSurfaces) {
- Set<String> existingSchemas = mSchemaMapLocked.get(databaseName);
- if (existingSchemas == null || !existingSchemas.contains(databasePrefix + schema)) {
- throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
- "Unknown schema(s): " + schemasHiddenFromPlatformSurfaces
- + " provided during setVisibility.");
- }
- qualifiedSchemasHiddenFromPlatformSurface.add(databasePrefix + schema);
- }
- mVisibilityStoreLocked.setVisibility(databaseName,
- qualifiedSchemasHiddenFromPlatformSurface);
+ fullSchema = getSchemaProtoLocked();
} finally {
- mReadWriteLock.writeLock().lock();
+ mReadWriteLock.readLock().unlock();
}
+
+ Set<AppSearchSchema> result = new ArraySet<>();
+ for (int i = 0; i < fullSchema.getTypesCount(); i++) {
+ String typeDatabase = getDatabaseName(fullSchema.getTypes(i).getSchemaType());
+ if (!databaseName.equals(typeDatabase)) {
+ continue;
+ }
+ // Rewrite SchemaProto.types.schema_type
+ SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(i).toBuilder();
+ String newSchemaType =
+ typeConfigBuilder.getSchemaType().substring(databaseName.length() + 1);
+ typeConfigBuilder.setSchemaType(newSchemaType);
+
+ // Rewrite SchemaProto.types.properties.schema_type
+ for (int propertyIdx = 0;
+ propertyIdx < typeConfigBuilder.getPropertiesCount();
+ propertyIdx++) {
+ PropertyConfigProto.Builder propertyConfigBuilder =
+ typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+ if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+ String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+ .substring(databaseName.length() + 1);
+ propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+ typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+ }
+ }
+
+ AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
+ result.add(schema);
+ }
+ return result;
}
/**
@@ -335,7 +362,7 @@
*/
public void putDocument(@NonNull String databaseName, @NonNull GenericDocument document)
throws AppSearchException {
- DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.convert(
+ DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
document).toBuilder();
addPrefixToDocument(documentBuilder, getDatabasePrefix(databaseName));
@@ -379,7 +406,7 @@
DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
removeDatabasesFromDocument(documentBuilder);
- return GenericDocumentToProtoConverter.convert(documentBuilder.build());
+ return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build());
}
/**
@@ -961,7 +988,7 @@
resultsBuilder.setResults(i, resultBuilder);
}
}
- return SearchResultToProtoConverter.convertToSearchResultPage(resultsBuilder);
+ return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder);
}
@GuardedBy("mReadWriteLock")
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index df21825..12b25fa 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -30,9 +30,7 @@
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.SetVisibilityRequest;
import androidx.appsearch.localstorage.util.FutureUtil;
-import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -69,7 +67,8 @@
return execute(() -> {
try {
mAppSearchImpl.setSchema(
- mDatabaseName, request.getSchemas(), request.isForceOverride());
+ mDatabaseName, request.getSchemas(),
+ request.getSchemasNotPlatformSurfaceable(), request.isForceOverride());
return AppSearchResult.newSuccessfulResult(/*value=*/ null);
} catch (Throwable t) {
return throwableToFailedResult(t);
@@ -79,21 +78,10 @@
@Override
@NonNull
- public ListenableFuture<AppSearchResult<Void>> setVisibility(
- @NonNull SetVisibilityRequest request) {
- Preconditions.checkNotNull(request);
+ public ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema() {
return execute(() -> {
try {
- Set<AppSearchSchema> appSearchSchemasHiddenFromPlatformSurfaces =
- request.getSchemasHiddenFromPlatformSurfaces();
- Set<String> schemasHiddenFromPlatformSurfaces =
- new ArraySet<>(appSearchSchemasHiddenFromPlatformSurfaces.size());
- for (AppSearchSchema schema : appSearchSchemasHiddenFromPlatformSurfaces) {
- schemasHiddenFromPlatformSurfaces.add(schema.getSchemaType());
- }
-
- mAppSearchImpl.setVisibility(mDatabaseName, schemasHiddenFromPlatformSurfaces);
- return AppSearchResult.newSuccessfulResult(/*value=*/ null);
+ return AppSearchResult.newSuccessfulResult(mAppSearchImpl.getSchema(mDatabaseName));
} catch (Throwable t) {
return throwableToFailedResult(t);
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
index feb32db..a95231f 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
@@ -53,21 +53,24 @@
// Schema type for documents that hold AppSearch's metadata, e.g. visibility settings
@VisibleForTesting
static final String SCHEMA_TYPE = "Visibility";
+
// Property that holds the list of platform-hidden schemas, as part of the visibility
// settings.
@VisibleForTesting
- static final String PLATFORM_HIDDEN_PROPERTY = "platformHidden";
+ static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
// Database name to prefix all visibility schemas and documents with. Special-cased to
// minimize the chance of collision with a client-supplied database.
+
@VisibleForTesting
static final String DATABASE_NAME = "$$__AppSearch__Database";
+
// Namespace of documents that contain visibility settings
private static final String NAMESPACE = "namespace";
private final AppSearchImpl mAppSearchImpl;
// The map contains schemas that are platform-hidden for each database. All schemas in the map
// have a database name prefix.
- private final Map<String, Set<String>> mPlatformHiddenMap = new ArrayMap<>();
+ private final Map<String, Set<String>> mNotPlatformSurfaceableMap = new ArrayMap<>();
/**
* Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()}
@@ -84,7 +87,8 @@
*
* <p>This is kept separate from the constructor because this will call methods on
* AppSearchImpl. Some may even then recursively call back into VisibilityStore (for example,
- * {@link AppSearchImpl#setSchema} will call {@link #updateSchemas}. We need to have both
+ * {@link AppSearchImpl#setSchema} will call {@link #setVisibility(String, Set)}. We need to
+ * have both
* AppSearchImpl and VisibilityStore fully initialized for this call flow to work.
*
* @throws AppSearchException AppSearchException on AppSearchImpl error.
@@ -95,12 +99,13 @@
mAppSearchImpl.setSchema(DATABASE_NAME,
Collections.singleton(new AppSearchSchema.Builder(SCHEMA_TYPE)
.addProperty(new AppSearchSchema.PropertyConfig.Builder(
- PLATFORM_HIDDEN_PROPERTY)
+ NOT_PLATFORM_SURFACEABLE_PROPERTY)
.setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.build())
.build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
/*forceOverride=*/ false);
}
@@ -116,8 +121,9 @@
GenericDocument document = mAppSearchImpl.getDocument(
DATABASE_NAME, NAMESPACE, /*uri=*/ database);
- String[] schemas = document.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY);
- mPlatformHiddenMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
+ String[] schemas = document.getPropertyStringArray(
+ NOT_PLATFORM_SURFACEABLE_PROPERTY);
+ mNotPlatformSurfaceableMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
} catch (AppSearchException e) {
if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
// TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -132,91 +138,33 @@
}
/**
- * Update visibility settings for the {@code databaseName}.
- *
- * @param schemasToRemove Database-prefixed schemas that should be removed
- */
- public void updateSchemas(@NonNull String databaseName,
- @NonNull Set<String> schemasToRemove) throws AppSearchException {
- Preconditions.checkNotNull(databaseName);
- Preconditions.checkNotNull(schemasToRemove);
-
- GenericDocument visibilityDocument;
- try {
- visibilityDocument = mAppSearchImpl.getDocument(
- DATABASE_NAME, NAMESPACE, /*uri=*/ databaseName);
- } catch (AppSearchException e) {
- if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
- // This might be the first time we're seeing visibility changes for a database.
- // Create a new visibility document.
- mAppSearchImpl.putDocument(DATABASE_NAME, new GenericDocument.Builder(
- /*uri=*/ databaseName, SCHEMA_TYPE)
- .setNamespace(NAMESPACE).build());
-
- // Since we know there was nothing that existed before, we don't need to remove
- // anything either. Return early.
- return;
- }
- // Otherwise, this is some real error we should pass up.
- throw e;
- }
-
- String[] hiddenSchemas =
- visibilityDocument.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY);
- if (hiddenSchemas == null) {
- // Nothing to remove.
- return;
- }
-
- // Create a new set so we can remove from it.
- Set<String> remainingSchemas = new ArraySet<>(Arrays.asList(hiddenSchemas));
- boolean changed = remainingSchemas.removeAll(schemasToRemove);
- if (!changed) {
- // Nothing was actually removed. Can return early.
- return;
- }
-
- // Update our persisted document
- // TODO(b/171882200): Switch to a .toBuilder API when it's available.
- GenericDocument.Builder newVisibilityDocument = new GenericDocument.Builder(
- /*uri=*/ databaseName, SCHEMA_TYPE)
- .setNamespace(NAMESPACE);
- if (!remainingSchemas.isEmpty()) {
- newVisibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY,
- remainingSchemas.toArray(new String[0]));
- }
- mAppSearchImpl.putDocument(DATABASE_NAME, newVisibilityDocument.build());
-
- // Update derived data structures
- mPlatformHiddenMap.put(databaseName, remainingSchemas);
- }
-
- /**
* Sets visibility settings for {@code databaseName}. Any previous visibility settings will be
* overwritten.
*
- * @param databaseName Database name that owns the {@code platformHiddenSchemas}.
- * @param platformHiddenSchemas Set of database-qualified schemas that should be hidden from
- * the platform.
+ * @param databaseName Database name that owns the {@code
+ * schemasNotPlatformSurfaceable}.
+ * @param schemasNotPlatformSurfaceable Set of database-qualified schemas that should be
+ * hidden from
+ * the platform.
* @throws AppSearchException on AppSearchImpl error.
*/
public void setVisibility(@NonNull String databaseName,
- @NonNull Set<String> platformHiddenSchemas) throws AppSearchException {
+ @NonNull Set<String> schemasNotPlatformSurfaceable) throws AppSearchException {
Preconditions.checkNotNull(databaseName);
- Preconditions.checkNotNull(platformHiddenSchemas);
+ Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
// Persist the document
GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
/*uri=*/ databaseName, SCHEMA_TYPE)
.setNamespace(NAMESPACE);
- if (!platformHiddenSchemas.isEmpty()) {
- visibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY,
- platformHiddenSchemas.toArray(new String[0]));
+ if (!schemasNotPlatformSurfaceable.isEmpty()) {
+ visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
+ schemasNotPlatformSurfaceable.toArray(new String[0]));
}
mAppSearchImpl.putDocument(DATABASE_NAME, visibilityDocument.build());
// Update derived data structures.
- mPlatformHiddenMap.put(databaseName, platformHiddenSchemas);
+ mNotPlatformSurfaceableMap.put(databaseName, schemasNotPlatformSurfaceable);
}
/**
@@ -228,13 +176,13 @@
* none exist.
*/
@NonNull
- public Set<String> getPlatformHiddenSchemas(@NonNull String databaseName) {
+ public Set<String> getSchemasNotPlatformSurfaceable(@NonNull String databaseName) {
Preconditions.checkNotNull(databaseName);
- Set<String> platformHiddenSchemas = mPlatformHiddenMap.get(databaseName);
- if (platformHiddenSchemas == null) {
+ Set<String> schemasNotPlatformSurfaceable = mNotPlatformSurfaceableMap.get(databaseName);
+ if (schemasNotPlatformSurfaceable == null) {
return Collections.emptySet();
}
- return platformHiddenSchemas;
+ return schemasNotPlatformSurfaceable;
}
/**
@@ -244,7 +192,7 @@
* @throws AppSearchException on AppSearchImpl error.
*/
public void handleReset() throws AppSearchException {
- mPlatformHiddenMap.clear();
+ mNotPlatformSurfaceableMap.clear();
initialize();
}
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index 2f4c23d..c27cb92 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -39,7 +39,7 @@
/** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
@NonNull
@SuppressWarnings("unchecked")
- public static DocumentProto convert(@NonNull GenericDocument document) {
+ public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
Preconditions.checkNotNull(document);
DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
mProtoBuilder.setUri(document.getUri())
@@ -81,7 +81,7 @@
}
} else if (documentValues != null) {
for (int j = 0; j < documentValues.length; j++) {
- DocumentProto proto = convert(documentValues[j]);
+ DocumentProto proto = toDocumentProto(documentValues[j]);
propertyProto.addDocumentValues(proto);
}
} else {
@@ -95,7 +95,7 @@
/** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
@NonNull
- public static GenericDocument convert(@NonNull DocumentProto proto) {
+ public static GenericDocument toGenericDocument(@NonNull DocumentProto proto) {
Preconditions.checkNotNull(proto);
GenericDocument.Builder<?> documentBuilder =
new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
@@ -140,7 +140,7 @@
} else if (property.getDocumentValuesCount() > 0) {
GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
for (int j = 0; j < values.length; j++) {
- values[j] = convert(property.getDocumentValues(j));
+ values[j] = toGenericDocument(property.getDocumentValues(j));
}
documentBuilder.setPropertyDocument(name, values);
} else {
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index 3d79150..f52c220 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -16,6 +16,8 @@
package androidx.appsearch.localstorage.converter;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.AppSearchSchema;
@@ -23,6 +25,7 @@
import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
import com.google.android.icing.proto.StringIndexingConfig;
import com.google.android.icing.proto.TermMatchType;
@@ -34,6 +37,8 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SchemaToProtoConverter {
+ private static final String TAG = "AppSearchSchemaToProtoC";
+
private SchemaToProtoConverter() {}
/**
@@ -41,23 +46,23 @@
* {@link SchemaTypeConfigProto}.
*/
@NonNull
- public static SchemaTypeConfigProto convert(@NonNull AppSearchSchema schema) {
+ public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema) {
Preconditions.checkNotNull(schema);
SchemaTypeConfigProto.Builder protoBuilder =
SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType());
List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
for (int i = 0; i < properties.size(); i++) {
- PropertyConfigProto propertyProto = convertProperty(properties.get(i));
+ PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
protoBuilder.addProperties(propertyProto);
}
return protoBuilder.build();
}
@NonNull
- private static PropertyConfigProto convertProperty(
+ private static PropertyConfigProto toPropertyConfigProto(
@NonNull AppSearchSchema.PropertyConfig property) {
Preconditions.checkNotNull(property);
- PropertyConfigProto.Builder propertyConfigProto = PropertyConfigProto.newBuilder()
+ PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
.setPropertyName(property.getName());
StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder();
@@ -68,12 +73,12 @@
if (dataTypeProto == null) {
throw new IllegalArgumentException("Invalid dataType: " + dataType);
}
- propertyConfigProto.setDataType(dataTypeProto);
+ builder.setDataType(dataTypeProto);
// Set schemaType
String schemaType = property.getSchemaType();
if (schemaType != null) {
- propertyConfigProto.setSchemaType(schemaType);
+ builder.setSchemaType(schemaType);
}
// Set cardinality
@@ -83,7 +88,7 @@
if (cardinalityProto == null) {
throw new IllegalArgumentException("Invalid cardinality: " + dataType);
}
- propertyConfigProto.setCardinality(cardinalityProto);
+ builder.setCardinality(cardinalityProto);
// Set indexingType
@AppSearchSchema.PropertyConfig.IndexingType int indexingType = property.getIndexingType();
@@ -114,7 +119,63 @@
indexingConfig.setTokenizerType(tokenizerTypeProto);
// Build!
- propertyConfigProto.setStringIndexingConfig(indexingConfig);
- return propertyConfigProto.build();
+ builder.setStringIndexingConfig(indexingConfig);
+ return builder.build();
+ }
+
+ /**
+ * Converts a {@link SchemaTypeConfigProto} into an
+ * {@link androidx.appsearch.app.AppSearchSchema}.
+ */
+ @NonNull
+ public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
+ Preconditions.checkNotNull(proto);
+ AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+ List<PropertyConfigProto> properties = proto.getPropertiesList();
+ for (int i = 0; i < properties.size(); i++) {
+ AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
+ builder.addProperty(propertyConfig);
+ }
+ return builder.build();
+ }
+
+ @NonNull
+ private static AppSearchSchema.PropertyConfig toPropertyConfig(
+ @NonNull PropertyConfigProto proto) {
+ Preconditions.checkNotNull(proto);
+ AppSearchSchema.PropertyConfig.Builder builder =
+ new AppSearchSchema.PropertyConfig.Builder(proto.getPropertyName())
+ .setDataType(proto.getDataType().getNumber())
+ .setCardinality(proto.getCardinality().getNumber())
+ .setTokenizerType(
+ proto.getStringIndexingConfig().getTokenizerType().getNumber());
+
+ // Set schema
+ if (!proto.getSchemaType().isEmpty()) {
+ builder.setSchemaType(proto.getSchemaType());
+ }
+
+ // Set indexingType
+ @AppSearchSchema.PropertyConfig.IndexingType int indexingType;
+ TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
+ switch (termMatchTypeProto) {
+ case UNKNOWN:
+ indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+ break;
+ case EXACT_ONLY:
+ indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS;
+ break;
+ case PREFIX:
+ indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+ break;
+ default:
+ // Avoid crashing in the 'read' path; we should try to interpret the document to the
+ // extent possible.
+ Log.w(TAG, "Invalid indexingType: " + termMatchTypeProto.getNumber());
+ indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+ }
+ builder.setIndexingType(indexingType);
+
+ return builder.build();
}
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 97c4adc..8d7257c 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -42,13 +42,12 @@
/** Translate a {@link SearchResultProto} into {@link SearchResultPage}. */
@NonNull
- public static SearchResultPage convertToSearchResultPage(
- @NonNull SearchResultProtoOrBuilder proto) {
+ public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto) {
Bundle bundle = new Bundle();
bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
for (int i = 0; i < proto.getResultsCount(); i++) {
- resultBundles.add(convertToSearchResultBundle(proto.getResults(i)));
+ resultBundles.add(toSearchResultBundle(proto.getResults(i)));
}
bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
return new SearchResultPage(bundle);
@@ -56,10 +55,11 @@
/** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */
@NonNull
- private static Bundle convertToSearchResultBundle(
+ private static Bundle toSearchResultBundle(
@NonNull SearchResultProto.ResultProtoOrBuilder proto) {
Bundle bundle = new Bundle();
- GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument());
+ GenericDocument document =
+ GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument());
bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
ArrayList<Bundle> matchList = new ArrayList<>();
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 44a22a8..f103e683 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -26,12 +26,16 @@
under the new package visibility changes for Android 11.
-->
<activity
- android:name=".MainActivity"
+ android:name=".TrivialStartupActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
</activity>
<activity
@@ -42,6 +46,5 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
</application>
</manifest>
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
index efddddb..bb07d9f 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
@@ -24,9 +24,11 @@
class RecyclerViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ title = "RecyclerView Sample"
setContentView(R.layout.activity_recycler_view)
val recycler = findViewById<RecyclerView>(R.id.recycler)
- val adapter = EntryAdapter(entries(1000))
+ val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
+ val adapter = EntryAdapter(entries(itemCount))
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
}
@@ -36,4 +38,8 @@
Entry("Item $it")
}
}
+
+ companion object {
+ const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+ }
}
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
similarity index 96%
rename from benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
rename to benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
index 05177a9..5cf2081 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
-class MainActivity : AppCompatActivity() {
+class TrivialStartupActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
index d8d6754..fa8493f 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
@@ -15,23 +15,44 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:minHeight="64dp"
+ android:padding="8dp">
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:padding="8dp">
- <TextView
- android:id="@+id/content"
- android:padding="8dp"
+ <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- tools:text="Sample text" />
+ android:layout_height="wrap_content">
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:layout_marginEnd="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ android:padding="8dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Sample text" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-
</LinearLayout>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index 398842e..3384fe8 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -34,6 +34,7 @@
}
dependencies {
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":benchmark:benchmark-macro"))
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
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/JankMetricValidation.kt
index 3285605..89e3b20 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/JankMetricValidation.kt
@@ -22,24 +22,23 @@
import androidx.benchmark.macro.MacrobenchmarkConfig
import androidx.benchmark.macro.MacrobenchmarkRule
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.UiDevice
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@LargeTest
+@SdkSuppress(minSdkVersion = 29)
@RunWith(Parameterized::class)
class JankMetricValidation(
- private val compilationMode: CompilationMode,
- private val killProcess: Boolean
+ private val compilationMode: CompilationMode
) {
-
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@@ -52,13 +51,11 @@
}
@Test
- @Ignore("Not running the test in CI")
fun start() {
val config = MacrobenchmarkConfig(
packageName = PACKAGE_NAME,
metrics = listOf(JankMetric()),
compilationMode = compilationMode,
- killProcessEachIteration = killProcess,
iterations = 10
)
@@ -67,7 +64,6 @@
setupBlock = {
val intent = Intent()
intent.action = ACTION
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
launchIntentAndWait(intent)
}
) {
@@ -85,17 +81,13 @@
"androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
private const val RESOURCE_ID = "recycler"
- @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+ @Parameterized.Parameters(name = "compilation_mode={0}")
@JvmStatic
fun jankParameters(): List<Array<Any>> {
- val compilationModes = listOf(
+ return listOf(
CompilationMode.None,
CompilationMode.SpeedProfile(warmupIterations = 3)
- )
- val processKillOptions = listOf(false, false)
- return compilationModes.zip(processKillOptions).map {
- arrayOf(it.first, it.second)
- }
+ ).map { arrayOf(it) }
}
}
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
deleted file mode 100644
index 1b7b5a6..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
+++ /dev/null
@@ -1,50 +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.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.MacrobenchmarkConfig
-import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class MacrobenchmarkRuleTest {
- @get:Rule
- val benchmarkRule = MacrobenchmarkRule()
-
- @Test
- @Ignore("Not running the test in CI")
- fun basicTest() = benchmarkRule.measureRepeated(
- MacrobenchmarkConfig(
- packageName = "androidx.benchmark.integration.macrobenchmark.target",
- listOf(StartupTimingMetric()),
- CompilationMode.Speed,
- killProcessEachIteration = true,
- iterations = 4
- )
- ) {
- pressHome()
- launchPackageAndWait()
- }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index 34ab2b4..bd1951a 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -16,58 +16,53 @@
package androidx.benchmark.integration.macrobenchmark
-import android.content.Intent
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.macrobenchmark
import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@LargeTest
+@SdkSuppress(minSdkVersion = 29)
@RunWith(Parameterized::class)
class ProcessSpeedProfileValidation(
private val compilationMode: CompilationMode,
- private val killProcess: Boolean
+ private val startupMode: StartupMode
) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
@Test
- @Ignore("Not running the test in CI")
- fun start() {
- val benchmarkName = "speed_profile_process_validation"
- val config = MacrobenchmarkConfig(
+ fun start() = benchmarkRule.measureStartupRepeated(
+ MacrobenchmarkConfig(
packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
- killProcessEachIteration = killProcess,
- iterations = 10
- )
- macrobenchmark(
- benchmarkName = benchmarkName,
- config = config
- ) {
- pressHome()
- launchPackageAndWait { launchIntent ->
- // Clear out any previous instances
- launchIntent.flags =
- Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
- }
+ iterations = 3
+ ),
+ startupMode
+ ) {
+ pressHome()
+ launchPackageAndWait()
}
companion object {
private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
- @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+ @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
@JvmStatic
fun kilProcessParameters(): List<Array<Any>> {
val compilationModes = listOf(
CompilationMode.None,
CompilationMode.SpeedProfile(warmupIterations = 3)
)
- val processKillOptions = listOf(true, false)
+ val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
return compilationModes.zip(processKillOptions).map {
arrayOf(it.first, it.second)
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
new file mode 100644
index 0000000..33778b4
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.integration.macrobenchmark
+
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureStartup(
+ profileCompiled = true,
+ startupMode = startupMode
+ ) {
+ action = "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
+ putExtra("ITEM_COUNT", 5)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "mode={0}")
+ @JvmStatic
+ fun parameters(): List<Array<Any>> {
+ return listOf(StartupMode.COLD, StartupMode.WARM)
+ .map { arrayOf(it) }
+ }
+ }
+}
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
new file mode 100644
index 0000000..e8c152e
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+
+const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
+
+fun MacrobenchmarkRule.measureStartup(
+ profileCompiled: Boolean,
+ startupMode: StartupMode,
+ iterations: Int = 5,
+ setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
+ MacrobenchmarkConfig(
+ packageName = "androidx.benchmark.integration.macrobenchmark.target",
+ metrics = listOf(StartupTimingMetric()),
+ compilationMode = if (profileCompiled) {
+ CompilationMode.SpeedProfile(warmupIterations = 3)
+ } else {
+ CompilationMode.None
+ },
+ iterations = iterations
+ ),
+ startupMode = startupMode
+) {
+ pressHome()
+ val intent = Intent()
+ intent.setPackage(TARGET_PACKAGE)
+ setupIntent(intent)
+ launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
new file mode 100644
index 0000000..af3e850
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.integration.macrobenchmark
+
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureStartup(
+ profileCompiled = true,
+ startupMode = startupMode
+ ) {
+ action = "androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "mode={0}")
+ @JvmStatic
+ fun parameters(): List<Array<Any>> {
+ return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
+ .map { arrayOf(it) }
+ }
+ }
+}
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
index d614eb1..d5e016d 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
@@ -37,7 +37,7 @@
@Test
@Ignore("Figure out why we can't launch the default activity.")
fun killTest() {
- val scope = MacrobenchmarkScope(PACKAGE_NAME)
+ val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
scope.pressHome()
scope.launchPackageAndWait()
assertTrue(isProcessAlive(PACKAGE_NAME))
@@ -48,7 +48,7 @@
@Test
@Ignore("Compilation modes are a bit flaky")
fun compile_speedProfile() {
- val scope = MacrobenchmarkScope(PACKAGE_NAME)
+ val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
val iterations = 1
var executions = 0
val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
index fa8afde..a2ece0e 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
@@ -56,7 +56,7 @@
val packageName = "androidx.benchmark.macro.test"
val helper = AppStartupHelper()
helper.startCollecting()
- val scope = MacrobenchmarkScope(packageName)
+ val scope = MacrobenchmarkScope(packageName, launchWithClearTask = false)
// note: don't killProcess first, that's our process too!
// additionally, skip pressHome, since it's not needed, and skipping saves significant time
scope.launchPackageAndWait {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
index 3a0d28d..be1fffd 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
@@ -21,9 +21,10 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import java.io.File
-import java.lang.IllegalStateException
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
-private const val TAG = "MacroBenchmarks"
+const val TAG = "MacroBenchmarks"
// SELinux enforcement
private const val PERMISSIVE = "Permissive"
@@ -38,6 +39,9 @@
// All modes
private val COMPILE_MODES = listOf(SPEED, SPEED_PROFILE, QUICKEN, VERIFY)
+// SELinux Permission Lock
+private val permissiveLock = ReentrantReadWriteLock()
+
/**
* Drops the kernel page cache
*
@@ -81,6 +85,7 @@
}
Thread.sleep(profileSaveTimeout)
}
+ Log.d(TAG, "Compiling $packageName ($mode)")
val response = device.executeShellCommand("cmd package compile -f -m $mode $packageName")
if (!response.contains("Success")) {
Log.d(TAG, "Received compile cmd response: $response")
@@ -95,6 +100,7 @@
instrumentation: Instrumentation,
packageName: String,
) {
+ Log.d(TAG, "Clearing profiles for $packageName")
instrumentation.device().executeShellCommand("cmd package compile --reset $packageName")
}
@@ -113,14 +119,16 @@
fun withPermissiveSeLinuxPolicy(block: () -> Unit) {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val previousPolicy = getSeLinuxPolicyEnforced(instrumentation)
- try {
- if (previousPolicy == 1) {
- setSeLinuxPolicyEnforced(instrumentation, 0)
- }
- block()
- } finally {
- if (previousPolicy == 1) {
- setSeLinuxPolicyEnforced(instrumentation, previousPolicy)
+ permissiveLock.read {
+ try {
+ if (previousPolicy == 1) {
+ setSeLinuxPolicyEnforced(instrumentation, 0)
+ }
+ block()
+ } finally {
+ if (previousPolicy == 1 && permissiveLock.readHoldCount == 1) {
+ setSeLinuxPolicyEnforced(instrumentation, previousPolicy)
+ }
}
}
}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
index 795f693..c0c35c3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
@@ -89,7 +89,7 @@
return List(expectedEventCount) { index ->
val appStart = appStartOccurredList.getOrNull(index)
AppStartupMetrics(
- transitionType = appStart?.type.toString(),
+ transitionType = appStart?.type?.toStartupMode(),
windowDrawnDelayMs = appStart?.windowsDrawnDelayMillis?.toLong(),
transitionDelayMs = appStart?.transitionDelayMillis?.toLong(),
appStartupTimeMs = appStartFullyDrawnList.getOrNull(index)?.appStartupTimeMillis,
@@ -113,14 +113,20 @@
)
}
val result = results.first()
- // TODO: potentially filter these further for app vs platform usage
+
+ Log.d(
+ "AppStartupHelper",
+ "saw startup of package $packageName, type ${result.transitionType}"
+ )
return mapOf(
// AppStartupHelper originally reports this as simply startup time, so we do the same
"startupMs" to result.windowDrawnDelayMs,
- "transitionDelayMs" to result.transitionDelayMs,
// Though the proto calls this appStartupTime, we clarify this is "fully drawn" startup
"startupFullyDrawnMs" to result.appStartupTimeMs,
- "processStartDelayMs" to result.processStartDelayMs,
+
+ // The following metrics are useful for platform devs, but disabled for now for brevity
+ // "transitionDelayMs" to result.transitionDelayMs,
+ // "processStartDelayMs" to result.processStartDelayMs,
)
.filterValues { it != null } // doesn't drop nullability for values...
.mapValues { it.value!! } // ...so we do that explicitly here
@@ -140,8 +146,17 @@
isProcStartDetailsDisabled = true
}
+ private fun Int.toStartupMode(): StartupMode? {
+ return when (this) {
+ AtomsProto.AppStartOccurred.COLD -> StartupMode.COLD
+ AtomsProto.AppStartOccurred.WARM -> StartupMode.WARM
+ AtomsProto.AppStartOccurred.HOT -> StartupMode.HOT
+ else -> null
+ }
+ }
+
data class AppStartupMetrics(
- val transitionType: String?,
+ val transitionType: StartupMode?,
val windowDrawnDelayMs: Long?,
val transitionDelayMs: Long?,
val appStartupTimeMs: Long?,
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 71ccadf..c386a1e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -16,6 +16,8 @@
package androidx.benchmark.macro
+import androidx.test.platform.app.InstrumentationRegistry
+
sealed class CompilationMode(
// for modes other than [None], is argument passed `cmd package compile`
private val compileArgument: String?
@@ -39,3 +41,23 @@
override fun toString() = "CompilationMode.Speed"
}
}
+
+internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ // Clear profile between runs.
+ clearProfile(instrumentation, packageName)
+ if (this == CompilationMode.None) {
+ return // nothing to do
+ }
+ if (this is CompilationMode.SpeedProfile) {
+ repeat(this.warmupIterations) {
+ block()
+ }
+ }
+ // TODO: merge in below method
+ compilationFilter(
+ InstrumentationRegistry.getInstrumentation(),
+ packageName,
+ compileArgument()
+ )
+}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
new file mode 100644
index 0000000..24e628d
--- /dev/null
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.macro
+
+import androidx.benchmark.Stats
+import java.util.Collections
+import kotlin.math.max
+
+internal fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
+ val maxLabelLength = Collections.max(statsList.map { it.name.length })
+
+ // max string length of any printed min/median/max is the largest max value seen. used to pad.
+ val maxValueLength = statsList
+ .map { it.max }
+ .reduce { acc, maxValue -> max(acc, maxValue) }
+ .toString().length
+
+ return "$benchmarkName\n" + statsList.joinToString("\n") {
+ val displayName = it.name.padStart(maxLabelLength)
+ val displayMin = it.min.toString().padStart(maxValueLength)
+ val displayMedian = it.median.toString().padStart(maxValueLength)
+ val displayMax = it.max.toString().padStart(maxValueLength)
+ " $displayName min $displayMin, median $displayMedian, max $displayMax"
+ } + "\n"
+}
\ 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 1d51fa8..4a88391 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -17,21 +17,27 @@
package androidx.benchmark.macro
import android.content.Intent
+import android.util.Log
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.Stats
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
-import java.util.Collections
-import kotlin.math.max
/**
* Provides access to common operations in app automation, such as killing the app,
* or navigating home.
*/
public class MacrobenchmarkScope(
- private val packageName: String
+ private val packageName: String,
+ /**
+ * Controls whether launches will automatically set [Intent.FLAG_ACTIVITY_CLEAR_TASK].
+ *
+ * Default to true, so Activity launches go through full creation lifecycle stages, instead of
+ * just resume.
+ */
+ private val launchWithClearTask: Boolean
) {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context = instrumentation.context
@@ -39,14 +45,9 @@
/**
* Launch the package, with a customizable intent.
- *
- * If [block] is not specified, launches with [Intent.FLAG_ACTIVITY_NEW_TASK] as well as
- * [Intent.FLAG_ACTIVITY_CLEAR_TASK]
*/
fun launchPackageAndWait(
- block: (Intent) -> Unit = {
- it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
+ block: (Intent) -> Unit = {}
) {
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
?: throw IllegalStateException("Unable to acquire intent for package $packageName")
@@ -56,6 +57,11 @@
}
fun launchIntentAndWait(intent: Intent) {
+ // Must launch with new task, as we're not launching from an existing task
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ if (launchWithClearTask) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
context.startActivity(intent)
device.wait(
Until.hasObject(By.pkg(packageName).depth(0)),
@@ -69,6 +75,7 @@
}
fun killProcess() {
+ Log.d(TAG, "Killing process $packageName")
device.executeShellCommand("am force-stop $packageName")
}
}
@@ -77,7 +84,6 @@
val packageName: String,
val metrics: List<Metric>,
val compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
- val killProcessEachIteration: Boolean = false,
val iterations: Int
)
@@ -89,16 +95,17 @@
fun macrobenchmark(
benchmarkName: String,
config: MacrobenchmarkConfig,
- setupBlock: MacrobenchmarkScope.() -> Unit = {},
+ launchWithClearTask: Boolean,
+ setupBlock: MacrobenchmarkScope.(Boolean) -> Unit,
measureBlock: MacrobenchmarkScope.() -> Unit
) = withPermissiveSeLinuxPolicy {
- val scope = MacrobenchmarkScope(config.packageName)
+ val scope = MacrobenchmarkScope(config.packageName, launchWithClearTask)
// always kill the process at beginning of test
scope.killProcess()
config.compilationMode.compile(config.packageName) {
- setupBlock(scope)
+ setupBlock(scope, false)
measureBlock(scope)
}
@@ -109,24 +116,21 @@
config.metrics.forEach {
it.configure(config)
}
+ var isFirstRun = true
val results = List(config.iterations) { iteration ->
- if (config.killProcessEachIteration) {
- // TODO: remove this flag, make this part of setupBlock
- scope.killProcess()
- }
- setupBlock(scope)
+ setupBlock(scope, isFirstRun)
+ isFirstRun = false
try {
perfettoCollector.start()
config.metrics.forEach {
it.start()
}
measureBlock(scope)
+ } finally {
config.metrics.forEach {
it.stop()
}
- } finally {
- val iterString = iteration.toString().padStart(3, '0')
- perfettoCollector.stop("${benchmarkName}_iter$iterString.trace")
+ perfettoCollector.stop(benchmarkName, iteration)
}
config.metrics
@@ -139,9 +143,14 @@
// merge each independent Map<String,Long> to one Map<String,List<Long>>
val setOfAllKeys = results.flatMap { it.keys }.toSet()
val listResults = setOfAllKeys.map { key ->
- key to results.map { it[key] ?: error("Value $key missing from one iteration") }
+ // b/174175947
+ key to results.mapNotNull {
+ if (key !in it) {
+ Log.w(TAG, "Value $key missing from one iteration {$it}")
+ }
+ it[key]
+ }
}.toMap()
-
val statsList = listResults.map { (metricName, values) ->
Stats(values.toLongArray(), metricName)
}.sortedBy { it.name }
@@ -155,40 +164,53 @@
}
}
-fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
- val maxLabelLength = Collections.max(statsList.map { it.name.length })
+enum class StartupMode {
+ /**
+ * Startup from scratch - app's process is not alive, and must be started in addition to
+ * Activity creation.
+ *
+ * See
+ * [Cold startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold)
+ */
+ COLD,
- // max string length of any printed min/median/max is the largest max value seen. used to pad.
- val maxValueLength = statsList
- .map { it.max }
- .reduce { acc, maxValue -> max(acc, maxValue) }
- .toString().length
+ /**
+ * Create and display a new Activity in a currently running app process.
+ *
+ * See
+ * [Warm startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#warm)
+ */
+ WARM,
- return "$benchmarkName\n" + statsList.joinToString("\n") {
- val displayName = it.name.padStart(maxLabelLength)
- val displayMin = it.min.toString().padStart(maxValueLength)
- val displayMedian = it.median.toString().padStart(maxValueLength)
- val displayMax = it.max.toString().padStart(maxValueLength)
- " $displayName min $displayMin, median $displayMedian, max $displayMax"
- } + "\n"
+ /**
+ * Bring existing activity to the foreground, process and Activity still exist from previous
+ * launch.
+ *
+ * See
+ * [Hot startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#hot)
+ */
+ HOT
}
-internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
- val instrumentation = InstrumentationRegistry.getInstrumentation()
- // Clear profile between runs.
- clearProfile(instrumentation, packageName)
- if (this == CompilationMode.None) {
- return // nothing to do
- }
- if (this is CompilationMode.SpeedProfile) {
- repeat(this.warmupIterations) {
- block()
- }
- }
- // TODO: merge in below method
- compilationFilter(
- InstrumentationRegistry.getInstrumentation(),
- packageName,
- compileArgument()
+fun startupMacrobenchmark(
+ benchmarkName: String,
+ config: MacrobenchmarkConfig,
+ startupMode: StartupMode,
+ performStartup: MacrobenchmarkScope.() -> Unit
+) {
+ macrobenchmark(
+ benchmarkName = benchmarkName,
+ config = config,
+ setupBlock = { firstIterAfterCompile ->
+ if (startupMode == StartupMode.COLD) {
+ killProcess()
+ } else if (firstIterAfterCompile) {
+ // warmup process by launching the activity, unmeasured
+ performStartup()
+ }
+ },
+ // only reuse existing activity if StartupMode == HOT
+ launchWithClearTask = startupMode != StartupMode.HOT,
+ measureBlock = performStartup
)
}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
index 86e31bc..a5edac8 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
@@ -28,10 +28,29 @@
fun measureRepeated(
config: MacrobenchmarkConfig,
- setupBlock: MacrobenchmarkScope.() -> Unit = {},
+ setupBlock: MacrobenchmarkScope.(Boolean) -> Unit = {},
measureBlock: MacrobenchmarkScope.() -> Unit
) {
- macrobenchmark(benchmarkName, config, setupBlock, measureBlock)
+ macrobenchmark(
+ benchmarkName = benchmarkName,
+ config = config,
+ launchWithClearTask = true,
+ setupBlock = setupBlock,
+ measureBlock = measureBlock
+ )
+ }
+
+ fun measureStartupRepeated(
+ config: MacrobenchmarkConfig,
+ startupMode: StartupMode,
+ performStartup: MacrobenchmarkScope.() -> Unit
+ ) {
+ startupMacrobenchmark(
+ benchmarkName = benchmarkName,
+ config = config,
+ startupMode = startupMode,
+ performStartup = performStartup
+ )
}
override fun apply(base: Statement, description: Description) = object : Statement() {
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 88014df..840dee5 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
@@ -42,11 +42,14 @@
return true
}
- fun stop(traceName: String): Boolean {
+ 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", destination)
+ reportAdditionalFileToCopy("perfetto_trace_$iterString", destination)
}
return true
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
index dc64b9a..f27e132 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
@@ -35,6 +35,9 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+const val composeSourceOption =
+ "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
+
/**
* Plugin to apply options across all of the androidx.ui projects
*/
@@ -81,6 +84,20 @@
}
}
+ project.afterEvaluate {
+ val androidXExtension =
+ project.extensions.findByType(AndroidXExtension::class.java)
+ if (androidXExtension != null) {
+ if (!conf.isEmpty && androidXExtension.publish.shouldPublish()) {
+ project.tasks.withType(KotlinCompile::class.java)
+ .configureEach { compile ->
+ compile.kotlinOptions.freeCompilerArgs +=
+ listOf("-P", composeSourceOption)
+ }
+ }
+ }
+ }
+
if (plugin is KotlinMultiplatformPluginWrapper) {
project.configureForMultiplatform()
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 5023caa..57e1f7d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -35,16 +35,16 @@
val BIOMETRIC = Version("1.2.0-alpha01")
val BROWSER = Version("1.3.0-rc01")
val BUILDSRC_TESTS = Version("1.0.0-alpha01")
- val CAMERA = Version("1.0.0-beta12")
- val CAMERA_EXTENSIONS = Version("1.0.0-alpha19")
+ val CAMERA = Version("1.0.0-rc01")
+ val CAMERA_EXTENSIONS = Version("1.0.0-alpha20")
val CAMERA_PIPE = Version("1.0.0-alpha01")
val CAMERA_VIDEO = Version("1.0.0-alpha01")
- val CAMERA_VIEW = Version("1.0.0-alpha19")
+ val CAMERA_VIEW = Version("1.0.0-alpha20")
val CARDVIEW = Version("1.1.0-alpha01")
val CAR_APP = Version("1.0.0-alpha01")
val COLLECTION = Version("1.2.0-alpha01")
val CONTENTPAGER = Version("1.1.0-alpha01")
- val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha08")
+ val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha09")
val COORDINATORLAYOUT = Version("1.2.0-alpha01")
val CORE = Version("1.5.0-alpha06")
val CORE_ANIMATION = Version("1.0.0-alpha03")
@@ -75,16 +75,16 @@
val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
val LEGACY = Version("1.1.0-alpha01")
val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
- val LIFECYCLE = Version("2.3.0-beta01")
+ val LIFECYCLE = Version("2.3.0-rc01")
val LIFECYCLE_EXTENSIONS = Version("2.2.0")
val LOADER = Version("1.2.0-alpha01")
val MEDIA = Version("1.3.0-alpha02")
val MEDIA2 = Version("1.2.0-alpha01")
val MEDIAROUTER = Version("1.3.0-alpha01")
val NAVIGATION = Version("2.4.0-alpha01")
- val NAVIGATION_COMPOSE = Version("1.0.0-alpha03")
+ val NAVIGATION_COMPOSE = Version("1.0.0-alpha04")
val PAGING = Version("3.0.0-alpha10")
- val PAGING_COMPOSE = Version("1.0.0-alpha03")
+ val PAGING_COMPOSE = Version("1.0.0-alpha04")
val PALETTE = Version("1.1.0-alpha01")
val PRINT = Version("1.1.0-beta01")
val PERCENTLAYOUT = Version("1.1.0-alpha01")
@@ -94,7 +94,7 @@
val RECYCLERVIEW_SELECTION = Version("2.0.0-alpha01")
val REMOTECALLBACK = Version("1.0.0-alpha02")
val ROOM = Version("2.3.0-alpha04")
- val SAVEDSTATE = Version("1.1.0-beta01")
+ val SAVEDSTATE = Version("1.1.0-rc01")
val SECURITY = Version("1.1.0-alpha03")
val SECURITY_BIOMETRIC = Version("1.0.0-alpha01")
val SECURITY_IDENTITY_CREDENTIAL = Version("1.0.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index a29179c..e764c62 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -91,7 +91,6 @@
isCheckReleaseBuilds = false
// Write output directly to the console (and nowhere else).
- textOutput("stderr")
textReport = true
htmlReport = false
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 02db235..d8ba393 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -98,7 +98,7 @@
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.16"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.18"
val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 5bfdf18..c92d780 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -47,8 +47,6 @@
"createArchive",
"createDiffArchiveForAll",
"createProjectZip",
- "desugarPublicDebugFileDependencies",
- "desugarTipOfTreeDebugFileDependencies",
"externalNativeBuildDebug",
"externalNativeBuildRelease",
"generateJsonModelDebug",
@@ -107,10 +105,7 @@
*/
"relocateShadowJar",
"stripArchiveForPartialDejetification",
- "transformClassesWithDexBuilderForPublicDebug",
- "transformClassesWithDexBuilderForTipOfTreeDebug",
"verifyDependencyVersions",
- "zipEcFiles",
"zipTestConfigsWithApks",
":camera:integration-tests:camera-testapp-core:mergeLibDexDebug",
@@ -123,10 +118,6 @@
":camera:integration-tests:camera-testapp-view:GenerateTestConfigurationdebugAndroidTest",
":camera:integration-tests:camera-testapp-view:mergeLibDexDebug",
":camera:integration-tests:camera-testapp-view:packageDebug",
-
- ":inspection:inspection-gradle-plugin:generatePomFileForInspectionPluginMarkerMavenPublication",
- ":inspection:inspection-gradle-plugin:" +
- "publishInspectionPluginMarkerMavenPublicationToMavenRepository"
)
// Additional tasks that are expected to be temporarily out-of-date after running once
diff --git a/busytown/androidx_max_dep_versions.sh b/busytown/androidx_max_dep_versions.sh
index 5dc8e64..aee0689 100755
--- a/busytown/androidx_max_dep_versions.sh
+++ b/busytown/androidx_max_dep_versions.sh
@@ -5,6 +5,8 @@
cd "$(dirname $0)"
-impl/build.sh --no-daemon assembleDebug assembleAndroidTest -PuseMaxDepVersions --offline "$@"
+impl/build.sh --no-daemon --offline assembleDebug assembleAndroidTest \
+ -PuseMaxDepVersions \
+ -Pandroidx.validateNoUnrecognizedMessages "$@"
echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index fe1548c..30e18b6 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -18,6 +18,7 @@
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static androidx.build.dependencies.DependenciesKt.*
@@ -25,6 +26,7 @@
id("AndroidXPlugin")
id("com.android.library")
id("kotlin-android")
+ id("kotlin-kapt")
}
apply from: "../camera-camera2-pipe/dependencies.gradle"
@@ -39,14 +41,21 @@
releaseBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportRelease"))
debugBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportDebug"))
+ // Classes and types that are needed at compile & runtime
api("androidx.annotation:annotation:1.1.0")
api(project(":camera:camera-core"))
+
+ // Classes and types that are only needed at runtime
+ implementation(project(":lifecycle:lifecycle-livedata-ktx"))
+ implementation(KOTLIN_COROUTINES_GUAVA)
implementation(KOTLIN_STDLIB)
// Since we jarjar CameraPipe, include the transitive dependencies as implementation
implementation CAMERA_PIPE_DEPS.API
implementation CAMERA_PIPE_DEPS.IMPLEMENTATION
+ kapt(DAGGER_COMPILER)
+
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
@@ -63,6 +72,20 @@
}
}
+kapt {
+ javacOptions {
+ option("-Adagger.fastInit=enabled")
+ option("-Adagger.fullBindingGraphValidation=ERROR")
+ }
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+ }
+}
+
androidx {
name = "Jetpack Camera Camera Pipe Integration Library"
publish = Publish.NONE
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
index 473559c..bb925bd 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
@@ -16,9 +16,9 @@
package androidx.camera.camera2.pipe.integration
import androidx.camera.core.CameraXConfig
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeFactory
-import androidx.camera.camera2.pipe.integration.impl.StreamConfigurationMap
-import androidx.camera.camera2.pipe.integration.impl.UseCaseConfigurationMap
+import androidx.camera.camera2.pipe.integration.adapter.CameraFactoryAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraSurfaceAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
/**
* Convenience class for generating a pre-populated CameraPipe based [CameraXConfig].
@@ -29,9 +29,9 @@
*/
fun defaultConfig(): CameraXConfig {
return CameraXConfig.Builder()
- .setCameraFactoryProvider(::CameraPipeFactory)
- .setDeviceSurfaceManagerProvider(::StreamConfigurationMap)
- .setUseCaseConfigFactoryProvider(::UseCaseConfigurationMap)
+ .setCameraFactoryProvider(::CameraFactoryAdapter)
+ .setDeviceSurfaceManagerProvider(::CameraSurfaceAdapter)
+ .setUseCaseConfigFactoryProvider(::CameraUseCaseAdapter)
.build()
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
new file mode 100644
index 0000000..dcf14ed5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.FocusMeteringResult
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Adapt the [CameraControlInternal] interface to [CameraPipe].
+ *
+ * This controller class maintains state as use-cases are attached / detached from the camera as
+ * well as providing access to other utility methods. The primary purpose of this class it to
+ * forward these interactions to the currently configured [UseCaseCamera].
+ */
+@CameraScope
+@OptIn(ExperimentalCoroutinesApi::class)
+class CameraControlAdapter @Inject constructor(
+ lazyCameraMetadata: Provider<CameraMetadata>,
+ private val cameraScope: CoroutineScope,
+ private val cameraState: CameraState,
+ private val useCaseManager: UseCaseManager
+) : CameraControlInternal {
+ private val cameraMetadata by lazy { lazyCameraMetadata.get() }
+ private var interopConfig: Config = MutableOptionsBundle.create()
+ private var imageCaptureFlashMode: Int = ImageCapture.FLASH_MODE_OFF
+
+ override fun getSensorRect(): Rect {
+ return cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]!!
+ }
+
+ override fun addInteropConfig(config: Config) {
+ interopConfig = Config.mergeConfigs(config, interopConfig)
+ }
+
+ override fun clearInteropConfig() {
+ interopConfig = MutableOptionsBundle.create()
+ }
+
+ override fun getInteropConfig(): Config {
+ return interopConfig
+ }
+
+ override fun enableTorch(torch: Boolean): ListenableFuture<Void> {
+ // Launch UNDISPATCHED to preserve interaction order with the camera.
+ return cameraScope.launchAsVoidFuture(start = CoroutineStart.UNDISPATCHED) {
+ useCaseManager.camera?.let {
+ // Tell the camera to turn the torch on / off.
+ val result = it.enableTorchAsync(torch)
+
+ // Update the torch state.
+ withContext(Dispatchers.Main) {
+ cameraState.torchState.value = when (torch) {
+ true -> TorchState.ON
+ false -> TorchState.OFF
+ }
+ }
+
+ // Wait until the command is received by the camera.
+ result.await()
+ }
+ }
+ }
+
+ override fun startFocusAndMetering(
+ action: FocusMeteringAction
+ ): ListenableFuture<FocusMeteringResult> {
+ warn { "TODO: startFocusAndMetering is not yet supported" }
+ return Futures.immediateFuture(FocusMeteringResult.emptyInstance())
+ }
+
+ override fun cancelFocusAndMetering(): ListenableFuture<Void> {
+ warn { "TODO: cancelFocusAndMetering is not yet supported" }
+ return Futures.immediateFuture(null)
+ }
+
+ override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
+ // Note: The current implementation waits until the update has been *submitted* to the
+ // camera, but does not wait for the total capture result.
+ warn { "TODO: setZoomRatio is not yet supported" }
+ return Futures.immediateFuture(null)
+ }
+
+ override fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
+ warn { "TODO: setLinearZoom is not yet supported" }
+ return Futures.immediateFuture(null)
+ }
+
+ override fun getFlashMode(): Int {
+ return imageCaptureFlashMode
+ }
+
+ override fun setFlashMode(flashMode: Int) {
+ warn { "TODO: setFlashMode is not yet supported" }
+ this.imageCaptureFlashMode = flashMode
+ }
+
+ override fun triggerAf(): ListenableFuture<CameraCaptureResult> {
+ warn { "TODO: triggerAf is not yet supported" }
+ return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+ }
+
+ override fun triggerAePrecapture(): ListenableFuture<CameraCaptureResult> {
+ warn { "TODO: triggerAePrecapture is not yet supported" }
+ return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+ }
+
+ override fun cancelAfAeTrigger(cancelAfTrigger: Boolean, cancelAePrecaptureTrigger: Boolean) {
+ warn { "TODO: cancelAfAeTrigger is not yet supported" }
+ }
+
+ @SuppressLint("UnsafeExperimentalUsageError")
+ override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> {
+ warn { "TODO: setExposureCompensationIndex is not yet supported" }
+ return Futures.immediateFuture(exposure)
+ }
+
+ override fun submitCaptureRequests(captureConfigs: MutableList<CaptureConfig>) {
+ warn { "TODO: submitCaptureRequests is not yet supported" }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
new file mode 100644
index 0000000..120f8b9
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.impl.Debug
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Timestamps
+import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
+import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.config.CameraAppConfig
+import androidx.camera.camera2.pipe.integration.config.DaggerCameraAppComponent
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.CameraThreadConfig
+
+/**
+ * The [CameraFactoryAdapter] is responsible for creating the root dagger component that is used
+ * to share resources across Camera instances.
+ */
+class CameraFactoryAdapter(
+ context: Context,
+ threadConfig: CameraThreadConfig,
+ availableCamerasSelector: CameraSelector?
+) : CameraFactory {
+ private val appComponent: CameraAppComponent by lazy {
+ Debug.traceStart { "CameraFactoryAdapter#appComponent" }
+ val start = Timestamps.now()
+ val result = DaggerCameraAppComponent.builder()
+ .config(CameraAppConfig(context, threadConfig))
+ .build()
+ debug { "Created CameraFactoryAdapter in ${start.measureNow().formatMs()}" }
+ debug { "availableCamerasSelector: $availableCamerasSelector " }
+ Debug.traceStop()
+ result
+ }
+
+ init {
+ debug { "Created CameraFactoryAdapter" }
+ }
+
+ override fun getCamera(cameraId: String): CameraInternal =
+ appComponent.cameraBuilder()
+ .config(CameraConfig(CameraId(cameraId)))
+ .build()
+ .getCameraInternal()
+
+ override fun getAvailableCameraIds(): Set<String> = appComponent.getAvailableCameraIds()
+ override fun getCameraManager(): Any? = appComponent
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
new file mode 100644
index 0000000..442db04
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.lifecycle.LiveData
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Adapt the [CameraInfoInternal] interface to [CameraPipe].
+ */
+@SuppressLint(
+ "UnsafeExperimentalUsageError" // Suppressed due to experimental ExposureState
+)
+@CameraScope
+class CameraInfoAdapter @Inject constructor(
+ private val lazyCameraMetadata: Provider<CameraMetadata>,
+ private val cameraConfig: CameraConfig,
+ private val cameraState: CameraState,
+ private val cameraCallbackMap: CameraCallbackMap
+) : CameraInfoInternal {
+
+ private val cameraMetadata: CameraMetadata
+ get() = lazyCameraMetadata.get()
+
+ override fun getCameraId(): String = cameraConfig.cameraId.value
+ override fun getLensFacing(): Int? = cameraMetadata[CameraCharacteristics.LENS_FACING]
+ override fun getSensorRotationDegrees(): Int = getSensorRotationDegrees(Surface.ROTATION_0)
+ override fun hasFlashUnit(): Boolean =
+ cameraMetadata[CameraCharacteristics.FLASH_INFO_AVAILABLE]!!
+
+ override fun getSensorRotationDegrees(relativeRotation: Int): Int {
+ val sensorOrientation: Int = cameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION]!!
+ val relativeRotationDegrees =
+ CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation)
+ // Currently this assumes that a back-facing camera is always opposite to the screen.
+ // This may not be the case for all devices, so in the future we may need to handle that
+ // scenario.
+ val lensFacing = lensFacing
+ val isOppositeFacingScreen =
+ lensFacing != null && CameraSelector.LENS_FACING_BACK == lensFacing
+ return CameraOrientationUtil.getRelativeImageRotation(
+ relativeRotationDegrees,
+ sensorOrientation,
+ isOppositeFacingScreen
+ )
+ }
+
+ override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomState
+ override fun getTorchState(): LiveData<Int> = cameraState.torchState
+ @SuppressLint("UnsafeExperimentalUsageError")
+ override fun getExposureState(): ExposureState = cameraState.exposureState.value!!
+
+ override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
+ cameraCallbackMap.addCaptureCallback(callback, executor)
+ override fun removeSessionCaptureCallback(callback: CameraCaptureCallback) =
+ cameraCallbackMap.removeCaptureCallback(callback)
+
+ override fun getImplementationType(): String = "CameraPipe"
+ override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
new file mode 100644
index 0000000..3174c2c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.LiveDataObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.atomicfu.atomic
+import javax.inject.Inject
+
+internal val defaultQuirks = Quirks(emptyList())
+internal val cameraAdapterIds = atomic(0)
+
+/**
+ * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
+ */
+@CameraScope
+class CameraInternalAdapter @Inject constructor(
+ config: CameraConfig,
+ private val useCaseManager: UseCaseManager,
+ private val cameraInfo: CameraInfoInternal,
+ private val cameraController: CameraControlInternal
+) : CameraInternal {
+ private val cameraId = config.cameraId
+ private val debugId = cameraAdapterIds.incrementAndGet()
+ private val cameraState = LiveDataObservable<CameraInternal.State>()
+
+ init {
+ cameraState.postValue(CameraInternal.State.CLOSED)
+
+ debug { "Created $this for $cameraId" }
+ // TODO: Consider preloading the list of camera ids and metadata.
+ }
+
+ override fun getCameraQuirks(): Quirks {
+ warn { "TODO: Quirks are not yet supported." }
+ return defaultQuirks
+ }
+
+ // Load / unload methods
+ override fun open() {
+ debug { "$this#open" }
+ }
+
+ override fun close() {
+ debug { "$this#close" }
+ }
+
+ override fun release(): ListenableFuture<Void> {
+ warn { "$this#release is not yet implemented." }
+ // TODO: Determine what the correct way to invoke release is.
+ return Futures.immediateFuture(null)
+ }
+
+ override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
+ override fun getCameraState(): Observable<CameraInternal.State> = cameraState
+ override fun getCameraControlInternal(): CameraControlInternal = cameraController
+
+ // UseCase attach / detach behaviors.
+ override fun attachUseCases(useCasesToAdd: MutableCollection<UseCase>) {
+ useCaseManager.attach(useCasesToAdd.toList())
+ }
+
+ override fun detachUseCases(useCasesToRemove: MutableCollection<UseCase>) {
+ useCaseManager.detach(useCasesToRemove.toList())
+ }
+
+ // UseCase state callbacks
+ override fun onUseCaseActive(useCase: UseCase) {
+ useCaseManager.enable(useCase)
+ }
+
+ override fun onUseCaseUpdated(useCase: UseCase) {
+ useCaseManager.update(useCase)
+ }
+
+ override fun onUseCaseReset(useCase: UseCase) {
+ useCaseManager.update(useCase)
+ }
+
+ override fun onUseCaseInactive(useCase: UseCase) {
+ useCaseManager.disable(useCase)
+ }
+
+ override fun toString(): String = "CameraInternalAdapter<$cameraId>"
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
new file mode 100644
index 0000000..d026e65
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.SurfaceConfig.ConfigSize
+import androidx.camera.core.impl.UseCaseConfig
+
+internal val MAXIMUM_PREVIEW_SIZE = Size(1920, 1080)
+
+/**
+ * Adapt the [CameraDeviceSurfaceManager] interface to [CameraPipe].
+ *
+ * This class provides Context-specific utility methods for querying and computing supported
+ * outputs.
+ */
+class CameraSurfaceAdapter(
+ context: Context,
+ cameraComponent: Any?,
+ availableCameraIds: Set<String>
+) : CameraDeviceSurfaceManager {
+ private val component = cameraComponent as CameraAppComponent
+
+ init {
+ debug { "AvailableCameraIds = $availableCameraIds" }
+ debug { "Created StreamConfigurationMap from $context" }
+ }
+
+ override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
+ // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
+ // guaranteed stream configurations for this camera's support level.
+ return component.getAvailableCameraIds().contains(cameraId)
+ }
+
+ override fun transformSurfaceConfig(
+ cameraId: String,
+ imageFormat: Int,
+ size: Size
+ ): SurfaceConfig? {
+ // TODO: Many of the "find a stream combination that will work" is already provided by the
+ // existing camera2 implementation, and this implementation should leverage that work.
+
+ val configType = when (imageFormat) {
+ ImageFormat.YUV_420_888 -> SurfaceConfig.ConfigType.YUV
+ ImageFormat.JPEG -> SurfaceConfig.ConfigType.JPEG
+ ImageFormat.RAW_SENSOR -> SurfaceConfig.ConfigType.RAW
+ else -> SurfaceConfig.ConfigType.PRIV
+ }
+
+ val configSize = ConfigSize.PREVIEW
+ return SurfaceConfig.create(configType, configSize)
+ }
+
+ override fun getSuggestedResolutions(
+ cameraId: String,
+ existingSurfaces: List<SurfaceConfig>,
+ newUseCaseConfigs: List<UseCaseConfig<*>?>
+ ): Map<UseCaseConfig<*>, Size> {
+ // TODO: Many of the "find a stream combination that will work" is already provided by the
+ // existing camera2 implementation, and this implementation should leverage that work.
+
+ val sizes: MutableMap<UseCaseConfig<*>, Size> = mutableMapOf()
+ for (config in newUseCaseConfigs) {
+ sizes[config as UseCaseConfig<*>] = MAXIMUM_PREVIEW_SIZE
+ }
+ return sizes
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
new file mode 100644
index 0000000..dcbcba5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.camera2.CameraDevice
+import android.util.Size
+import android.view.Display
+import android.view.WindowManager
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.camera2.pipe.integration.impl.asLandscape
+import androidx.camera.camera2.pipe.integration.impl.minByArea
+import androidx.camera.camera2.pipe.integration.impl.toSize
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory
+
+/**
+ * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
+ *
+ * This includes things like default template and session parameters, as well as maximum resolution
+ * and aspect ratios for the display.
+ */
+class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
+
+ private val display: Display by lazy {
+ @Suppress("deprecation")
+ (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay!!
+ }
+
+ init {
+ if (context === context.applicationContext) {
+ info {
+ "The provided context ($context) is application scoped and will be used to infer " +
+ "the default display for computing the default preview size, orientation, " +
+ "and default aspect ratio for UseCase outputs."
+ }
+ }
+ debug { "Created UseCaseConfigurationMap" }
+ }
+
+ /**
+ * Returns the configuration for the given capture type, or `null` if the
+ * configuration cannot be produced.
+ */
+ override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
+ debug { "Creating config for $captureType" }
+
+ val mutableConfig = MutableOptionsBundle.create()
+ val sessionBuilder = SessionConfig.Builder()
+ // TODO(b/114762170): Must set to preview here until we allow for multiple template
+ // types
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+ mutableConfig.insertOption(
+ UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG,
+ sessionBuilder.build()
+ )
+ val captureBuilder = CaptureConfig.Builder()
+ when (captureType) {
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE ->
+ captureBuilder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+ UseCaseConfigFactory.CaptureType.PREVIEW,
+ UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS,
+ UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE ->
+ captureBuilder.templateType = CameraDevice.TEMPLATE_PREVIEW
+ }
+ mutableConfig.insertOption(
+ UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG,
+ captureBuilder.build()
+ )
+
+ // Only CAPTURE_TYPE_IMAGE_CAPTURE has its own ImageCaptureOptionUnpacker. Other
+ // capture types all use the standard Camera2CaptureOptionUnpacker.
+ mutableConfig.insertOption(
+ UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER,
+ DefaultCaptureOptionsUnpacker
+ )
+ mutableConfig.insertOption(
+ UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER,
+ DefaultSessionOptionsUnpacker
+ )
+
+ if (captureType == UseCaseConfigFactory.CaptureType.PREVIEW) {
+ mutableConfig.insertOption(
+ ImageOutputConfig.OPTION_MAX_RESOLUTION,
+ getPreviewSize()
+ )
+ }
+
+ mutableConfig.insertOption(
+ ImageOutputConfig.OPTION_TARGET_ROTATION,
+ display.rotation
+ )
+ return OptionsBundle.from(mutableConfig)
+ }
+
+ /**
+ * Returns the device's screen resolution, or 1080p, whichever is smaller.
+ */
+ private fun getPreviewSize(): Size? {
+ val displaySize = Point()
+ display.getRealSize(displaySize)
+ return minByArea(MAXIMUM_PREVIEW_SIZE, displaySize.toSize().asLandscape())
+ }
+
+ object DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
+ override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
+ // Unused.
+ }
+ }
+
+ object DefaultSessionOptionsUnpacker : SessionConfig.OptionUnpacker {
+ override fun unpack(config: UseCaseConfig<*>, builder: SessionConfig.Builder) {
+ // Unused.
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
new file mode 100644
index 0000000..63f471f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.hardware.camera2.CaptureResult
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
+import androidx.camera.core.impl.CameraCaptureMetaData.AeState
+import androidx.camera.core.impl.CameraCaptureMetaData.AfMode
+import androidx.camera.core.impl.CameraCaptureMetaData.AfState
+import androidx.camera.core.impl.CameraCaptureMetaData.AwbState
+import androidx.camera.core.impl.CameraCaptureMetaData.FlashState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.TagBundle
+
+/**
+ * Adapts the [CameraCaptureResult] interface to [CameraPipe].
+ */
+class CaptureResultAdapter(
+ private val requestMetadata: RequestMetadata,
+ private val frameNumber: FrameNumber,
+ private val result: FrameInfo
+) : CameraCaptureResult {
+ override fun getAfMode(): AfMode =
+ when (val mode = result.metadata[CaptureResult.CONTROL_AF_MODE]) {
+ CaptureResult.CONTROL_AF_MODE_OFF,
+ CaptureResult.CONTROL_AF_MODE_EDOF -> AfMode.OFF
+ CaptureResult.CONTROL_AF_MODE_AUTO,
+ CaptureResult.CONTROL_AF_MODE_MACRO -> AfMode.ON_MANUAL_AUTO
+ CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO -> AfMode.ON_CONTINUOUS_AUTO
+ null -> AfMode.UNKNOWN
+ else -> {
+ Log.debug { "Unknown AF mode ($mode) for $frameNumber!" }
+ AfMode.UNKNOWN
+ }
+ }
+
+ override fun getAfState(): AfState =
+ when (val state = result.metadata[CaptureResult.CONTROL_AF_STATE]) {
+ CaptureResult.CONTROL_AF_STATE_INACTIVE -> AfState.INACTIVE
+ CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> AfState.SCANNING
+ CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> AfState.LOCKED_FOCUSED
+ CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> AfState.LOCKED_NOT_FOCUSED
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> AfState.FOCUSED
+ null -> AfState.UNKNOWN
+ else -> {
+ Log.debug { "Unknown AF state ($state) for $frameNumber!" }
+ AfState.UNKNOWN
+ }
+ }
+
+ override fun getAeState(): AeState =
+ when (val state = result.metadata[CaptureResult.CONTROL_AE_STATE]) {
+ CaptureResult.CONTROL_AE_STATE_INACTIVE -> AeState.INACTIVE
+ CaptureResult.CONTROL_AE_STATE_SEARCHING,
+ CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> AeState.SEARCHING
+ CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> AeState.FLASH_REQUIRED
+ CaptureResult.CONTROL_AE_STATE_CONVERGED -> AeState.CONVERGED
+ CaptureResult.CONTROL_AE_STATE_LOCKED -> AeState.LOCKED
+ null -> AeState.UNKNOWN
+ else -> {
+ Log.debug { "Unknown AE state ($state) for $frameNumber!" }
+ AeState.UNKNOWN
+ }
+ }
+
+ override fun getAwbState(): AwbState =
+ when (val state = result.metadata[CaptureResult.CONTROL_AWB_STATE]) {
+ CaptureResult.CONTROL_AWB_STATE_INACTIVE -> AwbState.INACTIVE
+ CaptureResult.CONTROL_AWB_STATE_SEARCHING -> AwbState.METERING
+ CaptureResult.CONTROL_AWB_STATE_CONVERGED -> AwbState.CONVERGED
+ CaptureResult.CONTROL_AWB_STATE_LOCKED -> AwbState.LOCKED
+ null -> AwbState.UNKNOWN
+ else -> {
+ Log.debug { "Unknown AWB state ($state) for $frameNumber!" }
+ AwbState.UNKNOWN
+ }
+ }
+
+ override fun getFlashState(): FlashState =
+ when (val state = result.metadata[CaptureResult.FLASH_STATE]) {
+ CaptureResult.FLASH_STATE_UNAVAILABLE,
+ CaptureResult.FLASH_STATE_CHARGING -> FlashState.NONE
+ CaptureResult.FLASH_STATE_READY -> FlashState.READY
+ CaptureResult.FLASH_STATE_FIRED,
+ CaptureResult.FLASH_STATE_PARTIAL -> FlashState.FIRED
+ null -> FlashState.UNKNOWN
+ else -> {
+ Log.debug { "Unknown flash state ($state) for $frameNumber!" }
+ FlashState.UNKNOWN
+ }
+ }
+
+ override fun getTimestamp(): Long {
+ return result.metadata.getOrDefault(CaptureResult.SENSOR_TIMESTAMP, -1L)
+ }
+
+ override fun getTagBundle(): TagBundle {
+ return requestMetadata.getOrDefault(CAMERAX_TAG_BUNDLE, TagBundle.emptyBundle())
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
new file mode 100644
index 0000000..96c3dc8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.guava.asListenableFuture
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * This allows a java method that returns a ListenableFuture<Void> to be implemented by calling a
+ * suspend function.
+ *
+ * Exceptions thrown from the coroutine scope are propagated to the returned future.
+ * Canceling the future will attempt to cancel the coroutine.
+ */
+fun CoroutineScope.launchAsVoidFuture(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend CoroutineScope.() -> Unit
+): ListenableFuture<Void> {
+ // TODO: This method currently uses guava.asListenableFuture. This may be an expensive
+ // dependency to take on. We may need to evaluate this.
+ @Suppress("UNCHECKED_CAST")
+ return this.async(context = context, start = start, block = block)
+ .asListenableFuture() as ListenableFuture<Void>
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
new file mode 100644
index 0000000..bb05d8d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.util.Range
+import android.util.Rational
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ExposureState
+
+internal val EMPTY_RANGE = Range(0, 0)
+
+/** Adapt [ExposureState] to a [CameraMetadata] instance. */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ExposureStateAdapter(
+ private val cameraMetadata: CameraMetadata,
+ private val exposureCompensation: Int
+) : ExposureState {
+ override fun isExposureCompensationSupported(): Boolean {
+ val range = exposureCompensationRange
+ return range.lower != 0 && range.upper != 0
+ }
+
+ override fun getExposureCompensationIndex(): Int = exposureCompensation
+ override fun getExposureCompensationStep(): Rational {
+ if (!isExposureCompensationSupported) {
+ return Rational.ZERO
+ }
+ return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP]!!
+ }
+
+ override fun getExposureCompensationRange(): Range<Int> {
+ return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE]
+ ?: EMPTY_RANGE
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
new file mode 100644
index 0000000..4d42273
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ZoomState
+
+/**
+ * Adapt [ZoomState] to a [CameraMetadata] instance.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ZoomStateAdapter(
+ cameraMetadata: CameraMetadata,
+ private val zoomRatio: Float
+) : ZoomState {
+ // TODO: Zoom state has API-specific compat requirements. This is a placeholder for newer
+ // android API versions and will require compat changes to support older versions.
+ private val zoomRange = cameraMetadata[CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE]!!
+
+ override fun getMaxZoomRatio(): Float = zoomRange.upper as Float
+ override fun getMinZoomRatio(): Float = zoomRange.lower as Float
+ override fun getLinearZoom(): Float {
+ val range = zoomRange.upper - zoomRange.lower
+ if (range > 0) {
+ return (zoomRatio - zoomRange.lower) / range
+ }
+ return 1.0f
+ }
+ override fun getZoomRatio(): Float = zoomRatio
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
index 8a6dfd3..f7a9ea2 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
@@ -13,17 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.compose.ui.test
-import org.jetbrains.skiko.Library
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.adapter;
-fun initCompose() {
- ComposeInit
-}
-
-private object ComposeInit {
- init {
- Library.load("/", "skiko")
- System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
- }
-}
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
new file mode 100644
index 0000000..d8eadf0
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.camera.camera2.pipe.integration.config
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.CameraFactory
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/** Dependency bindings for adapting a [CameraFactory] instance to [CameraPipe] */
+@Module(
+ subcomponents = [CameraComponent::class]
+)
+abstract class CameraAppModule {
+ companion object {
+ @Singleton
+ @Provides
+ fun provideCameraPipe(context: Context): CameraPipe {
+ return CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
+ }
+
+ @Provides
+ fun provideAvailableCameraIds(cameraPipe: CameraPipe): Set<String> {
+ return cameraPipe.cameras().findAll().map { it.value }.toSet()
+ }
+ }
+}
+
+/** Configuration properties that are shared across this app process */
+@Module
+class CameraAppConfig(
+ private val context: Context,
+ private val threadConfig: CameraThreadConfig
+) {
+ @Provides
+ fun provideContext(): Context = context
+}
+
+/** Dagger component for Application (Process) scoped dependencies. */
+@Singleton
+@Component(
+ modules = [
+ CameraAppModule::class,
+ CameraAppConfig::class
+ ]
+)
+interface CameraAppComponent {
+ fun cameraBuilder(): CameraComponent.Builder
+ fun getAvailableCameraIds(): Set<String>
+
+ @Component.Builder
+ interface Builder {
+ fun config(config: CameraAppConfig): Builder
+ fun build(): CameraAppComponent
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
new file mode 100644
index 0000000..803d605
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.camera.camera2.pipe.integration.config
+
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInternalAdapter
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import javax.inject.Scope
+
+@Scope
+annotation class CameraScope
+
+/** Dependency bindings for adapting an individual [CameraInternal] instance to [CameraPipe] */
+@Module(
+ subcomponents = [UseCaseCameraComponent::class]
+)
+abstract class CameraModule {
+ companion object {
+ @CameraScope
+ @Provides
+ fun provideCameraCoroutineScope(cameraConfig: CameraConfig): CoroutineScope {
+ // TODO: Dispatchers.Default is the standard kotlin coroutine executor for background
+ // work, but we may want to pass something in.
+ return CoroutineScope(
+ Job() +
+ Dispatchers.Default +
+ CoroutineName("CXCP-Camera-${cameraConfig.cameraId.value}")
+ )
+ }
+
+ @Provides
+ fun provideCameraMetadata(cameraPipe: CameraPipe, config: CameraConfig): CameraMetadata =
+ cameraPipe.cameras().awaitMetadata(config.cameraId)
+ }
+
+ @Binds
+ abstract fun bindCameraInternal(adapter: CameraInternalAdapter): CameraInternal
+
+ @Binds
+ abstract fun bindCameraInfoInternal(adapter: CameraInfoAdapter): CameraInfoInternal
+
+ @Binds
+ abstract fun bindCameraControlInternal(adapter: CameraControlAdapter): CameraControlInternal
+}
+
+/** Configuration properties used when creating a [CameraInternal] instance. */
+@Module
+class CameraConfig(val cameraId: CameraId) {
+ @Provides
+ fun provideCameraConfig(): CameraConfig = this
+}
+
+/** Dagger subcomponent for a single [CameraInternal] instance. */
+@CameraScope
+@Subcomponent(
+ modules = [
+ CameraModule::class,
+ CameraConfig::class
+ ]
+)
+interface CameraComponent {
+ @Subcomponent.Builder
+ interface Builder {
+ fun config(config: CameraConfig): Builder
+ fun build(): CameraComponent
+ }
+
+ fun getCameraInternal(): CameraInternal
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
new file mode 100644
index 0000000..8ea76cf
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.camera.camera2.pipe.integration.config
+
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.UseCase
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import javax.inject.Scope
+
+@Scope
+annotation class UseCaseCameraScope
+
+/** Dependency bindings for building a [UseCaseCamera] */
+@Module(includes = [UseCaseCamera.Bindings::class])
+abstract class UseCaseCameraModule {
+ // Used for dagger provider methods that are static.
+ companion object
+}
+
+/** Dagger module for binding the [UseCase]'s to the [UseCaseCamera]. */
+@Module
+class UseCaseCameraConfig(
+ private val useCases: List<UseCase>
+) {
+ @UseCaseCameraScope
+ @Provides
+ fun provideUseCaseList(): java.util.ArrayList<UseCase> {
+ return java.util.ArrayList(useCases)
+ }
+}
+
+/** Dagger subcomponent for a single [UseCaseCamera] instance. */
+@UseCaseCameraScope
+@Subcomponent(
+ modules = [
+ UseCaseCameraModule::class,
+ UseCaseCameraConfig::class
+ ]
+)
+interface UseCaseCameraComponent {
+ fun getUseCaseCamera(): UseCaseCamera
+
+ @Subcomponent.Builder
+ interface Builder {
+ fun config(config: UseCaseCameraConfig): Builder
+ fun build(): UseCaseCameraComponent
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
index 8a6dfd3..b7d2f6a 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
@@ -13,17 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.compose.ui.test
-import org.jetbrains.skiko.Library
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.config;
-fun initCompose() {
- ComposeInit
-}
-
-private object ComposeInit {
- init {
- Library.load("/", "skiko")
- System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
- }
-}
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
deleted file mode 100644
index 3d24397..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
+++ /dev/null
@@ -1,101 +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.camera.camera2.pipe.integration.impl
-
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CameraInfoInternal
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.Quirks
-import androidx.camera.core.impl.utils.futures.Futures
-import com.google.common.util.concurrent.ListenableFuture
-
-/**
- * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
- */
-class CameraAdaptor(
- private val cameraPipe: CameraPipe,
- private val cameraId: CameraId
-) : CameraInternal {
-
- init {
- debug { "Created CameraAdaptor from $cameraPipe for $cameraId" }
- // TODO: Consider preloading the list of camera ids and metadata.
- }
-
- // Load / unload methods
- override fun open() {
- TODO("Not yet implemented")
- }
-
- override fun close() {
- TODO("Not yet implemented")
- }
-
- override fun release(): ListenableFuture<Void> {
- // TODO: Determine what the correct way to invoke release is.
- return Futures.immediateFuture(null)
- }
-
- // Static properties of this camera
- override fun getCameraInfoInternal(): CameraInfoInternal {
- TODO("Not yet implemented")
- }
-
- override fun getCameraQuirks(): Quirks {
- TODO("Not yet implemented")
- }
-
- // Controls for interacting with or observing the state of the camera.
- override fun getCameraState(): Observable<CameraInternal.State> {
- TODO("Not yet implemented")
- }
-
- override fun getCameraControlInternal(): CameraControlInternal {
- TODO("Not yet implemented")
- }
-
- // UseCase attach / detach behaviors.
- override fun attachUseCases(useCases: MutableCollection<UseCase>) {
- TODO("Not yet implemented")
- }
-
- override fun detachUseCases(useCases: MutableCollection<UseCase>) {
- TODO("Not yet implemented")
- }
-
- // UseCase state callbacks
- override fun onUseCaseActive(useCase: UseCase) {
- TODO("Not yet implemented")
- }
-
- override fun onUseCaseUpdated(useCase: UseCase) {
- TODO("Not yet implemented")
- }
-
- override fun onUseCaseReset(useCase: UseCase) {
- TODO("Not yet implemented")
- }
-
- override fun onUseCaseInactive(useCase: UseCase) {
- TODO("Not yet implemented")
- }
-}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
new file mode 100644
index 0000000..1619e7f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CaptureFailure
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.integration.adapter.CaptureResultAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * A map of [CameraCaptureCallback] that are invoked on each [Request].
+ */
+@CameraScope
+class CameraCallbackMap @Inject constructor() : Request.Listener {
+ private val callbackMap = mutableMapOf<CameraCaptureCallback, Executor>()
+ @Volatile
+ private var callbacks: Map<CameraCaptureCallback, Executor> = mapOf()
+
+ fun addCaptureCallback(callback: CameraCaptureCallback, executor: Executor) {
+ check(!callbacks.contains(callback)) { "$callback was already registered!" }
+
+ synchronized(callbackMap) {
+ callbackMap[callback] = executor
+ callbacks = callbackMap.toMap()
+ }
+ }
+
+ fun removeCaptureCallback(callback: CameraCaptureCallback) {
+ synchronized(callbackMap) {
+ callbackMap.remove(callback)
+ callbacks = callbackMap.toMap()
+ }
+ }
+
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo
+ ) {
+ val captureResult = CaptureResultAdapter(requestMetadata, frameNumber, result)
+ for ((callback, executor) in callbacks) {
+ executor.execute { callback.onCaptureCompleted(captureResult) }
+ }
+ }
+
+ override fun onFailed(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ captureFailure: CaptureFailure
+ ) {
+ val failure = CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR)
+ for ((callback, executor) in callbacks) {
+ executor.execute { callback.onCaptureFailed(failure) }
+ }
+ }
+
+ override fun onAborted(request: Request) {
+ for ((callback, executor) in callbacks) {
+ executor.execute { callback.onCaptureCancelled() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
deleted file mode 100644
index 071da5d..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
+++ /dev/null
@@ -1,77 +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.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.CameraThreadConfig
-
-/**
- * The CameraPipeCameraFactory is responsible for creating and configuring CameraPipe for CameraX.
- */
-class CameraPipeFactory(
- context: Context,
- threadConfig: CameraThreadConfig
-) : CameraFactory {
- // Lazily create and configure a CameraPipe instance.
- private val cameraPipe: CameraPipe by lazy {
- Debug.traceStart { "CameraPipeCameraFactory#cameraPipe" }
- val result: CameraPipe?
- val start = Timestamps.now()
-
- // TODO: CameraPipe should find a way to make sure callbacks are executed on the configured
- // executors that are provided in `threadConfig`
- debug { "TODO: Use $threadConfig if defined" }
-
- result = CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
- debug { "Created CameraPipe in ${start.measureNow().formatMs()}" }
- Debug.traceStop()
- result
- }
-
- init {
- debug { "Created CameraPipeCameraFactory" }
- // TODO: Consider preloading the list of camera ids and metadata.
- }
-
- override fun getCamera(cameraId: String): CameraInternal {
- // TODO: The CameraInternal object is an facade that covers most of the high level camera
- // state and interactions. CameraInternal objects are persistent across camera switches.
-
- return CameraAdaptor(cameraPipe, CameraId(cameraId))
- }
-
- override fun getAvailableCameraIds(): Set<String> {
- // TODO: This may need some amount of work to limit the returned values well behaved "Front"
- // and "Back" camera devices.
- return cameraPipe.cameras().findAll().map { it.value }.toSet()
- }
-
- override fun getCameraManager(): Any? {
- // Note: This object is passed around as an untyped parameter when constructing a few
- // objects (Such as `DeviceSurfaceManagerProvider`). It's better to rely on the parameter
- // passing than to try to turn this object into a singleton.
- return cameraPipe
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
new file mode 100644
index 0000000..e66deba
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.camera.camera2.pipe.integration.impl
+
+import android.annotation.SuppressLint
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.adapter.ExposureStateAdapter
+import androidx.camera.camera2.pipe.integration.adapter.ZoomStateAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.MutableLiveData
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * [CameraState] caches and updates based on callbacks from the active CameraGraph.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+@CameraScope
+class CameraState @Inject constructor(
+ private val cameraMetadata: Provider<CameraMetadata>,
+) {
+ val torchState = MutableLiveData<Int>()
+ val zoomState by lazy {
+ MutableLiveData<ZoomState>(
+ ZoomStateAdapter(
+ cameraMetadata.get(),
+ 1.0f
+ )
+ )
+ }
+ val exposureState by lazy {
+ MutableLiveData<ExposureState>(
+ ExposureStateAdapter(
+ cameraMetadata.get(),
+ 0
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
new file mode 100644
index 0000000..aecf6ab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.camera.camera2.pipe.integration.impl
+
+import android.graphics.Point
+import android.util.Size
+
+fun Size.area(): Int = this.width * this.height
+fun Size.asLandscape(): Size =
+ if (this.width >= this.height) this else Size(this.height, this.width)
+fun Size.asPortrait(): Size =
+ if (this.width <= this.height) this else Size(this.height, this.width)
+
+fun minByArea(left: Size, right: Size) = if (left.area() < right.area()) left else right
+fun maxByArea(left: Size, right: Size) = if (left.area() > right.area()) left else right
+
+fun Point.area(): Int = this.x * this.y
+fun Point.toSize() = Size(this.x, this.y)
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
deleted file mode 100644
index 56b5966..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
+++ /dev/null
@@ -1,64 +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.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import android.util.Size
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.SurfaceConfig
-import androidx.camera.core.impl.UseCaseConfig
-
-/**
- * Provide utilities for interacting with the set of guaranteed stream combinations.
- */
-class StreamConfigurationMap(context: Context, cameraManager: Any?) : CameraDeviceSurfaceManager {
- private val cameraPipe: CameraPipe = cameraManager as CameraPipe
-
- init {
- debug { "Created StreamConfigurationMap from $context" }
- }
-
- override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
- // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
- // guaranteed stream configurations for this camera's support level.
- return cameraPipe.cameras().findAll().contains(CameraId(cameraId))
- }
-
- override fun transformSurfaceConfig(
- cameraId: String,
- imageFormat: Int,
- size: Size
- ): SurfaceConfig? {
- // TODO: Many of the "find a stream combination that will work" is already provided by the
- // existing camera2 implementation, and this implementation should leverage that work.
-
- TODO("Not Implemented")
- }
-
- override fun getSuggestedResolutions(
- cameraId: String,
- existingSurfaces: List<SurfaceConfig>,
- newUseCaseConfigs: List<UseCaseConfig<*>?>
- ): Map<UseCaseConfig<*>, Size> {
- // TODO: Many of the "find a stream combination that will work" is already provided by the
- // existing camera2 implementation, and this implementation should leverage that work.
-
- TODO("Not Implemented")
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
similarity index 68%
rename from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
rename to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
index 8a6dfd3..75a1976 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
@@ -13,17 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.compose.ui.test
-import org.jetbrains.skiko.Library
+package androidx.camera.camera2.pipe.integration.impl
-fun initCompose() {
- ComposeInit
-}
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.core.impl.TagBundle
-private object ComposeInit {
- init {
- Library.load("/", "skiko")
- System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
- }
-}
\ No newline at end of file
+/** Custom tags that can be passed used by CameraPipe */
+public val CAMERAX_TAG_BUNDLE = Metadata.Key.create<TagBundle>("camerax.tag_bundle")
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
new file mode 100644
index 0000000..088855a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraDevice
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import dagger.Module
+import dagger.Provides
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+internal val useCaseCameraIds = atomic(0)
+
+/**
+ * API for interacting with a [CameraGraph] that has been configured with a set of [UseCase]'s
+ */
+class UseCaseCamera(
+ private val cameraGraph: CameraGraph,
+ private val useCases: List<UseCase>,
+ private val surfaceToStreamMap: Map<DeferrableSurface, StreamId>,
+ private val cameraScope: CoroutineScope
+) {
+ private val debugId = useCaseCameraIds.incrementAndGet()
+
+ private var _activeUseCases = setOf<UseCase>()
+
+ var activeUseCases: Set<UseCase>
+ get() = _activeUseCases
+ set(value) {
+ // Note: This may be called with the same set of values that was previously set. This
+ // is used as a signal to indicate the properties of the UseCase may have changed.
+ _activeUseCases = value
+ updateUseCases()
+ }
+
+ init {
+ debug { "Configured $this for $useCases" }
+ }
+
+ fun close() {
+ debug { "Closing $this" }
+ cameraGraph.close()
+ }
+
+ suspend fun enableTorchAsync(enabled: Boolean): Deferred<FrameNumber> {
+ return cameraGraph.acquireSession().use {
+ it.setTorch(
+ when (enabled) {
+ true -> TorchState.ON
+ false -> TorchState.OFF
+ }
+ )
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private fun updateUseCases() {
+ val repeatingStreamIds = mutableSetOf<StreamId>()
+ for (useCase in activeUseCases) {
+ val repeatingCapture = useCase.sessionConfig?.repeatingCaptureConfig
+ if (repeatingCapture != null) {
+ for (deferrableSurface in repeatingCapture.surfaces) {
+ val streamId = surfaceToStreamMap[deferrableSurface]
+ if (streamId != null) {
+ repeatingStreamIds.add(streamId)
+ }
+ }
+ }
+ }
+
+ // TODO: This needs to aggregate the current parameters and pass them to the request.
+
+ // In order to preserve ordering, this starts acquiring the session on the current thread,
+ // and will only switch to the cameraScope threads if it needs to suspend. This is important
+ // because access to the cameraGraph is well ordered, and if the coroutine suspends, it will
+ // resume in the order it accessed the cameraGraph.
+ cameraScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ cameraGraph.acquireSession().use {
+ it.setRepeating(
+ Request(
+ streams = repeatingStreamIds.toList()
+ )
+ )
+ }
+ }
+ }
+
+ override fun toString(): String = "UseCaseCamera-$debugId"
+
+ @Module
+ class Bindings {
+ companion object {
+ @UseCaseCameraScope
+ @Provides
+ fun provideCameraGraphController(
+ cameraPipe: CameraPipe,
+ useCases: java.util.ArrayList<UseCase>,
+ cameraConfig: CameraConfig,
+ callbackMap: CameraCallbackMap,
+ coroutineScope: CoroutineScope,
+ ): UseCaseCamera {
+ val streamConfigs = mutableListOf<StreamConfig>()
+ val useCaseMap = mutableMapOf<StreamConfig, UseCase>()
+
+ // TODO: This may need to combine outputs that are (or will) share the same output
+ // imageReader or surface. Right now, each UseCase gets its own [StreamConfig]
+ // TODO: useCases only have a single `attachedSurfaceResolution`, yet they have a
+ // list of deferrableSurfaces.
+ for (useCase in useCases) {
+ val config = StreamConfig(
+ size = useCase.attachedSurfaceResolution!!,
+ format = StreamFormat(useCase.imageFormat),
+ camera = cameraConfig.cameraId,
+ type = StreamType.SURFACE,
+ deferrable = false
+ )
+ streamConfigs.add(config)
+ useCaseMap[config] = useCase
+ }
+
+ // Build up a config (using TEMPLATE_PREVIEW by default)
+ val config = CameraGraph.Config(
+ camera = cameraConfig.cameraId,
+ streams = streamConfigs,
+ listeners = listOf(callbackMap),
+ template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+ )
+ val graph = cameraPipe.create(config)
+
+ val surfaceToStreamMap = mutableMapOf<DeferrableSurface, StreamId>()
+ for ((streamConfig, useCase) in useCaseMap) {
+ val stream = graph.streams[streamConfig]
+ val useCaseSessionConfig = useCase.sessionConfig
+
+ // TODO: UseCases have inconsistent opinions about how surfaces are handled,
+ // this code assumes only a single surface per UseCase.
+ val deferredSurfaces = useCaseSessionConfig?.surfaces
+ if (stream != null && deferredSurfaces != null && deferredSurfaces.size == 1) {
+ val deferredSurface = deferredSurfaces[0]
+ graph.setSurface(stream.id, deferredSurface.surface.get())
+ surfaceToStreamMap[deferredSurface] = stream.id
+ }
+ }
+
+ graph.start()
+ return UseCaseCamera(graph, useCases, surfaceToStreamMap, coroutineScope)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
deleted file mode 100644
index a0ddab2..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
+++ /dev/null
@@ -1,50 +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.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.info
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.UseCaseConfigFactory
-
-/**
- * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
- *
- * This includes things like default template and session parameters, as well as maximum resolution
- * and aspect ratios for the display.
- */
-class UseCaseConfigurationMap(context: Context) : UseCaseConfigFactory {
- init {
- if (context === context.applicationContext) {
- info {
- "The provided context ($context) is application scoped and will be used to infer " +
- "the default display for computing the default preview size, orientation, " +
- "and default aspect ratio for UseCase outputs."
- }
- }
- debug { "Created UseCaseConfigurationMap" }
- }
-
- /**
- * Returns the configuration for the given capture type, or `null` if the configuration
- * cannot be produced.
- */
- override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
- TODO("Not Implemented")
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
new file mode 100644
index 0000000..85f4155
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.camera.camera2.pipe.integration.impl
+
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
+import androidx.camera.core.UseCase
+import javax.inject.Inject
+
+/**
+ * This class keeps track of the currently attached and active [UseCase]'s for a specific camera.
+ */
+@CameraScope
+class UseCaseManager @Inject constructor(
+ private val cameraConfig: CameraConfig,
+ private val builder: UseCaseCameraComponent.Builder
+) {
+ private val attachedUseCases = mutableListOf<UseCase>()
+ private val enabledUseCases = mutableSetOf<UseCase>()
+
+ @Volatile
+ private var _activeComponent: UseCaseCameraComponent? = null
+ val camera: UseCaseCamera?
+ get() = _activeComponent?.getUseCaseCamera()
+
+ fun attach(useCases: List<UseCase>) {
+ if (useCases.isEmpty()) {
+ Log.warn { "Attach [] from $this (Ignored)" }
+ return
+ }
+ Log.debug { "Attaching $useCases from $this" }
+
+ var modified = false
+ for (useCase in useCases) {
+ if (!attachedUseCases.contains(useCase)) {
+ attachedUseCases.add(useCase)
+ modified = true
+ }
+ }
+
+ if (modified) {
+ start(attachedUseCases)
+ }
+ }
+
+ fun detach(useCases: List<UseCase>) {
+ if (useCases.isEmpty()) {
+ Log.warn { "Detaching [] from $this (Ignored)" }
+ return
+ }
+ Log.debug { "Detaching $useCases from $this" }
+
+ var modified = false
+ for (useCase in useCases) {
+ modified = attachedUseCases.remove(useCase) || modified
+ }
+
+ // TODO: We might only want to tear down when the number of attached use cases goes to
+ // zero. If a single UseCase is removed, we could deactivate it?
+ if (modified) {
+ start(attachedUseCases)
+ }
+ }
+
+ fun enable(useCase: UseCase) {
+ if (enabledUseCases.add(useCase)) {
+ invalidate()
+ }
+ }
+
+ fun disable(useCase: UseCase) {
+ if (enabledUseCases.remove(useCase)) {
+ invalidate()
+ }
+ }
+
+ fun update(useCase: UseCase) {
+ if (attachedUseCases.contains(useCase)) {
+ invalidate()
+ }
+ }
+
+ override fun toString(): String = "UseCaseManager<${cameraConfig.cameraId}>"
+
+ private fun invalidate() {
+ camera?.let {
+ it.activeUseCases = enabledUseCases.toSet()
+ }
+ }
+
+ private fun start(newUseCases: List<UseCase>) {
+ val useCases = newUseCases.toList()
+
+ // Close prior camera graph
+ camera.let {
+ _activeComponent = null
+ it?.close()
+ }
+
+ // Update list of active useCases
+ if (useCases.isEmpty()) {
+ return
+ }
+
+ // Create and configure the new camera component.
+ _activeComponent = builder.config(UseCaseCameraConfig(useCases)).build()
+ invalidate()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index c7bb3b0..d514661 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -22,6 +22,9 @@
import androidx.camera.camera2.pipe.impl.CameraPipeComponent
import androidx.camera.camera2.pipe.impl.CameraPipeConfigModule
import androidx.camera.camera2.pipe.impl.DaggerCameraPipeComponent
+import kotlinx.atomicfu.atomic
+
+internal val cameraPipeIds = atomic(0)
/**
* [CameraPipe] is the top level scope for all interactions with a Camera2 camera.
@@ -33,6 +36,7 @@
* the [CameraGraph] interface.
*/
class CameraPipe(config: Config) {
+ private val debugId = cameraPipeIds.incrementAndGet()
private val component: CameraPipeComponent = DaggerCameraPipeComponent.builder()
.cameraPipeConfigModule(CameraPipeConfigModule(config))
.build()
@@ -63,4 +67,6 @@
val appContext: Context,
val cameraThread: HandlerThread? = null
)
+
+ override fun toString(): String = "CameraPipe-$debugId"
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index 0c3cdda..f961304 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -47,7 +47,7 @@
* This will create a new Key instance, and will check to see that the key has not been
* previously created somewhere else.
*/
- internal fun <T> create(name: String): Key<T> {
+ fun <T> create(name: String): Key<T> {
synchronized(keys) {
check(keys.add(name)) { "$name is already defined!" }
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
index 5204cc8..3a87a61 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
@@ -277,7 +277,7 @@
val surface = surfaceMap[stream]
if (surface != null) {
- Log.debug { " Binding $surface to $stream" }
+ Log.debug { " Binding $stream to $surface" }
// TODO(codelogic) There should be a more efficient way to do these lookups than
// having two maps.
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 633e483..de75e09 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -46,14 +46,12 @@
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CameraStateRegistry;
-import androidx.camera.core.impl.CameraThreadConfig;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImmediateSurface;
@@ -112,7 +110,6 @@
CameraInternal.State.OPEN,
CameraInternal.State.RELEASED));
- private static CameraFactory sCameraFactory;
static ExecutorService sCameraExecutor;
@Rule
@@ -130,13 +127,11 @@
SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
@BeforeClass
- public static void classSetup() {
+ public static void classSetup() throws InitializationException {
sCameraHandlerThread = new HandlerThread("cameraThread");
sCameraHandlerThread.start();
sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
sCameraExecutor = CameraXExecutors.newHandlerExecutor(sCameraHandler);
- sCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
- CameraThreadConfig.create(sCameraExecutor, sCameraHandler));
}
@AfterClass
@@ -145,14 +140,18 @@
}
@Before
- public void setup() throws CameraUnavailableException {
+ public void setup() throws Exception {
mMockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
mSemaphore = new Semaphore(0);
mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
- mCamera2CameraImpl = new Camera2CameraImpl(
- CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+ CameraManagerCompat cameraManagerCompat =
+ CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+ Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+ mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+ mCamera2CameraImpl = new Camera2CameraImpl(cameraManagerCompat, mCameraId,
+ camera2CameraInfo,
mCameraStateRegistry, sCameraExecutor, sCameraHandler);
}
@@ -513,7 +512,7 @@
@Test
public void cameraTransitionsThroughPendingState_whenNoCamerasAvailable() {
@SuppressWarnings("unchecked") // Cannot mock generic type inline
- Observable.Observer<CameraInternal.State> mockObserver =
+ Observable.Observer<CameraInternal.State> mockObserver =
mock(Observable.Observer.class);
// Ensure real camera can't open due to max cameras being open
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 28660f6..a053541 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -35,6 +35,7 @@
import android.hardware.camera2.TotalCaptureResult;
import android.os.Handler;
import android.os.HandlerThread;
+import android.util.Log;
import android.util.Size;
import android.view.Surface;
@@ -44,7 +45,6 @@
import androidx.camera.camera2.internal.util.SemaphoreReleasingCamera2Callbacks;
import androidx.camera.camera2.interop.Camera2Interop;
import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.ExperimentalExposureCompensation;
import androidx.camera.core.ExposureState;
import androidx.camera.core.ImageCapture;
@@ -131,12 +131,14 @@
}
@Before
- public void setup() throws CameraUnavailableException {
+ public void setup() throws Exception {
// TODO(b/162296654): Workaround the google_3a specific behavior.
assumeFalse("Cuttlefish uses google_3a v1 or v2 it might fail to set EV before "
+ "first AE converge.", android.os.Build.MODEL.contains("Cuttlefish"));
assumeFalse("Pixel uses google_3a v1 or v2 it might fail to set EV before "
+ "first AE converge.", android.os.Build.MODEL.contains("Pixel"));
+ assumeFalse("Disable Nexus 5 in postsubmit for b/173743705",
+ android.os.Build.MODEL.contains("Nexus 5") && !Log.isLoggable("MH", Log.DEBUG));
assumeTrue(CameraUtil.deviceHasCamera());
assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING));
@@ -144,8 +146,13 @@
mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
mSemaphore = new Semaphore(0);
mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+ CameraManagerCompat cameraManagerCompat =
+ CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+ Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+ mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
mCamera2CameraImpl = new Camera2CameraImpl(
CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+ camera2CameraInfo,
mCameraStateRegistry, sCameraExecutor, sCameraHandler);
mCameraInfoInternal = mCamera2CameraImpl.getCameraInfoInternal();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
index 4b02c8f..0df28bd 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
@@ -47,9 +47,10 @@
// Create the DeviceSurfaceManager for Camera2
CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
- (context, cameraManager) -> {
+ (context, cameraManager, availableCameraIds) -> {
try {
- return new Camera2DeviceSurfaceManager(context, cameraManager);
+ return new Camera2DeviceSurfaceManager(context, cameraManager,
+ availableCameraIds);
} catch (CameraUnavailableException e) {
throw new InitializationException(e);
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
index 2236387..61c4069 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
@@ -19,17 +19,21 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CameraStateRegistry;
import androidx.camera.core.impl.CameraThreadConfig;
-import java.util.Arrays;
+import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -38,40 +42,57 @@
public final class Camera2CameraFactory implements CameraFactory {
private static final int DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS = 1;
private final CameraThreadConfig mThreadConfig;
-
private final CameraStateRegistry mCameraStateRegistry;
private final CameraManagerCompat mCameraManager;
+ private final List<String> mAvailableCameraIds;
+ private final Map<String, Camera2CameraInfoImpl> mCameraInfos = new HashMap<>();
/** Creates a Camera2 implementation of CameraFactory */
public Camera2CameraFactory(@NonNull Context context,
- @NonNull CameraThreadConfig threadConfig) {
+ @NonNull CameraThreadConfig threadConfig,
+ @Nullable CameraSelector availableCamerasSelector) throws InitializationException {
mThreadConfig = threadConfig;
mCameraStateRegistry = new CameraStateRegistry(DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS);
mCameraManager = CameraManagerCompat.from(context, mThreadConfig.getSchedulerHandler());
+
+ mAvailableCameraIds = CameraSelectionOptimizer
+ .getSelectedAvailableCameraIds(this, availableCamerasSelector);
}
@Override
@NonNull
public CameraInternal getCamera(@NonNull String cameraId) throws CameraUnavailableException {
- if (!getAvailableCameraIds().contains(cameraId)) {
+ if (!mAvailableCameraIds.contains(cameraId)) {
throw new IllegalArgumentException(
"The given camera id is not on the available camera id list.");
}
- return new Camera2CameraImpl(mCameraManager, cameraId, mCameraStateRegistry,
- mThreadConfig.getCameraExecutor(), mThreadConfig.getSchedulerHandler());
+ return new Camera2CameraImpl(mCameraManager,
+ cameraId,
+ getCameraInfo(cameraId),
+ mCameraStateRegistry,
+ mThreadConfig.getCameraExecutor(),
+ mThreadConfig.getSchedulerHandler());
}
- @Override
- @NonNull
- public Set<String> getAvailableCameraIds() throws CameraUnavailableException {
- List<String> camerasList;
+ Camera2CameraInfoImpl getCameraInfo(@NonNull String cameraId)
+ throws CameraUnavailableException {
try {
- camerasList = Arrays.asList(mCameraManager.getCameraIdList());
+ Camera2CameraInfoImpl camera2CameraInfoImpl = mCameraInfos.get(cameraId);
+ if (camera2CameraInfoImpl == null) {
+ camera2CameraInfoImpl = new Camera2CameraInfoImpl(
+ cameraId, mCameraManager.getCameraCharacteristicsCompat(cameraId));
+ mCameraInfos.put(cameraId, camera2CameraInfoImpl);
+ }
+ return camera2CameraInfoImpl;
} catch (CameraAccessExceptionCompat e) {
throw CameraUnavailableExceptionHelper.createFrom(e);
}
+ }
+ @Override
+ @NonNull
+ public Set<String> getAvailableCameraIds() {
// Use a LinkedHashSet to preserve order
- return new LinkedHashSet<>(camerasList);
+ return new LinkedHashSet<>(mAvailableCameraIds);
}
@NonNull
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 3cd5d0d..7adafc2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -185,6 +185,7 @@
*/
Camera2CameraImpl(@NonNull CameraManagerCompat cameraManager,
@NonNull String cameraId,
+ @NonNull Camera2CameraInfoImpl cameraInfoImpl,
@NonNull CameraStateRegistry cameraStateRegistry,
@NonNull Executor executor,
@NonNull Handler schedulerHandler) throws CameraUnavailableException {
@@ -206,10 +207,8 @@
mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
executorScheduler, mExecutor, new ControlUpdateListenerInternal(),
mCameraQuirks);
- mCameraInfoInternal = new Camera2CameraInfoImpl(
- cameraId,
- cameraCharacteristicsCompat,
- mCameraControlInternal);
+ mCameraInfoInternal = cameraInfoImpl;
+ mCameraInfoInternal.linkWithCameraControl(mCameraControlInternal);
} catch (CameraAccessExceptionCompat e) {
throw CameraUnavailableExceptionHelper.createFrom(e);
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index ccea681..43c735b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -18,8 +18,10 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraMetadata;
+import android.util.Pair;
import android.view.Surface;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.experimental.UseExperimental;
@@ -37,12 +39,24 @@
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
import java.util.concurrent.Executor;
/**
* Implementation of the {@link CameraInfoInternal} interface that exposes parameters through
* camera2.
+ *
+ * <p>Construction consists of two stages. The constructor creates a implementation without a
+ * {@link Camera2CameraControlImpl} and will return default values for camera control related
+ * states like zoom/exposure/torch. After {@link #linkWithCameraControl} is called,
+ * zoom/exposure/torch API will reflect the states in the {@link Camera2CameraControlImpl}. Any
+ * CameraCaptureCallbacks added before this link will also be added
+ * to the {@link Camera2CameraControlImpl}.
*/
@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
public final class Camera2CameraInfoImpl implements CameraInfoInternal {
@@ -50,22 +64,62 @@
private static final String TAG = "Camera2CameraInfo";
private final String mCameraId;
private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
- private final Camera2CameraControlImpl mCamera2CameraControlImpl;
- private final ZoomControl mZoomControl;
- private final TorchControl mTorchControl;
- private final ExposureControl mExposureControl;
private final Camera2CameraInfo mCamera2CameraInfo;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ @Nullable
+ private Camera2CameraControlImpl mCamera2CameraControlImpl;
+ @GuardedBy("mLock")
+ @Nullable
+ private RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
+ @GuardedBy("mLock")
+ @Nullable
+ private RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
+ @GuardedBy("mLock")
+ @Nullable
+ private List<Pair<CameraCaptureCallback, Executor>> mCameraCaptureCallbacks = null;
+
+ /**
+ * Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
+ * called, camera control related API (torch/exposure/zoom) will return default values.
+ */
Camera2CameraInfoImpl(@NonNull String cameraId,
- @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat,
- @NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+ @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
mCameraId = Preconditions.checkNotNull(cameraId);
mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
- mCamera2CameraControlImpl = camera2CameraControlImpl;
- mZoomControl = camera2CameraControlImpl.getZoomControl();
- mTorchControl = camera2CameraControlImpl.getTorchControl();
- mExposureControl = camera2CameraControlImpl.getExposureControl();
mCamera2CameraInfo = new Camera2CameraInfo(this);
+ }
+
+ /**
+ * Links with a {@link Camera2CameraControlImpl}. After the link, zoom/torch/exposure
+ * operations of CameraControl will modify the states in this Camera2CameraInfoImpl.
+ * Also, any CameraCaptureCallbacks added before this link will be added to the
+ * {@link Camera2CameraControlImpl}.
+ */
+ void linkWithCameraControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+ synchronized (mLock) {
+ mCamera2CameraControlImpl = camera2CameraControlImpl;
+
+ if (mRedirectZoomStateLiveData != null) {
+ mRedirectZoomStateLiveData.redirectTo(
+ mCamera2CameraControlImpl.getZoomControl().getZoomState());
+ }
+
+ if (mRedirectTorchStateLiveData != null) {
+ mRedirectTorchStateLiveData.redirectTo(
+ mCamera2CameraControlImpl.getTorchControl().getTorchState());
+ }
+
+ if (mCameraCaptureCallbacks != null) {
+ for (Pair<CameraCaptureCallback, Executor> pair :
+ mCameraCaptureCallbacks) {
+ mCamera2CameraControlImpl.addSessionCameraCaptureCallback(pair.second,
+ pair.first);
+ }
+ mCameraCaptureCallbacks = null;
+ }
+ }
logDeviceInfo();
}
@@ -175,20 +229,55 @@
@NonNull
@Override
public LiveData<Integer> getTorchState() {
- return mTorchControl.getTorchState();
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ if (mRedirectTorchStateLiveData == null) {
+ mRedirectTorchStateLiveData =
+ new RedirectableLiveData<>(TorchControl.DEFAULT_TORCH_STATE);
+ }
+ return mRedirectTorchStateLiveData;
+ }
+
+ // if RedirectableLiveData exists, use it directly.
+ if (mRedirectTorchStateLiveData != null) {
+ return mRedirectTorchStateLiveData;
+ }
+
+ return mCamera2CameraControlImpl.getTorchControl().getTorchState();
+ }
}
@NonNull
@Override
public LiveData<ZoomState> getZoomState() {
- return mZoomControl.getZoomState();
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ if (mRedirectZoomStateLiveData == null) {
+ mRedirectZoomStateLiveData = new RedirectableLiveData<>(
+ ZoomControl.getDefaultZoomState(mCameraCharacteristicsCompat));
+ }
+ return mRedirectZoomStateLiveData;
+ }
+
+ // if RedirectableLiveData exists, use it directly.
+ if (mRedirectZoomStateLiveData != null) {
+ return mRedirectZoomStateLiveData;
+ }
+
+ return mCamera2CameraControlImpl.getZoomControl().getZoomState();
+ }
}
@NonNull
@Override
@ExperimentalExposureCompensation
public ExposureState getExposureState() {
- return mExposureControl.getExposureState();
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ return ExposureControl.getDefaultExposureState(mCameraCharacteristicsCompat);
+ }
+ return mCamera2CameraControlImpl.getExposureControl().getExposureState();
+ }
}
/**
@@ -213,12 +302,38 @@
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
- mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ if (mCameraCaptureCallbacks == null) {
+ mCameraCaptureCallbacks = new ArrayList<>();
+ }
+ mCameraCaptureCallbacks.add(new Pair<>(callback, executor));
+ return;
+ }
+
+ mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+ }
}
@Override
public void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback) {
- mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+ synchronized (mLock) {
+ if (mCamera2CameraControlImpl == null) {
+ if (mCameraCaptureCallbacks == null) {
+ return;
+ }
+ Iterator<Pair<CameraCaptureCallback, Executor>> it =
+ mCameraCaptureCallbacks.iterator();
+ while (it.hasNext()) {
+ Pair<CameraCaptureCallback, Executor> pair = it.next();
+ if (pair.first == callback) {
+ it.remove();
+ }
+ }
+ return;
+ }
+ mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+ }
}
/**
@@ -228,4 +343,40 @@
public Camera2CameraInfo getCamera2CameraInfo() {
return mCamera2CameraInfo;
}
+
+ /**
+ * A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
+ * is set, initial value will be used.
+ */
+ static class RedirectableLiveData<T> extends MediatorLiveData<T> {
+ private LiveData<T> mLiveDataSource;
+ private T mInitialValue;
+
+ RedirectableLiveData(T initialValue) {
+ mInitialValue = initialValue;
+ }
+
+ void redirectTo(@NonNull LiveData<T> liveDataSource) {
+ if (mLiveDataSource != null) {
+ super.removeSource(mLiveDataSource);
+ }
+ mLiveDataSource = liveDataSource;
+ super.addSource(liveDataSource, this::setValue);
+ }
+
+ @Override
+ public <S> void addSource(@NonNull LiveData<S> source,
+ @NonNull Observer<? super S> onChanged) {
+ throw new UnsupportedOperationException();
+ }
+
+ // Overrides getValue() to reflect the correct value from source. This is required to ensure
+ // getValue() is correct when observe() or observeForever() is not called.
+ @Override
+ public T getValue() {
+ // Returns initial value if source is not set.
+ return mLiveDataSource == null ? mInitialValue : mLiveDataSource.getValue();
+ }
+ }
+
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index e5eb1ff..9479bf5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -24,7 +24,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
@@ -36,6 +35,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -60,13 +60,15 @@
*/
@RestrictTo(Scope.LIBRARY)
public Camera2DeviceSurfaceManager(@NonNull Context context,
- @Nullable Object cameraManager) throws CameraUnavailableException {
- this(context, CamcorderProfile::hasProfile, cameraManager);
+ @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
+ throws CameraUnavailableException {
+ this(context, CamcorderProfile::hasProfile, cameraManager, availableCameraIds);
}
Camera2DeviceSurfaceManager(@NonNull Context context,
@NonNull CamcorderProfileHelper camcorderProfileHelper,
- @Nullable Object cameraManager)
+ @Nullable Object cameraManager,
+ @NonNull Set<String> availableCameraIds)
throws CameraUnavailableException {
Preconditions.checkNotNull(camcorderProfileHelper);
mCamcorderProfileHelper = camcorderProfileHelper;
@@ -77,25 +79,22 @@
} else {
cameraManagerCompat = CameraManagerCompat.from(context);
}
- init(context, cameraManagerCompat);
+ init(context, cameraManagerCompat, availableCameraIds);
}
/**
* Prepare necessary resources for the surface manager.
*/
- private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager)
+ private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager,
+ @NonNull Set<String> availableCameraIds)
throws CameraUnavailableException {
Preconditions.checkNotNull(context);
- try {
- for (String cameraId : cameraManager.getCameraIdList()) {
- mCameraSupportedSurfaceCombinationMap.put(
- cameraId,
- new SupportedSurfaceCombination(
- context, cameraId, cameraManager, mCamcorderProfileHelper));
- }
- } catch (CameraAccessExceptionCompat e) {
- throw CameraUnavailableExceptionHelper.createFrom(e);
+ for (String cameraId : availableCameraIds) {
+ mCameraSupportedSurfaceCombinationMap.put(
+ cameraId,
+ new SupportedSurfaceCombination(
+ context, cameraId, cameraManager, mCamcorderProfileHelper));
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
index 2d6d755..703e6a1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
@@ -42,6 +42,7 @@
class CameraBurstCaptureCallback extends CameraCaptureSession.CaptureCallback {
final Map<CaptureRequest, List<CameraCaptureSession.CaptureCallback>> mCallbackMap;
+ CaptureSequenceCallback mCaptureSequenceCallback = null;
CameraBurstCaptureCallback() {
mCallbackMap = new HashMap<>();
@@ -98,13 +99,18 @@
@Override
public void onCaptureSequenceAborted(
@NonNull CameraCaptureSession session, int sequenceId) {
- // No-op.
+ if (mCaptureSequenceCallback != null) {
+ mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId, true);
+ }
}
@Override
public void onCaptureSequenceCompleted(
@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
- // No-op.
+ if (mCaptureSequenceCallback != null) {
+ mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId,
+ false);
+ }
}
private List<CameraCaptureSession.CaptureCallback> getCallbacks(CaptureRequest request) {
@@ -131,4 +137,20 @@
}
}
+ /**
+ * Sets the callback to receive the notification when the capture sequence is completed or
+ * aborted.
+ */
+ public void setCaptureSequenceCallback(@NonNull CaptureSequenceCallback callback) {
+ mCaptureSequenceCallback = callback;
+ }
+
+ /**
+ * A interface to receive the notification of onCaptureSequenceCompleted or
+ * onCaptureSequenceAborted.
+ */
+ interface CaptureSequenceCallback {
+ void onCaptureSequenceCompletedOrAborted(
+ @NonNull CameraCaptureSession session, int sequenceId, boolean isAborted);
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
new file mode 100644
index 0000000..d0a146f
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
@@ -0,0 +1,108 @@
+/*
+ * 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.camera.camera2.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraMetadata;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class CameraSelectionOptimizer {
+ private CameraSelectionOptimizer() {
+ }
+
+ static List<String> getSelectedAvailableCameraIds(
+ @NonNull Camera2CameraFactory cameraFactory,
+ @Nullable CameraSelector availableCamerasSelector)
+ throws InitializationException {
+ try {
+ List<String> availableCameraIds = new ArrayList<>();
+ String[] cameraIdList = cameraFactory.getCameraManager().getCameraIdList();
+ if (availableCamerasSelector == null) {
+ for (String id : cameraIdList) {
+ availableCameraIds.add(id);
+ }
+ return availableCameraIds;
+ }
+
+ // Skip camera ID by heuristic: 0 is back lens facing, 1 is front lens facing.
+ Integer lensFacingInteger = availableCamerasSelector.getLensFacing();
+ String skippedCameraId = decideSkippedCameraIdByHeuristic(
+ cameraFactory.getCameraManager(), lensFacingInteger);
+ List<CameraInfo> cameraInfos = new ArrayList<>();
+
+ for (String id : cameraIdList) {
+ if (id.equals(skippedCameraId)) {
+ continue;
+ }
+ Camera2CameraInfoImpl cameraInfo = cameraFactory.getCameraInfo(id);
+ cameraInfos.add(cameraInfo);
+ }
+
+ List<CameraInfo> filteredCameraInfos =
+ availableCamerasSelector.filter(cameraInfos);
+
+ for (CameraInfo cameraInfo : filteredCameraInfos) {
+ String cameraId = ((CameraInfoInternal) cameraInfo).getCameraId();
+ availableCameraIds.add(cameraId);
+ }
+
+ return availableCameraIds;
+ } catch (CameraAccessExceptionCompat e) {
+ throw new InitializationException(CameraUnavailableExceptionHelper.createFrom(e));
+ } catch (CameraUnavailableException e) {
+ throw new InitializationException(e);
+ }
+ }
+
+ // Returns the camera id that can be safely skipped.
+ // Returns null if no camera ids can be skipped.
+ private static String decideSkippedCameraIdByHeuristic(CameraManagerCompat cameraManager,
+ Integer lensFacingInteger) throws CameraAccessExceptionCompat {
+ String skippedCameraId = null;
+ if (lensFacingInteger == null) { // Not specifying lens facing, cannot skip any camera id.
+ return null;
+ } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_BACK) {
+ if (cameraManager.getCameraCharacteristicsCompat("0").get(
+ CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
+ // If apps requires back lens facing, and "0" is confirmed to be back
+ // We can safely ignore "1" as a optimization for initialization latency
+ skippedCameraId = "1";
+ }
+ } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_FRONT) {
+ if (cameraManager.getCameraCharacteristicsCompat("1").get(
+ CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
+ // If apps requires front lens facing, and "1" is confirmed to be back
+ // We can safely ignore "0" as a optimization for initialization latency
+ skippedCameraId = "0";
+ }
+ }
+
+ return skippedCameraId;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 7418b99..f61d60e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -32,6 +32,7 @@
import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
+import androidx.camera.camera2.internal.compat.workaround.StillCaptureFlow;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureCallback;
@@ -120,6 +121,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mStateLock")
CallbackToFutureAdapter.Completer<Void> mReleaseCompleter;
+ final StillCaptureFlow mStillCaptureFlow = new StillCaptureFlow();
/**
* Constructor for CaptureSession.
@@ -633,6 +635,7 @@
try {
CameraBurstCaptureCallback callbackAggregator = new CameraBurstCaptureCallback();
List<CaptureRequest> captureRequests = new ArrayList<>();
+ boolean isStillCapture = false;
Logger.d(TAG, "Issuing capture request.");
for (CaptureConfig captureConfig : captureConfigs) {
if (captureConfig.getSurfaces().isEmpty()) {
@@ -657,6 +660,9 @@
continue;
}
+ if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+ isStillCapture = true;
+ }
CaptureConfig.Builder captureConfigBuilder = CaptureConfig.Builder.from(
captureConfig);
@@ -692,6 +698,18 @@
}
if (!captureRequests.isEmpty()) {
+ if (mStillCaptureFlow
+ .shouldStopRepeatingBeforeCapture(captureRequests, isStillCapture)) {
+ mSynchronizedCaptureSession.stopRepeating();
+ callbackAggregator.setCaptureSequenceCallback(
+ (session, sequenceId, isAborted) -> {
+ synchronized (mStateLock) {
+ if (mState == State.OPENED) {
+ issueRepeatingCaptureRequests();
+ }
+ }
+ });
+ }
mSynchronizedCaptureSession.captureBurstRequests(captureRequests,
callbackAggregator);
} else {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
index 9b8e776..12e6f7f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -93,6 +93,11 @@
mExecutor = executor;
}
+ static ExposureState getDefaultExposureState(
+ CameraCharacteristicsCompat cameraCharacteristics) {
+ return new ExposureStateImpl(cameraCharacteristics, DEFAULT_EXPOSURE_COMPENSATION);
+ }
+
/**
* Set current active state. Set active if it is ready to accept operations.
*
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
index 13fdc47..bf44fdf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
@@ -75,6 +75,8 @@
public boolean isExposureCompensationSupported() {
Range<Integer> compensationRange =
mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
- return compensationRange.getLower() != 0 && compensationRange.getUpper() != 0;
+ return compensationRange != null
+ && compensationRange.getLower() != 0
+ && compensationRange.getUpper() != 0;
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7c86acb..695b9bc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -48,6 +48,7 @@
*/
final class TorchControl {
private static final String TAG = "TorchControl";
+ static final int DEFAULT_TORCH_STATE = TorchState.OFF;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
private final Camera2CameraControlImpl mCamera2CameraControlImpl;
@@ -77,7 +78,7 @@
Boolean hasFlashUnit =
cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
- mTorchState = new MutableLiveData<>(TorchState.OFF);
+ mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
mCamera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
index 7d79583..888e0b2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
@@ -98,7 +98,15 @@
camera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
}
- private ZoomImpl createZoomImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
+ static ZoomState getDefaultZoomState(CameraCharacteristicsCompat cameraCharacteristics) {
+ ZoomImpl zoomImpl = createZoomImpl(cameraCharacteristics);
+ ZoomStateImpl zoomState = new ZoomStateImpl(zoomImpl.getMaxZoom(), zoomImpl.getMinZoom());
+ zoomState.setZoomRatio(DEFAULT_ZOOM_RATIO);
+ return ImmutableZoomState.create(zoomState);
+ }
+
+ private static ZoomImpl createZoomImpl(
+ @NonNull CameraCharacteristicsCompat cameraCharacteristics) {
if (isAndroidRZoomSupported(cameraCharacteristics)) {
return new AndroidRZoomImpl(cameraCharacteristics);
} else {
@@ -106,7 +114,8 @@
}
}
- private boolean isAndroidRZoomSupported(CameraCharacteristicsCompat cameraCharacteristics) {
+ private static boolean isAndroidRZoomSupported(
+ CameraCharacteristicsCompat cameraCharacteristics) {
return Build.VERSION.SDK_INT >= 30 && cameraCharacteristics.get(
CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) != null;
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index ba59291..374c616 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -57,6 +57,9 @@
if (PreviewPixelHDRnetQuirk.load()) {
quirks.add(new PreviewPixelHDRnetQuirk());
}
+ if (StillCaptureFlashStopRepeatingQuirk.load()) {
+ quirks.add(new StillCaptureFlashStopRepeatingQuirk());
+ }
return quirks;
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
new file mode 100644
index 0000000..bc50f27
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * 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.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Locale;
+
+/**
+ * Quirk that still capture with flash on/auto requires stopRepeating() being called ahead of
+ * capture.
+ *
+ * <p>On some devices like Samsung SM-A716B, it could lead to CaptureRequest not being completed
+ * when taking photos in dark environment with flash on/auto. Calling stopRepeating ahead of
+ * still capture and setRepeating again after capture is done can fix the issue. See b/172036589.
+ */
+public class StillCaptureFlashStopRepeatingQuirk implements Quirk {
+ static boolean load() {
+ return "SAMSUNG".equals(Build.MANUFACTURER.toUpperCase(Locale.US))
+ // Enables it on all A716 models.
+ && android.os.Build.MODEL.toUpperCase(Locale.US).startsWith("SM-A716");
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
new file mode 100644
index 0000000..7a22956
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
@@ -0,0 +1,63 @@
+/*
+ * 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.camera.camera2.internal.compat.workaround;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.StillCaptureFlashStopRepeatingQuirk;
+
+import java.util.List;
+
+/**
+ * Workaround to fix device issues such as calling stopRepeating ahead of still
+ * capture on some devices when flash is on or auto. See b/172036589.
+ */
+public class StillCaptureFlow {
+ private final boolean mShouldStopRepeatingBeforeStillCapture;
+ public StillCaptureFlow() {
+ final StillCaptureFlashStopRepeatingQuirk quirk = DeviceQuirks.get(
+ StillCaptureFlashStopRepeatingQuirk.class);
+
+ mShouldStopRepeatingBeforeStillCapture = (quirk != null);
+ }
+
+ /**
+ * Returns whether or not it should call stopRepeating ahead of capture request.
+ *
+ * @param captureRequests captureRequests to be executed
+ * @param isStillCapture true if captureRequests contain a still capture request.
+ * @return
+ */
+ public boolean shouldStopRepeatingBeforeCapture(
+ @NonNull List<CaptureRequest> captureRequests, boolean isStillCapture) {
+ if (!mShouldStopRepeatingBeforeStillCapture || !isStillCapture) {
+ return false;
+ }
+
+ for (CaptureRequest request : captureRequests) {
+ int aeMode = request.get(CaptureRequest.CONTROL_AE_MODE);
+ if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+ || aeMode == CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index ffa744e..f528b07c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,18 +33,19 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExposureState;
import androidx.camera.core.TorchState;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.internal.ImmutableZoomState;
+import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
@@ -78,6 +80,7 @@
private CameraCharacteristicsCompat mCameraCharacteristics1;
private ZoomControl mMockZoomControl;
private TorchControl mMockTorchControl;
+ private ExposureControl mExposureControl;
private Camera2CameraControlImpl mMockCameraControl;
@Before
@@ -97,23 +100,26 @@
mMockZoomControl = mock(ZoomControl.class);
mMockTorchControl = mock(TorchControl.class);
+ mExposureControl = mock(ExposureControl.class);
mMockCameraControl = mock(Camera2CameraControlImpl.class);
when(mMockCameraControl.getZoomControl()).thenReturn(mMockZoomControl);
when(mMockCameraControl.getTorchControl()).thenReturn(mMockTorchControl);
+ when(mMockCameraControl.getExposureControl()).thenReturn(mExposureControl);
}
@Test
public void canCreateCameraInfo() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
assertThat(cameraInfoInternal).isNotNull();
}
@Test
public void cameraInfo_canReturnSensorOrientation() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
assertThat(cameraInfoInternal.getSensorRotationDegrees()).isEqualTo(
CAMERA0_SENSOR_ORIENTATION);
}
@@ -121,7 +127,7 @@
@Test
public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
// Note: these numbers depend on the camera being a back-facing camera.
assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -137,7 +143,7 @@
@Test
public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
// Note: these numbers depend on the camera being a front-facing camera.
assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -153,47 +159,130 @@
@Test
public void cameraInfo_canReturnLensFacing() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
assertThat(cameraInfoInternal.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
}
@Test
public void cameraInfo_canReturnHasFlashUnit_forBackCamera() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA0_FLASH_INFO_BOOLEAN);
}
@Test
public void cameraInfo_canReturnHasFlashUnit_forFrontCamera() {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+ new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA1_FLASH_INFO_BOOLEAN);
}
@Test
- public void cameraInfo_canReturnTorchState() {
- CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
- when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.OFF));
- assertThat(cameraInfoInternal.getTorchState().getValue()).isEqualTo(TorchState.OFF);
+ public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState() {
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ assertThat(camera2CameraInfoImpl.getTorchState().getValue())
+ .isEqualTo(TorchControl.DEFAULT_TORCH_STATE);
+ }
+
+ @Test
+ public void cameraInfoWithCameraControl_canReturnTorchState() {
+ when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+ assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
+ }
+
+ @Test
+ public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+ // Calls getTorchState() to trigger RedirectableLiveData
+ LiveData<Integer> torchStateLiveData = camera2CameraInfoImpl.getTorchState();
+
+ when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+ camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+ // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+ assertThat(camera2CameraInfoImpl.getTorchState()).isSameInstanceAs(torchStateLiveData);
+ assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
}
// zoom related tests just ensure it uses ZoomControl to get the value
// Full tests are performed at ZoomControlDeviceTest / ZoomControlTest.
@Test
- public void cameraInfo_getZoom_valueIsCorrect() {
- CameraInfoInternal cameraInfo =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+ public void cameraInfoWithCameraControl_getZoom_valueIsCorrect() {
ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
- assertThat(mMockZoomControl.getZoomState().getValue()).isEqualTo(zoomState);
+
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+ assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+ }
+
+ @Test
+ public void cameraInfoWithoutCameraControl_getDetaultZoomState() {
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ assertThat(camera2CameraInfoImpl.getZoomState().getValue())
+ .isEqualTo(ZoomControl.getDefaultZoomState(mCameraCharacteristics0));
+ }
+
+ @Test
+ public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+ // Calls getZoomState() to trigger RedirectableLiveData
+ LiveData<ZoomState> zoomStateLiveData = camera2CameraInfoImpl.getZoomState();
+
+ ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
+ when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
+ camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+ // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+ assertThat(camera2CameraInfoImpl.getZoomState()).isSameInstanceAs(zoomStateLiveData);
+ assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+ }
+
+ @Test
+ public void cameraInfoWithCameraControl_canReturnExposureState() {
+ ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
+ when(mExposureControl.getExposureState()).thenReturn(exposureState);
+
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+ assertThat(camera2CameraInfoImpl.getExposureState()).isEqualTo(exposureState);
+ }
+
+ @Test
+ public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState() {
+ Camera2CameraInfoImpl camera2CameraInfoImpl =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+ ExposureState defaultState =
+ ExposureControl.getDefaultExposureState(mCameraCharacteristics0);
+
+ assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationIndex())
+ .isEqualTo(defaultState.getExposureCompensationIndex());
+ assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationRange())
+ .isEqualTo(defaultState.getExposureCompensationRange());
+ assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationStep())
+ .isEqualTo(defaultState.getExposureCompensationStep());
+ assertThat(camera2CameraInfoImpl.getExposureState().isExposureCompensationSupported())
+ .isEqualTo(defaultState.isExposureCompensationSupported());
}
@Test
public void cameraInfo_getImplementationType_legacy() {
final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
- mCameraCharacteristics0, mMockCameraControl);
+ mCameraCharacteristics0);
assertThat(cameraInfo.getImplementationType()).isEqualTo(
CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
}
@@ -201,43 +290,68 @@
@Test
public void cameraInfo_getImplementationType_noneLegacy() {
final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1, mMockCameraControl);
+ mCameraCharacteristics1);
assertThat(cameraInfo.getImplementationType()).isEqualTo(
CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
}
@Test
public void addSessionCameraCaptureCallback_isCalledToCameraControl() {
- final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1, mMockCameraControl);
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+ mCameraCharacteristics1);
+ cameraInfo.linkWithCameraControl(mMockCameraControl);
Executor executor = mock(Executor.class);
CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
cameraInfo.addSessionCaptureCallback(executor, callback);
- ArgumentCaptor<Executor> executorCaptor = ArgumentCaptor.forClass(Executor.class);
- ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
- ArgumentCaptor.forClass(CameraCaptureCallback.class);
-
- verify(mMockCameraControl).addSessionCameraCaptureCallback(executorCaptor.capture(),
- callbackCaptor.capture());
- assertThat(executorCaptor.getValue()).isSameInstanceAs(executor);
- assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+ verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
}
@Test
public void removeSessionCameraCaptureCallback_isCalledToCameraControl() {
- final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1, mMockCameraControl);
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+ mCameraCharacteristics1);
+ cameraInfo.linkWithCameraControl(mMockCameraControl);
CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
cameraInfo.removeSessionCaptureCallback(callback);
- ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
- ArgumentCaptor.forClass(CameraCaptureCallback.class);
+ verify(mMockCameraControl).removeSessionCameraCaptureCallback(callback);
+ }
- verify(mMockCameraControl).removeSessionCameraCaptureCallback(callbackCaptor.capture());
- assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+ @Test
+ public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater() {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+ mCameraCharacteristics1);
+ Executor executor = mock(Executor.class);
+ CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
+ cameraInfo.addSessionCaptureCallback(executor, callback);
+
+ cameraInfo.linkWithCameraControl(mMockCameraControl);
+
+ verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
+ }
+
+ @Test
+ public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved() {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+ mCameraCharacteristics1);
+ // Add two callbacks
+ Executor executor1 = mock(Executor.class);
+ CameraCaptureCallback callback1 = mock(CameraCaptureCallback.class);
+ Executor executor2 = mock(Executor.class);
+ CameraCaptureCallback callback2 = mock(CameraCaptureCallback.class);
+ cameraInfo.addSessionCaptureCallback(executor1, callback1);
+ cameraInfo.addSessionCaptureCallback(executor2, callback2);
+
+ // Remove first callback.
+ cameraInfo.removeSessionCaptureCallback(callback1);
+
+ // Only second callback will be added to camera control.
+ cameraInfo.linkWithCameraControl(mMockCameraControl);
+ verify(mMockCameraControl, never()).addSessionCameraCaptureCallback(executor1, callback1);
+ verify(mMockCameraControl).addSessionCameraCaptureCallback(executor2, callback2);
}
private void initCameras() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index b248170..0ec8db2 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -570,9 +570,7 @@
@CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
lensFacing);
mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
- new Camera2CameraInfoImpl(cameraId,
- getCameraCharacteristicsCompat(cameraId),
- mock(Camera2CameraControlImpl.class))));
+ new Camera2CameraInfoImpl(cameraId, getCameraCharacteristicsCompat(cameraId))));
}
private void initCameraX() {
@@ -592,11 +590,11 @@
// Create the DeviceSurfaceManager for Camera2
CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
- (context, cameraManager) -> {
+ (context, cameraManager, availableCameraIds) -> {
try {
return new Camera2DeviceSurfaceManager(mContext,
mMockCamcorderProfileHelper,
- (CameraManagerCompat) cameraManager);
+ (CameraManagerCompat) cameraManager, availableCameraIds);
} catch (CameraUnavailableException e) {
throw new InitializationException(e);
}
@@ -608,7 +606,7 @@
CameraXConfig.Builder appConfigBuilder =
new CameraXConfig.Builder()
- .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+ .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
.setUseCaseConfigFactoryProvider(factoryProvider);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
index 1d588c5..a8ddb0e 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
@@ -117,4 +117,28 @@
burstCaptureCallback.onCaptureStarted(mSession, mRequest0, 0, 0);
// No listener called.
}
+
+ @Test
+ public void captureSequenceCallback_calledWhenSequenceCompleted() {
+ CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+ CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+ CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+ burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+ burstCaptureCallback.onCaptureSequenceCompleted(mSession, 0, 0);
+
+ verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, false);
+ }
+
+ @Test
+ public void captureSequenceCallback_calledWhenSequenceAborted() {
+ CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+ CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+ CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+ burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+ burstCaptureCallback.onCaptureSequenceAborted(mSession, 0);
+
+ verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, true);
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
new file mode 100644
index 0000000..de4b238
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.camera.camera2.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.camera.camera2.interop.Camera2CameraFilter;
+import androidx.camera.camera2.interop.Camera2CameraInfo;
+import androidx.camera.core.CameraFilter;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.impl.CameraThreadConfig;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CameraSelectionOptimizerTest {
+ private Camera2CameraFactory mCamera2CameraFactory;
+
+ @Before
+ public void setUp() throws Exception {
+ mCamera2CameraFactory =
+ spy(new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
+ CameraThreadConfig.create(CameraXExecutors.mainThreadExecutor(),
+ new Handler()),
+ null));
+ }
+
+ void setupNormalCameras() throws Exception {
+ initCharacterisic("0", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+ initCharacterisic("1", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+ initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+ initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+ }
+
+ void setupAbnormalCameras() throws Exception {
+ // "0" is front
+ initCharacterisic("0", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+ // "1" is back
+ initCharacterisic("1", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+ initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+ initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+ }
+
+ @Test
+ public void availableCamerasSelectorNull_returnAllCameras() throws Exception {
+ setupNormalCameras();
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ null);
+
+ assertThat(cameraIds).containsExactly("0", "1", "2", "3");
+ }
+
+ @Test
+ public void requireLensFacingBack() throws Exception {
+ setupNormalCameras();
+
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build();
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ cameraSelector);
+
+ assertThat(cameraIds).containsExactly("0", "2", "3");
+ verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+ }
+
+ @Test
+ public void requireLensFacingFront() throws Exception {
+ setupNormalCameras();
+
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build();
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ cameraSelector);
+
+ assertThat(cameraIds).containsExactly("1");
+ // only camera "0" 's getCameraCharacteristics can be avoided.
+ verify(mCamera2CameraFactory, never()).getCameraInfo("0");
+ }
+
+ @Test
+ public void requireLensFacingBack_andSelectWidestAngle() throws Exception {
+ setupNormalCameras();
+
+ CameraFilter widestAngleFilter = Camera2CameraFilter.createCameraFilter(
+ cameraInfoList -> {
+ float minFocalLength = 10000;
+ Camera2CameraInfo minFocalCameraInfo = null;
+ for (Camera2CameraInfo camera2CameraInfo : cameraInfoList) {
+ float focalLength = camera2CameraInfo.getCameraCharacteristic(
+ CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)[0];
+ if (focalLength < minFocalLength) {
+ minFocalLength = focalLength;
+ minFocalCameraInfo = camera2CameraInfo;
+ }
+ }
+ return Arrays.asList(minFocalCameraInfo);
+ });
+
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .addCameraFilter(widestAngleFilter)
+ .build();
+
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ cameraSelector);
+
+ assertThat(cameraIds).containsExactly("2");
+ // only camera "1" 's getCameraCharacteristics can be avoided.
+ verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+
+ }
+
+ @Test
+ public void abnormalCameraSetup_requireLensFacingBack() throws Exception {
+ setupAbnormalCameras();
+
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build();
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ cameraSelector);
+
+ // even though heuristic failed, it still works as expected.
+ assertThat(cameraIds).containsExactly("1", "2", "3");
+ }
+
+ @Test
+ public void abnormalCameraSetup_requireLensFacingFront() throws Exception {
+ setupAbnormalCameras();
+
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build();
+ List<String> cameraIds =
+ CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+ cameraSelector);
+
+ // even though heuristic failed, it still works as expected.
+ assertThat(cameraIds).containsExactly("0");
+ }
+
+ private void initCharacterisic(String cameraId, int lensFacing, float focalLength) {
+ CameraCharacteristics characteristics =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+
+ ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+
+ shadowCharacteristics.set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL);
+
+ // Add a lens facing to the camera
+ shadowCharacteristics.set(CameraCharacteristics.LENS_FACING, lensFacing);
+
+ shadowCharacteristics.set(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS,
+ new float[]{focalLength});
+
+ // Add the camera to the camera service
+ ((ShadowCameraManager)
+ Shadow.extract(
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(Context.CAMERA_SERVICE)))
+ .addCamera(cameraId, characteristics);
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index 129f334..e530f60 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -195,8 +195,7 @@
cameraFactory.insertCamera(lensFacingEnum, BACK_CAMERA_ID,
() -> new FakeCamera(BACK_CAMERA_ID, null,
new Camera2CameraInfoImpl(BACK_CAMERA_ID,
- getCameraCharacteristicsCompat(BACK_CAMERA_ID),
- mock(Camera2CameraControlImpl.class))));
+ getCameraCharacteristicsCompat(BACK_CAMERA_ID))));
initCameraX(cameraFactory);
}
@@ -204,7 +203,7 @@
private void initCameraX(final FakeCameraFactory cameraFactory) {
CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
Camera2Config.defaultConfig())
- .setCameraFactoryProvider((ignored0, ignored1) -> cameraFactory)
+ .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> cameraFactory)
.build();
CameraX.initialize(mContext, cameraXConfig);
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index c4bf842..368e5fe 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -2172,8 +2172,7 @@
mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
new Camera2CameraInfoImpl(cameraId,
- mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId),
- mock(Camera2CameraControlImpl.class))));
+ mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId))));
initCameraX();
}
@@ -2181,7 +2180,7 @@
private void initCameraX() {
CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
Camera2Config.defaultConfig())
- .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+ .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
.build();
CameraX.initialize(mContext, cameraXConfig);
CameraX cameraX;
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
new file mode 100644
index 0000000..6f52773
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.camera.camera2.internal.compat.workaround;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(ParameterizedRobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class StillCaptureFlowTest {
+ @ParameterizedRobolectricTestRunner.Parameters
+ public static Collection<Object[]> data() {
+ final List<Object[]> data = new ArrayList<>();
+ data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON, true, false});
+ data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+ data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+ data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+ data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON, true, false});
+ data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+ data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+ data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+ data.add(new Object[]{"Google", "Pixel 2", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+ data.add(new Object[]{"Moto", "G3", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+ data.add(new Object[]{"Samsung", "SM-A722", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+
+ return data;
+ }
+
+ private final String mBrand;
+ private final String mModel;
+ private final int mAeMode;
+ private final boolean mIsStillCapture;
+ private final boolean mExpectedShouldStopRepeating;
+ public StillCaptureFlowTest(
+ String brand,
+ String model,
+ int aeMode,
+ boolean isStillCapture,
+ boolean expectedShouldStopRepeating) {
+ mBrand = brand;
+ mModel = model;
+ mAeMode = aeMode;
+ mIsStillCapture = isStillCapture;
+ mExpectedShouldStopRepeating = expectedShouldStopRepeating;
+ }
+
+ @Test
+ public void shouldStopRepeating() {
+ ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", mBrand);
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", mModel);
+
+ StillCaptureFlow stillCaptureFlow = new StillCaptureFlow();
+ CaptureRequest captureRequest = mock(CaptureRequest.class);
+ when(captureRequest.get(CaptureRequest.CONTROL_AE_MODE)).thenReturn(mAeMode);
+
+ assertThat(stillCaptureFlow.shouldStopRepeatingBeforeCapture(
+ Arrays.asList(captureRequest), mIsStillCapture))
+ .isEqualTo(mExpectedShouldStopRepeating);
+ }
+}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
index 779d45f..8157db1 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
@@ -165,9 +165,11 @@
@Test
public void init_withDifferentCameraXConfig() throws ExecutionException, InterruptedException {
CameraFactory cameraFactory0 = new FakeCameraFactory();
- CameraFactory.Provider cameraFactoryProvider0 = (ignored0, ignored1) -> cameraFactory0;
+ CameraFactory.Provider cameraFactoryProvider0 =
+ (ignored0, ignored1, ignored2) -> cameraFactory0;
CameraFactory cameraFactory1 = new FakeCameraFactory();
- CameraFactory.Provider cameraFactoryProvider1 = (ignored0, ignored1) -> cameraFactory1;
+ CameraFactory.Provider cameraFactoryProvider1 =
+ (ignored0, ignored1, ignored2) -> cameraFactory1;
mConfigBuilder.setCameraFactoryProvider(cameraFactoryProvider0);
CameraX.initialize(mContext, mConfigBuilder.build());
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
index 74dc16b..eb06e28 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
@@ -74,12 +74,7 @@
executor = mThreadPoolExecutor;
}
- int cameraNumber = 0;
- try {
- cameraNumber = cameraFactory.getAvailableCameraIds().size();
- } catch (CameraUnavailableException e) {
- e.printStackTrace();
- }
+ int cameraNumber = cameraFactory.getAvailableCameraIds().size();
// According to the document of ThreadPoolExecutor, "If there are more than corePoolSize
// but less than maximumPoolSize threads running, a new thread will be created only if
// the queue is full."
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index cf53190..ea5eea6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -563,7 +563,7 @@
mSchedulerHandler);
mCameraFactory = cameraFactoryProvider.newInstance(mAppContext,
- cameraThreadConfig);
+ cameraThreadConfig, null);
CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
mCameraXConfig.getDeviceSurfaceManagerProvider(null);
if (surfaceManagerProvider == null) {
@@ -572,7 +572,8 @@
+ "CameraDeviceSurfaceManager."));
}
mSurfaceManager = surfaceManagerProvider.newInstance(mAppContext,
- mCameraFactory.getCameraManager());
+ mCameraFactory.getCameraManager(),
+ mCameraFactory.getAvailableCameraIds());
UseCaseConfigFactory.Provider configFactoryProvider =
mCameraXConfig.getUseCaseConfigFactoryProvider(null);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 12fe5ea..ba1a31e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -622,6 +622,7 @@
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
setupEncoder(cameraId, resolution);
+ notifyReset();
}
}
});
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index d0a633d..00df7bd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -41,12 +42,13 @@
*
* @param context the android context
* @param cameraManager the camera manager object used to query the camera information.
+ * @param availableCameraIds current available camera ids.
* @return the factory instance
* @throws InitializationException if it fails to create the factory
*/
@NonNull
CameraDeviceSurfaceManager newInstance(@NonNull Context context,
- @Nullable Object cameraManager)
+ @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
throws InitializationException;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
index 6f35f16..3efbda6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.InitializationException;
@@ -39,11 +40,14 @@
*
* @param context the android context
* @param threadConfig the thread config to run the camera operations
+ * @param availableCamerasSelector a CameraSelector used to specify which cameras will be
+ * loaded and available to CameraX.
* @return the factory instance
* @throws InitializationException if it fails to create the factory.
*/
@NonNull CameraFactory newInstance(@NonNull Context context,
- @NonNull CameraThreadConfig threadConfig) throws InitializationException;
+ @NonNull CameraThreadConfig threadConfig,
+ @Nullable CameraSelector availableCamerasSelector) throws InitializationException;
}
/**
@@ -63,11 +67,9 @@
* Gets the ids of all available cameras.
*
* @return the list of available cameras
- * @throws CameraUnavailableException if unable to access cameras, perhaps due
- * to insufficient permissions.
*/
@NonNull
- Set<String> getAvailableCameraIds() throws CameraUnavailableException;
+ Set<String> getAvailableCameraIds();
/**
* Gets the camera manager instance that is used to access the camera API.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 15de65f..7fa7f6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -104,7 +104,7 @@
CameraInternal camera = new FakeCamera();
- CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+ CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
FakeCameraFactory cameraFactory = new FakeCameraFactory();
cameraFactory.insertDefaultBackCamera(camera.getCameraInfoInternal().getCameraId(),
() -> camera);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index c085467..25a96c7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -30,7 +30,6 @@
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.TagBundle
@@ -71,7 +70,6 @@
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicReference
-import kotlin.jvm.Throws
private const val MAX_IMAGES = 3
@@ -104,7 +102,7 @@
val camera = FakeCamera()
val cameraFactoryProvider =
- CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+ CameraFactory.Provider { _, _, _ ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
camera
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 3ee0eb8..986d317 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -23,6 +23,7 @@
import android.util.Rational
import android.util.Size
import android.view.Surface
+import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.SessionConfig
@@ -68,7 +69,7 @@
val camera = FakeCamera()
val cameraFactoryProvider =
- CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+ CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertDefaultBackCamera(
camera.cameraInfoInternal.cameraId
@@ -169,7 +170,7 @@
cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
// Set SurfaceProvider
- var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+ var receivedTransformationInfo: TransformationInfo? = null
preview.setSurfaceProvider { request ->
request.setTransformationInfoListener(
CameraXExecutors.directExecutor(),
@@ -209,7 +210,7 @@
ApplicationProvider.getApplicationContext(), TEST_CAMERA_SELECTOR
)
cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
- var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+ var receivedTransformationInfo: TransformationInfo? = null
preview.setSurfaceProvider { request ->
request.setTransformationInfoListener(
CameraXExecutors.directExecutor(),
@@ -248,7 +249,7 @@
// Get pending SurfaceRequest created by pipeline.
val pendingSurfaceRequest = preview.mCurrentSurfaceRequest
var receivedSurfaceRequest: SurfaceRequest? = null
- var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+ var receivedTransformationInfo: TransformationInfo? = null
// Act: set a SurfaceProvider after attachment.
preview.setSurfaceProvider { request ->
@@ -323,13 +324,12 @@
return bindToLifecycleAndGetResult(null).first
}
- private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?):
- SurfaceRequest.TransformationInfo {
- return bindToLifecycleAndGetResult(viewPort).second
- }
+ private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?): TransformationInfo {
+ return bindToLifecycleAndGetResult(viewPort).second
+ }
private fun bindToLifecycleAndGetResult(viewPort: ViewPort?): Pair<SurfaceRequest,
- SurfaceRequest.TransformationInfo> {
+ TransformationInfo> {
// Arrange.
val sessionOptionUnpacker =
{ _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
@@ -338,7 +338,7 @@
.setSessionOptionUnpacker(sessionOptionUnpacker)
.build()
var surfaceRequest: SurfaceRequest? = null
- var transformationInfo: SurfaceRequest.TransformationInfo? = null
+ var transformationInfo: TransformationInfo? = null
preview.setSurfaceProvider { request ->
request.setTransformationInfoListener(
CameraXExecutors.directExecutor(),
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
index 65074ab..2cc5e17 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
@@ -20,7 +20,6 @@
import android.os.Build
import android.os.Looper
import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.fakes.FakeAppConfig
import androidx.camera.testing.fakes.FakeCamera
@@ -52,7 +51,7 @@
val camera = FakeCamera()
val cameraFactoryProvider =
- CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+ CameraFactory.Provider { _, _, _ ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
camera
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index f38206c..a361f64 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,7 +26,6 @@
import androidx.camera.core.CameraXConfig
import androidx.camera.core.Preview
import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.testing.fakes.FakeAppConfig
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
@@ -422,7 +421,7 @@
@Test
fun bindUseCases_withNotExistedLensFacingCamera() {
val cameraFactoryProvider =
- CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+ CameraFactory.Provider { _, _, _ ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertCamera(
CameraSelector.LENS_FACING_BACK,
@@ -441,7 +440,7 @@
val appConfigBuilder = CameraXConfig.Builder()
.setCameraFactoryProvider(cameraFactoryProvider)
- .setDeviceSurfaceManagerProvider { _, _ -> FakeCameraDeviceSurfaceManager() }
+ .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
.setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
ProcessCameraProvider.configureInstance(appConfigBuilder.build())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 8a551a6..9dd7dec 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -38,7 +38,7 @@
/** Generates a fake {@link CameraXConfig}. */
@NonNull
public static CameraXConfig create() {
- CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+ CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
FakeCameraFactory cameraFactory = new FakeCameraFactory();
cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0,
() -> new FakeCamera(CAMERA_ID_0, null,
@@ -52,7 +52,7 @@
};
CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
- (context, cameraManager) -> new FakeCameraDeviceSurfaceManager();
+ (ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
CameraXConfig.Builder appConfigBuilder =
new CameraXConfig.Builder()
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
index 8e0613f..3b16b89 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -627,6 +627,7 @@
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
setupEncoder(cameraId, resolution);
+ notifyReset();
}
}
});
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
index 5c97071..085efd3 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.os.Build
import android.os.Looper
+import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraX
import androidx.camera.core.CameraXConfig
import androidx.camera.core.impl.CameraFactory
@@ -54,7 +55,7 @@
val camera = FakeCamera()
val cameraFactoryProvider =
- CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+ CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
val cameraFactory = FakeCameraFactory()
cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
camera
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 21e6f30..46089e9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -114,6 +114,7 @@
*/
@UseExperimental(markerClass = ExperimentalVideo.class)
@Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef(flag = true, value = {IMAGE_CAPTURE, IMAGE_ANALYSIS, VIDEO_CAPTURE})
public @interface UseCases {
}
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index a304989..bee7e6ae 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -69,13 +69,13 @@
implementation(project(":camera:camera-camera2-pipe-integration"))
implementation(project(":camera:camera-core"))
implementation(project(":camera:camera-lifecycle"))
+ implementation(project(":appcompat:appcompat"))
+ implementation(project(":activity:activity"))
+ implementation(project(":fragment:fragment"))
implementation("androidx.concurrent:concurrent-futures:1.0.0")
// Android Support Library
api(CONSTRAINT_LAYOUT, { transitive = true })
- implementation("androidx.appcompat:appcompat:1.1.0")
- implementation("androidx.activity:activity:1.2.0-alpha05")
- implementation("androidx.fragment:fragment:1.3.0-alpha05")
implementation(GUAVA_ANDROID)
implementation(ESPRESSO_IDLING_RESOURCE)
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
index e6efb98..e288a77 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
@@ -36,6 +36,20 @@
}
@Test
+ public void defaultBackgroundColor_doesNotThrow() {
+ Action action = Action.builder().setTitle("Test").setBackgroundColor(
+ CarColor.DEFAULT).build();
+ }
+
+ @Test
+ public void backgroundColor_throws() {
+ Action action1 = Action.builder().setTitle("Test").setBackgroundColor(
+ CarColor.BLUE).build();
+ assertThrows(IllegalArgumentException.class,
+ () -> ActionStrip.builder().addAction(action1));
+ }
+
+ @Test
public void addDuplicatedTypes_throws() {
Action action1 = Action.BACK;
Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index ecfad77..00ce152 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -51,14 +51,15 @@
* <h4>Accessing Location</h4>
*
* When the app is running in the car display, the system will not consider it as being in the
- * foreground, and hence it will considered in the background for the purpose of retrieving location
- * as described <a
+ * foreground, and hence it will be considered in the background for the purpose of retrieving
+ * location as described <a
* href="https://developer.android.com/about/versions/10/privacy/changes#app-access-device
* -location">here</a>.
*
* <p>To reliably get location for your car app, we recommended that you use a <a
* href="https://developer.android.com/guide/components/services?#Types-of-services">foreground
- * service</a>.
+ * service</a>. Also note that accessing location may become unreliable when the phone is in the
+ * battery saver mode.
*/
// This lint warning is triggered because this has a finish() API. Suppress because we are not
// actually cleaning any held resources in that method.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
index 19ee2d5..ee98a8c 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
@@ -117,17 +117,23 @@
/**
* Adds an {@link Action} to the list.
*
+ * @throws IllegalArgumentException if the background color of the action is specified.
* @throws IllegalArgumentException if {@code action} is a standard action and an action of
* the same type has already been added.
* @throws NullPointerException if {@code action} is {@code null}.
*/
@NonNull
public Builder addAction(@NonNull Action action) {
- int actionType = requireNonNull(action).getType();
+ Action actionObj = requireNonNull(action);
+ int actionType = actionObj.getType();
if (actionType != Action.TYPE_CUSTOM && mAddedActionTypes.contains(actionType)) {
throw new IllegalArgumentException(
"Duplicated action types are disallowed: " + action);
}
+ if (!CarColor.DEFAULT.equals(actionObj.getBackgroundColor())) {
+ throw new IllegalArgumentException(
+ "Action strip actions don't support background colors");
+ }
mAddedActionTypes.add(actionType);
mActions.add(action);
return this;
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 ae04556..5ae0e97 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
@@ -49,7 +49,8 @@
*
* <p>Similar to Android devices, car screens cover a wide range of pixel densities. To ensure that
* icons and images render well across all car screens, use vector assets whenever possible to avoid
- * scaling issues.
+ * scaling issues. If you use a bitmap instead, ensure that you have resources that address multiple
+ * pixel density buckets.
*
* <p>In order to support all car screen sizes and pixel density, you can use configuration
* qualifiers in your resource files (e.g. "mdpi", "hdpi", etc). See
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 49e6d3a..95fa7cf 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -69,7 +69,7 @@
/**
* Represents a large image to be displayed in the grid item.
*
- * <p>If necessary, these images will be scaled down to fit within a 80 x 80 dp bounding box,
+ * <p>If necessary, these images will be scaled down to fit within a 64 x 64 dp bounding box,
* preserving their aspect ratio.
*/
public static final int IMAGE_TYPE_LARGE = (1 << 1);
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 7a1c100..25721ed 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
@@ -58,6 +58,9 @@
* <p>A cue must always be set when the step is created and is used as a fallback when {@link
* Maneuver} is not set or is unavailable.
*
+ * <p>Some cluster displays do not support UTF-8 encoded characters, in which case unsupported
+ * characters will not be displayed properly.
+ *
* @throws NullPointerException if {@code cue} is {@code null}.
* @see Builder#setCue(CharSequence)
*/
diff --git a/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java b/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java
new file mode 100644
index 0000000..9994190
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.collection;
+
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ConcurrentModificationException;
+
+@RunWith(JUnit4.class)
+public class ArrayMapTest {
+ ArrayMap<String, String> mMap = new ArrayMap<>();
+
+ /**
+ * Attempt to generate a ConcurrentModificationException in ArrayMap.
+ * <p>
+ * ArrayMap is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+ * this up; ArrayMap should (in the spirit of the core Java collection types) make an effort to
+ * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+ * internals.
+ */
+ @Test
+ public void testConcurrentModificationException() throws Exception {
+ final int testLenMs = 5000;
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ int i = 0;
+ while (mMap != null) {
+ try {
+ mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ fail(e.getMessage());
+ } catch (ClassCastException e) {
+ fail(e.getMessage());
+ } catch (ConcurrentModificationException e) {
+ System.out.println("[successfully caught CME at put #" + i
+ + " size=" + (mMap == null ? "??" : String.valueOf(mMap.size()))
+ + "]");
+ }
+ }
+ }
+ }).start();
+ for (int i = 0; i < (testLenMs / 100); i++) {
+ try {
+ Thread.sleep(100);
+ mMap.clear();
+ } catch (InterruptedException e) {
+ } catch (ArrayIndexOutOfBoundsException e) {
+ fail(e.getMessage());
+ } catch (ClassCastException e) {
+ fail(e.getMessage());
+ } catch (ConcurrentModificationException e) {
+ System.out.println(
+ "[successfully caught CME at clear #"
+ + i + " size=" + mMap.size() + "]");
+ }
+ }
+ mMap = null; // will stop other thread
+ System.out.println();
+ }
+
+ /**
+ * Check to make sure the same operations behave as expected in a single thread.
+ */
+ @Test
+ public void testNonConcurrentAccesses() throws Exception {
+ for (int i = 0; i < 100000; i++) {
+ try {
+ mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT");
+ if (i % 200 == 0) {
+ System.out.print(".");
+ }
+ if (i % 500 == 0) {
+ mMap.clear();
+ System.out.print("X");
+ }
+ } catch (ConcurrentModificationException e) {
+ fail(e.getMessage());
+ }
+ }
+ }
+}
diff --git a/collection/collection/src/test/java/androidx/collection/ArraySetTest.java b/collection/collection/src/test/java/androidx/collection/ArraySetTest.java
new file mode 100644
index 0000000..643e67d
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/ArraySetTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.collection;
+
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ConcurrentModificationException;
+
+@RunWith(JUnit4.class)
+public class ArraySetTest {
+ ArraySet<String> mSet = new ArraySet<>();
+
+ @After
+ public void tearDown() {
+ mSet = null;
+ }
+
+ /**
+ * Attempt to generate a ConcurrentModificationException in ArraySet.
+ * <p>
+ * ArraySet is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+ * this up; ArraySet should (in the spirit of the core Java collection types) make an effort to
+ * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+ * internals.
+ */
+ @Test
+ public void testConcurrentModificationException() throws Exception {
+ final int testDurMs = 10_000;
+ System.out.println("Starting ArraySet concurrency test");
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ int i = 0;
+ while (mSet != null) {
+ try {
+ mSet.add(String.format("key %d", i++));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ fail(e.getMessage());
+ } catch (ClassCastException e) {
+ fail(e.getMessage());
+ } catch (ConcurrentModificationException e) {
+ System.out.println("[successfully caught CME at put #" + i
+ + " size=" + (mSet == null ? "??" : String.valueOf(mSet.size()))
+ + "]");
+ }
+ }
+ }
+ }).start();
+ for (int i = 0; i < (testDurMs / 100); i++) {
+ try {
+ if (mSet.size() % 4 == 0) {
+ mSet.clear();
+ }
+ System.out.print("X");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ fail(e.getMessage());
+ } catch (ClassCastException e) {
+ fail(e.getMessage());
+ } catch (ConcurrentModificationException e) {
+ System.out.println(
+ "[successfully caught CME at clear #" + i + " size=" + mSet.size() + "]");
+ }
+ }
+ }
+
+ /**
+ * Check to make sure the same operations behave as expected in a single thread.
+ */
+ @Test
+ public void testNonConcurrentAccesses() throws Exception {
+ for (int i = 0; i < 100000; i++) {
+ try {
+ mSet.add(String.format("key %d", i++));
+ if (i % 200 == 0) {
+ System.out.print(".");
+ }
+ if (i % 500 == 0) {
+ mSet.clear();
+ }
+ } catch (ConcurrentModificationException e) {
+ fail(e.getMessage());
+ }
+ }
+ }
+
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 668a4f8..1dbecc6 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -40,7 +40,6 @@
listOf(
ComposableDemo("Animated scrolling") { FancyScrollingDemo() },
ComposableDemo("animate()") { SingleValueAnimationDemo() },
- ComposableDemo("Swipe to dismiss") { SwipeToDismissDemo() }
)
),
DemoCategory(
@@ -58,9 +57,10 @@
DemoCategory(
"Suspend Animation Demos",
listOf(
- ComposableDemo("Spring back scrolling") { SpringBackScrollingDemo() },
ComposableDemo("Follow the tap") { SuspendAnimationDemo() },
ComposableDemo("Infinitely Animating") { InfiniteAnimationDemo() },
+ ComposableDemo("Spring back scrolling") { SpringBackScrollingDemo() },
+ ComposableDemo("Swipe to dismiss") { SwipeToDismissDemo() },
)
),
)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
index f131f17..9d70c16 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
@@ -21,6 +21,8 @@
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.isFinished
+import androidx.compose.animation.core.velocity
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
@@ -64,7 +66,6 @@
var scrollPosition by remember { mutableStateOf(0f) }
val itemWidth = remember { mutableStateOf(0f) }
- val isFlinging = remember { mutableStateOf(false) }
val mutatorMutex = remember { MutatorMutex() }
var animation by remember { mutableStateOf(AnimationState(scrollPosition)) }
@@ -91,31 +92,35 @@
launch {
mutatorMutex.mutate {
animation = AnimationState(scrollPosition, velocity)
+ val target = ExponentialDecay().getTarget(scrollPosition, velocity)
+ val springBackTarget: Float = calculateSpringBackTarget(
+ target,
+ velocity,
+ itemWidth.value
+ )
+
animation.animateDecay(ExponentialDecay()) {
scrollPosition = this.value
-
- val springBackTarget = calculateSpringBackTarget(
- targetValue,
- this.velocityVector.value, itemWidth.value
- )
-
// Spring back as soon as the target position is crossed.
- if ((this.velocityVector.value > 0 && value > springBackTarget) ||
- (this.velocityVector.value < 0 && value < springBackTarget)
+ if ((this.velocity > 0 && value > springBackTarget) ||
+ (this.velocity < 0 && value < springBackTarget)
) {
cancelAnimation()
- launch {
- animation.animateTo(
- springBackTarget,
- SpringSpec(
- dampingRatio = 0.8f,
- stiffness = 200f
- ),
- sequentialAnimation = true
- ) {
- scrollPosition = this.value
- }
- }
+ }
+ }
+
+ // The previous animation is either finished or interrupted (via
+ // cancelAnimation(). If interrupted, spring back.
+ if (!animation.isFinished) {
+ animation.animateTo(
+ springBackTarget,
+ SpringSpec(
+ dampingRatio = 0.8f,
+ stiffness = 200f
+ ),
+ sequentialAnimation = true
+ ) {
+ scrollPosition = this.value
}
}
}
@@ -125,9 +130,6 @@
}
Canvas(gesture.fillMaxWidth().preferredHeight(400.dp)) {
itemWidth.value = size.width / 2f
- if (isFlinging.value) {
- // Figure out what position to spring back to
- }
if (DEBUG) {
println(
"Anim, Spring back scrolling, redrawing with new" +
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
index ca72348..1e5ef0d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
@@ -16,166 +16,135 @@
package androidx.compose.animation.demos
-import androidx.compose.animation.animatedFloat
-import androidx.compose.animation.core.AnimationEndReason
-import androidx.compose.animation.core.ExponentialDecay
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.TargetAnimation
-import androidx.compose.animation.core.fling
-import androidx.compose.foundation.Canvas
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.animation.AndroidFlingDecaySpec
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.verticalDrag
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.DragObserver
-import androidx.compose.ui.gesture.rawDragGestureFilter
+import androidx.compose.ui.composed
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.gesture.util.VelocityTracker
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import kotlin.math.sign
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.abs
@Composable
fun SwipeToDismissDemo() {
Column {
- SwipeToDismiss()
+ var index by remember { mutableStateOf(0) }
+ Box(Modifier.height(500.dp).fillMaxWidth()) {
+ Box(
+ Modifier.swipeToDismiss(index).align(Alignment.BottomCenter).size(300.dp)
+ .background(pastelColors[index])
+ )
+ }
Text(
"Swipe up to dismiss",
fontSize = 30.sp,
- modifier = Modifier.padding(40.dp)
+ modifier = Modifier.padding(40.dp).align(Alignment.CenterHorizontally)
)
+ Button(
+ onClick = {
+ index = (index + 1) % pastelColors.size
+ },
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ ) {
+ Text("New Card")
+ }
}
}
-private val height = 1600f
-private val itemHeight = 1600f * 2 / 3f
-private val padding = 10f
+@OptIn(ExperimentalPointerInput::class)
+private fun Modifier.swipeToDismiss(index: Int): Modifier = composed {
+ val mutatorMutex = remember { MutatorMutex() }
+ var alpha by remember { mutableStateOf(1f) }
+ var offset by remember { mutableStateOf(0f) }
-@Composable
-private fun SwipeToDismiss() {
- val itemBottom = animatedFloat(height)
- val index = remember { mutableStateOf(0) }
- val itemWidth = remember { mutableStateOf(0f) }
- val isFlinging = remember { mutableStateOf(false) }
- val modifier = Modifier.rawDragGestureFilter(
- dragObserver = object : DragObserver {
- override fun onStart(downPosition: Offset) {
- itemBottom.setBounds(0f, height)
- if (isFlinging.value && itemBottom.targetValue < 100f) {
- reset()
+ remember(index) {
+ // Reset internal states if index has been updated
+ alpha = 1f
+ offset = 0f
+ }
+ this.pointerInput {
+ fun updateOffset(value: Float) {
+ offset = value
+ alpha = 1f - abs(offset / size.height)
+ }
+ coroutineScope {
+ while (true) {
+ val pointerId = handlePointerInput {
+ awaitFirstDown().id
}
- }
-
- private fun reset() {
- itemBottom.snapTo(height)
- index.value--
- if (index.value < 0) {
- index.value += pastelColors.size
+ val velocityTracker = VelocityTracker()
+ // Set a high priority on the mutatorMutex for gestures
+ mutatorMutex.mutate(MutatePriority.UserInput) {
+ handlePointerInput {
+ verticalDrag(pointerId) {
+ updateOffset(offset + it.positionChange().y)
+ velocityTracker.addPosition(
+ it.current.uptime,
+ it.current.position
+ )
+ }
+ }
}
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- itemBottom.snapTo(itemBottom.targetValue + dragDistance.y)
- return dragDistance
- }
-
- fun adjustTarget(velocity: Float): (Float) -> TargetAnimation? {
- return { target: Float ->
- // The velocity is fast enough to fly off screen
- if (target <= 0) {
- null
- } else {
- val animation = SpringSpec<Float>(
- dampingRatio = 0.8f, stiffness = 300f
- )
- val projectedTarget = target + sign(velocity) * 0.2f * height
- if (projectedTarget < 0.6 * height) {
- TargetAnimation(0f, animation)
+ val velocity = velocityTracker.calculateVelocity().pixelsPerSecond.y
+ launch {
+ // Use mutatorMutex to make sure drag gesture would cancel any on-going
+ // animation job.
+ mutatorMutex.mutate {
+ // Either fling out of the sight, or snap back
+ val animationState = AnimationState(offset, velocity)
+ val decay = AndroidFlingDecaySpec(this@pointerInput)
+ if (decay.getTarget(offset, velocity) >= -size.height) {
+ // Not enough velocity to be dismissed
+ animationState.animateTo(0f) {
+ updateOffset(value)
+ }
} else {
- TargetAnimation(height, animation)
+ animationState.animateDecay(decay) {
+ // End animation early if it reaches the bounds
+ if (value <= -size.height) {
+ cancelAnimation()
+ updateOffset(-size.height.toFloat())
+ } else {
+ updateOffset(value)
+ }
+ }
}
}
}
}
-
- override fun onStop(velocity: Offset) {
- isFlinging.value = true
- itemBottom.fling(
- velocity.y,
- ExponentialDecay(3.0f),
- adjustTarget(velocity.y),
- onEnd = { endReason, final, _ ->
- isFlinging.value = false
- if (endReason != AnimationEndReason.Interrupted && final == 0f) {
- reset()
- }
- }
- )
- }
}
- )
-
- val heightDp = with(AmbientDensity.current) { height.toDp() }
-
- Canvas(
- modifier.fillMaxWidth()
- .preferredHeight(heightDp)
- .onGloballyPositioned { coordinates ->
- itemWidth.value = coordinates.size.width * 2 / 3f
- }
- ) {
- val progress = 1 - itemBottom.value / height
- // TODO: this progress can be used to drive state transitions
- val alpha = 1f - FastOutSlowInEasing(progress)
- val horizontalOffset = progress * itemWidth.value
- drawLeftItems(horizontalOffset, itemWidth.value, itemHeight, index.value)
- drawDismissingItem(itemBottom.value, itemWidth.value, itemHeight, index.value + 1, alpha)
- }
+ }.offset(y = { offset }).graphicsLayer(alpha = alpha)
}
-private fun DrawScope.drawLeftItems(
- horizontalOffset: Float,
- width: Float,
- height: Float,
- index: Int
-) {
- val offset = Offset(center.x - width * 1.5f + horizontalOffset + padding, size.height - height)
- val rectSize = Size(width - (2 * padding), height)
- drawRect(pastelColors[index % pastelColors.size], offset, rectSize)
-
- if (offset.x >= 0) {
- // draw another item
- drawRect(
- pastelColors[(index - 1 + pastelColors.size) % pastelColors.size],
- offset - Offset(width, 0.0f),
- rectSize
- )
- }
-}
-
-private fun DrawScope.drawDismissingItem(
- bottom: Float,
- width: Float,
- height: Float,
- index: Int,
- alpha: Float
-) = drawRect(
- pastelColors[index % pastelColors.size],
- topLeft = Offset(center.x - width / 2 + padding, bottom - height),
- size = Size(width - (2 * padding), height),
- alpha = alpha
-)
-
internal val pastelColors = listOf(
Color(0xFFffd7d7),
Color(0xFFffe9d6),
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index d5b7b43..b12654e 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -31,20 +31,21 @@
1600 to "0.1.0-dev16",
1700 to "1.0.0-alpha06",
1800 to "1.0.0-alpha07",
- 1900 to "1.0.0-alpha08"
+ 1900 to "1.0.0-alpha08",
+ 2000 to "1.0.0-alpha09"
)
/**
* The minimum version int that this compiler is guaranteed to be compatible with. Typically
* this will match the version int that is in ComposeVersion.kt in the runtime.
*/
- private val minimumRuntimeVersionInt: Int = 1900
+ private val minimumRuntimeVersionInt: Int = 2000
/**
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- private val compilerVersion: String = "1.0.0-alpha08"
+ private val compilerVersion: String = "1.0.0-alpha09"
private val minimumRuntimeVersion: String
get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
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 1cb6ffb..23699a4 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
@@ -68,10 +68,10 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphicsLayer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.plus
@@ -83,7 +83,7 @@
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.fontFamily
import androidx.compose.ui.text.platform.font
import androidx.compose.ui.text.style.TextAlign
@@ -179,7 +179,7 @@
val inlineIndicatorId = "indicator"
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
append("The quick ")
if (animation.value) {
appendInlineContent(inlineIndicatorId)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index a36969c..163625a 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -18,6 +18,7 @@
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppWindow
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -31,6 +32,8 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.material.ButtonConstants
@@ -156,7 +159,8 @@
.background(color = Color(255, 255, 255, 10))
.fillMaxWidth()
) {
- Spacer(modifier = Modifier.height(60.dp))
+ ContextMenu()
+ Spacer(modifier = Modifier.height(30.dp))
Spacer(modifier = Modifier.height(60.dp))
Row {
Checkbox(
@@ -339,9 +343,9 @@
}
@Composable
-fun TextBox(text: String = "") {
+fun TextBox(text: String = "", modifier: Modifier = Modifier.height(30.dp)) {
Box(
- modifier = Modifier.height(30.dp),
+ modifier = modifier,
contentAlignment = Alignment.Center
) {
Text(
@@ -352,6 +356,45 @@
}
@Composable
+fun ContextMenu() {
+ val items = listOf("Item A", "Item B", "Item C", "Item D", "Item E", "Item F")
+ val showMenu = remember { mutableStateOf(false) }
+ val selectedIndex = remember { mutableStateOf(0) }
+
+ Surface(
+ modifier = Modifier
+ .padding(start = 4.dp, top = 2.dp),
+ color = Color(255, 255, 255, 40),
+ shape = RoundedCornerShape(4.dp)
+ ) {
+ DropdownMenu(
+ toggle = {
+ TextBox(
+ text = "Selected: ${items[selectedIndex.value]}",
+ modifier = Modifier
+ .height(26.dp)
+ .padding(start = 4.dp, end = 4.dp)
+ .clickable(onClick = { showMenu.value = true })
+ )
+ },
+ expanded = showMenu.value,
+ onDismissRequest = { showMenu.value = false }
+ ) {
+ items.forEachIndexed { index, name ->
+ DropdownMenuItem(
+ onClick = {
+ selectedIndex.value = index
+ showMenu.value = false
+ }
+ ) {
+ Text(text = name)
+ }
+ }
+ }
+ }
+}
+
+@Composable
fun RadioButton(text: String, state: MutableState<Boolean>) {
Box(
modifier = Modifier.height(30.dp),
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index c8141df..fb477e1 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -18,7 +18,6 @@
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppWindowAmbient
import androidx.compose.desktop.ComposePanel
-import androidx.compose.desktop.setContent
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
index 589cdc7..fd818bd 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.drawBehind
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -75,7 +75,7 @@
}
public interface IndicationInstance {
- method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+ method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
method public default void onDispose();
}
@@ -472,7 +472,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
}
public final class CoreTextKt {
@@ -518,6 +518,10 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class MaxLinesHeightModifierKt {
+ method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+ }
+
public final class TextFieldCursorKt {
method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -75,7 +75,7 @@
}
public interface IndicationInstance {
- method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+ method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
method public default void onDispose();
}
@@ -472,7 +472,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
}
public final class CoreTextKt {
@@ -518,6 +518,10 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class MaxLinesHeightModifierKt {
+ method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+ }
+
public final class TextFieldCursorKt {
method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -75,7 +75,7 @@
}
public interface IndicationInstance {
- method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+ method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
method public default void onDispose();
}
@@ -472,7 +472,7 @@
}
public final class CoreTextFieldKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, 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 long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
}
public final class CoreTextKt {
@@ -518,6 +518,10 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class MaxLinesHeightModifierKt {
+ method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+ }
+
public final class TextFieldCursorKt {
method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
index a4ee0a7..1f39a77 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.text.TextStyle
@Composable
-fun CoreTextFieldMinMaxDemo() {
+fun BasicTextFieldMinMaxDemo() {
ScrollableColumn {
TagLine("empty text, no maxLines")
TextFieldWithMaxLines("", maxLines = Int.MAX_VALUE)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
index 1959250..2411c77 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
@@ -22,7 +22,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
@@ -52,7 +52,7 @@
val text1 = "paragraph1 paragraph1 paragraph1 paragraph1 paragraph1"
val text2 = "paragraph2 paragraph2 paragraph2 paragraph2 paragraph2"
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
append(text1)
withStyle(ParagraphStyle()) {
append(text2)
@@ -64,7 +64,7 @@
@Composable
fun TextDemoParagraphTextAlign() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
TextAlign.values().forEach { textAlign ->
val str = List(4) { "TextAlign.$textAlign" }.joinToString(" ")
withStyle(ParagraphStyle(textAlign = textAlign)) {
@@ -114,7 +114,7 @@
val text2 = "TextIndent restLine TextIndent restLine TextIndent restLine"
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(ParagraphStyle(textIndent = TextIndent(firstLine = 20.sp))) {
append(text1)
}
@@ -131,7 +131,7 @@
val ltrText = "Hello World! Hello World! Hello World! Hello World! Hello World!"
val rtlText = "مرØبا بالعالم مرØبا بالعالم مرØبا بالعالم مرØبا بالعالم مرØبا بالعالم"
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(ParagraphStyle()) {
append(ltrText)
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 6e7724e..b4d10b0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -104,7 +104,7 @@
fun TagLine(tag: String) {
Text(
style = TextStyle(fontSize = fontSize8),
- text = annotatedString {
+ text = buildAnnotatedString {
append("\n")
withStyle(
style = SpanStyle(
@@ -121,7 +121,7 @@
@Composable
fun SecondTagLine(tag: String) {
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFFAAAAAA),
@@ -139,7 +139,7 @@
// This group of text composables show different color, fontSize, fontWeight and fontStyle in
// English.
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(
SpanStyle(
color = Color(0xFFFF0000),
@@ -186,7 +186,7 @@
// This group of text composables show different color, fontSize, fontWeight and fontStyle in
// Chinese, Arabic, and Hindi.
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFFFF0000),
@@ -227,7 +227,7 @@
fun TextDemoFontFamily() {
// This group of text composables show different fontFamilies in English.
Text(
- annotatedString {
+ buildAnnotatedString {
withStyle(
style = SpanStyle(
fontSize = fontSize8,
@@ -279,7 +279,7 @@
fun TextDemoLetterSpacing() {
// This group of text composables show different letterSpacing.
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = fontSize8)) {
append("$displayText ")
}
@@ -319,7 +319,7 @@
fun TextDemoBackground() {
// This group of text composables show different background.
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(style = SpanStyle(background = Color(0xFFFF0000))) {
append("$displayText ")
}
@@ -341,7 +341,7 @@
// This group of text composables show different Locales of the same Unicode codepoint.
val text = "\u82B1"
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(style = SpanStyle(localeList = LocaleList("ja-JP"))) {
append("$text ")
}
@@ -458,7 +458,7 @@
)
Text(
style = TextStyle(fontSize = fontSize8),
- text = annotatedString {
+ text = buildAnnotatedString {
append("text with ")
withStyle(style = SpanStyle(shadow = shadow)) {
append("shadow!")
@@ -471,7 +471,7 @@
fun TextDemoFontSizeScale() {
Text(
style = TextStyle(fontSize = fontSize8),
- text = annotatedString {
+ text = buildAnnotatedString {
for (i in 4..12 step 4) {
val scale = i * 0.1f
withStyle(style = SpanStyle(fontSize = scale.em)) {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
index 4bedcd7..9a8ecbc 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
@@ -22,7 +22,7 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.VerbatimTtsAnnotation
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.intl.LocaleList
@Composable
@@ -30,7 +30,7 @@
Column {
TagLine("Text to speech with different locales.")
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
pushStyle(SpanStyle(localeList = LocaleList("en-us")))
append("Hello!\n")
pop()
@@ -55,7 +55,7 @@
TagLine("VerbatimTtsAnnotation ")
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
append("This word is read verbatim: ")
pushTtsAnnotation(VerbatimTtsAnnotation(verbatim = "hello"))
append("hello\n")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
index dda5da3..725ef4c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.selection.SelectionContainer
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.LocaleList
@@ -61,7 +61,7 @@
fontWeight = FontWeight.W200,
fontStyle = FontStyle.Italic
),
- text = annotatedString {
+ text = buildAnnotatedString {
append(text = "$displayText ")
append(text = "$displayTextArabic ")
append(text = "$displayTextChinese ")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
index 4442e6b..9cf591b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
@@ -30,8 +30,8 @@
import androidx.compose.ui.selection.SelectionContainer
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -114,7 +114,7 @@
Row {
Column(Modifier.weight(1f)) {
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
append("To begin, follow the")
withStyle(link) {
append(" Jetpack Compose setup instructions ")
@@ -137,7 +137,7 @@
.background(rectColor)
)
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(commonStyle.toSpanStyle()) {
append(
"The setContent block defines the activity's layout. Instead of " +
@@ -165,7 +165,7 @@
style = commonStyle.merge(header2)
)
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(commonStyle.toSpanStyle()) {
withStyle(commonStyle.toParagraphStyle()) {
append(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 0f365b4..0788943 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -38,9 +38,9 @@
ComposableDemo("Tricky input field") { InputFieldTrickyUseCase() },
ComposableDemo("Focus transition") { TextFieldFocusTransition() },
ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
- ComposableDemo("TextField in Scroller") { TextFieldWithScrollerDemo() },
+ ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
ComposableDemo("Soft Wrap") { SoftWrapDemo() },
- ComposableDemo("Min/Max Lines") { CoreTextFieldMinMaxDemo() },
+ ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
ComposableDemo("Capitalization/AutoCorrect") { CapitalizationAutoCorrectDemo() },
ComposableDemo("TextFieldValue") { TextFieldValueDemo() }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
index eb8c609..aeb630d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
@@ -17,25 +17,49 @@
package androidx.compose.foundation.demos.text
import androidx.compose.foundation.ScrollableColumn
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
+private val LongText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam pellentesque" +
+ " arcu quis diam malesuada pulvinar. In id condimentum metus. Suspendisse potenti. " +
+ "Praesent arcu tortor, ultrices ut vehicula sit amet, accumsan id sem."
+
@Composable
-fun TextFieldWithScrollerDemo() {
+fun ScrollableTextFieldDemo() {
ScrollableColumn {
- val state = remember {
- mutableStateOf(List(100) { "Line: $it" }.joinToString("\n"))
- }
- BasicTextField(
- value = state.value,
- onValueChange = { state.value = it },
- modifier = Modifier
- .padding(20.dp)
- )
+ TagLine("Multiline with 200.dp height")
+ MultilineTextField()
+ TagLine("Single line with 200.dp width")
+ SingleLineTextField()
}
}
+
+@Composable
+fun MultilineTextField() {
+ val state = remember { mutableStateOf(LongText) }
+ BasicTextField(
+ value = state.value,
+ onValueChange = { state.value = it },
+ modifier = demoTextFieldModifiers.size(200.dp),
+ singleLine = false,
+ textStyle = TextStyle(fontSize = fontSize8)
+ )
+}
+
+@Composable
+fun SingleLineTextField() {
+ val state = remember { mutableStateOf(LongText) }
+ BasicTextField(
+ value = state.value,
+ onValueChange = { state.value = it },
+ modifier = demoTextFieldModifiers.width(200.dp),
+ singleLine = true,
+ textStyle = TextStyle(fontSize = fontSize8)
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
index fe4bcdc..58b2dca 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
@@ -28,14 +28,14 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.em
@Sampled
@Composable
fun InlineTextContentSample() {
val myId = "inlineContent"
- val text = annotatedString {
+ val text = buildAnnotatedString {
append("Hello")
// Append a placeholder string "[myBox]" and attach an annotation "inlineContent" on it.
appendInlineContent(myId, "[myBox]")
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 64dc751..591300c 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
@@ -23,6 +23,8 @@
import androidx.compose.foundation.layout.preferredSizeIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.test.R
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -50,7 +52,11 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.res.imageResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
@@ -516,4 +522,40 @@
Assert.assertEquals(Color.Red.toArgb(), getPixel(50, height - 1))
}
}
+
+ @Test
+ fun testPainterResourceWithImage() {
+ val testTag = "testTag"
+ var imageColor = Color.Black
+
+ rule.setContent {
+ val painterId = remember {
+ mutableStateOf(R.drawable.ic_vector_square_asset_test)
+ }
+ with(imageResource(R.drawable.ic_image_test)) {
+ // Sample the actual color of the image to make sure we are loading it properly
+ // when we toggle the state of the resource id
+ imageColor = toPixelMap()[width / 2, height / 2]
+ }
+ Image(
+ painterResource(painterId.value),
+ contentScale = ContentScale.FillBounds,
+ 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
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+
+ rule.onNodeWithTag(testTag).performClick()
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { imageColor }
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
index ea79178..509ba73 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
@@ -19,7 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.InspectableValue
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt
new file mode 100644
index 0000000..79c8407
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.ValueElement
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(InternalTextApi::class)
+class MaxLinesHeightModifierTest {
+
+ private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
+ " quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " +
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
+ "fugiat nulla pariatur."
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Before
+ fun before() {
+ isDebugInspectorInfoEnabled = true
+ }
+
+ @After
+ fun after() {
+ isDebugInspectorInfoEnabled = false
+ }
+
+ @Test
+ fun maxLinesHeight_shortInputText() {
+ val (textLayoutResult, height) = setTextFieldWithMaxLines(TextFieldValue("abc"), 5)
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+ assertThat(textLayoutResult.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test
+ fun maxLinesHeight_notApplied_infiniteMaxLines() {
+ val (textLayoutResult, height) =
+ setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.size.height).isEqualTo(height)
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun maxLinesHeight_invalidValue() {
+ rule.setContent {
+ CoreTextField(
+ value = TextFieldValue(),
+ onValueChange = {},
+ modifier = Modifier.maxLinesHeight(0, TextStyle.Default)
+ )
+ }
+ }
+
+ @Test
+ fun maxLinesHeight_longInputText() {
+ val (textLayoutResult, _) = setTextFieldWithMaxLines(TextFieldValue(longText), 2)
+
+ rule.runOnIdle {
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+ }
+ }
+
+ @Test
+ fun testInspectableValue() {
+ val modifier = Modifier.maxLinesHeight(10, TextStyle.Default) as InspectableValue
+ assertThat(modifier.nameFallback).isEqualTo("maxLinesHeight")
+ assertThat(modifier.inspectableElements.asIterable()).containsExactly(
+ ValueElement("maxLines", 10),
+ ValueElement("textStyle", TextStyle.Default)
+ )
+ }
+
+ private fun setTextFieldWithMaxLines(
+ textFieldValue: TextFieldValue,
+ maxLines: Int
+ ): Pair<TextLayoutResult?, Int?> {
+ var textLayoutResult: TextLayoutResult? = null
+ var height: Int? = null
+ val positionedLatch = CountDownLatch(1)
+
+ rule.setContent {
+ Box(
+ Modifier.onGloballyPositioned {
+ height = it.size.height
+ positionedLatch.countDown()
+ }
+ ) {
+ CoreTextField(
+ value = textFieldValue,
+ onValueChange = {},
+ textStyle = TextStyle.Default,
+ modifier = Modifier
+ .width(100.dp)
+ .maxLinesHeight(maxLines, TextStyle.Default),
+ onTextLayout = { textLayoutResult = it }
+ )
+ }
+ }
+ assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ return Pair(textLayoutResult, height)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
index e170d76..3afb9ea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
@@ -22,8 +22,10 @@
import androidx.compose.foundation.animation.FlingConfig
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.text.CoreTextField
import androidx.compose.foundation.text.TextFieldScrollerPosition
+import androidx.compose.foundation.text.maxLinesHeight
import androidx.compose.foundation.text.textFieldScroll
import androidx.compose.runtime.remember
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
@@ -102,9 +104,9 @@
onValueChange = {},
onTextLayout = { textLayoutResultRef.value = it },
softWrap = false,
- maxLines = 1,
modifier = Modifier
.preferredSize(width = 300.dp, height = 50.dp)
+ .maxLinesHeight(1, TextStyle.Default)
.textFieldScroll(
orientation = Orientation.Horizontal,
remember { scrollerPosition },
@@ -134,6 +136,37 @@
onTextLayout = { 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,
+ textLayoutResultRef,
+ )
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(scrollerPosition.maximum).isLessThan(Float.POSITIVE_INFINITY)
+ assertThat(scrollerPosition.maximum).isGreaterThan(0f)
+ }
+ }
+
+ @Test
+ fun testTextField_verticallyScrollable_withLongInput_whenMaxLinesProvided() {
+ val scrollerPosition = TextFieldScrollerPosition()
+ val value = TextFieldValue(longText)
+
+ rule.setContent {
+ val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
+ CoreTextField(
+ value = value,
+ onValueChange = {},
+ onTextLayout = { textLayoutResultRef.value = it },
+ modifier = Modifier
+ .preferredWidth(100.dp)
+ .maxLinesHeight(3, TextStyle.Default)
.textFieldScroll(
orientation = Orientation.Vertical,
remember { scrollerPosition },
@@ -162,9 +195,9 @@
onValueChange = {},
onTextLayout = { textLayoutResultRef.value = it },
softWrap = false,
- maxLines = 1,
modifier = Modifier
.preferredSize(width = 300.dp, height = 50.dp)
+ .maxLinesHeight(1, TextStyle.Default)
.textFieldScroll(
orientation = Orientation.Horizontal,
remember { scrollerPosition },
@@ -231,7 +264,6 @@
onValueChange = {},
onTextLayout = { textLayoutResultRef.value = it },
softWrap = false,
- maxLines = 1,
modifier = Modifier
.preferredSize(textFieldSize.toDp())
.textFieldScroll(
@@ -312,11 +344,11 @@
value = value,
onValueChange = {},
onTextLayout = { textLayoutResultRef.value = it },
- maxLines = 1,
softWrap = false,
modifier = Modifier
.preferredSize(width = 300.dp, height = 50.dp)
.testTag(TextfieldTag)
+ .maxLinesHeight(1, TextStyle.Default)
.textFieldScroll(
Orientation.Horizontal,
remember { scrollerPosition },
@@ -408,11 +440,11 @@
value = value,
onValueChange = {},
onTextLayout = { textLayoutResultRef.value = it },
- maxLines = 1,
softWrap = false,
modifier = Modifier
.preferredSize(width = 300.dp, height = 50.dp)
.testTag(TextfieldTag)
+ .maxLinesHeight(1, TextStyle.Default)
.textFieldScroll(
Orientation.Horizontal,
scrollerPosition,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
index 1dbed00..4d6ba65 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
@@ -20,7 +20,6 @@
import androidx.compose.animation.core.ManualAnimationClock
import androidx.compose.foundation.animation.FlingConfig
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -39,6 +38,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.TouchSlop
import androidx.compose.ui.platform.testTag
@@ -72,7 +72,6 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -335,46 +334,6 @@
.assertIsDisplayed()
}
- @Ignore("This test is not fully working. To be fixed in b/167913500")
- @Test
- fun contentPaddingIsApplied() = with(rule.density) {
- val itemTag = "item"
-
- rule.setContent {
- LazyColumnFor(
- items = listOf(1),
- modifier = Modifier.size(100.dp)
- .testTag(LazyColumnForTag),
- contentPadding = PaddingValues(
- start = 10.dp,
- top = 50.dp,
- end = 10.dp,
- bottom = 50.dp
- )
- ) {
- Spacer(Modifier.fillParentMaxWidth().preferredHeight(50.dp).testTag(itemTag))
- }
- }
-
- var itemBounds = rule.onNodeWithTag(itemTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
- assertThat(itemBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
- assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(10.dp.toIntPx())
- assertThat(itemBounds.right.toIntPx())
- .isWithin1PixelFrom(100.dp.toIntPx() - 10.dp.toIntPx())
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 51.dp, density = rule.density)
-
- itemBounds = rule.onNodeWithTag(itemTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(0)
- assertThat(itemBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
- }
-
@Test
fun lazyColumnWrapsContent() = with(rule.density) {
val itemInsideLazyColumn = "itemInsideLazyColumn"
@@ -1020,6 +979,67 @@
}
}
+ @Test
+ fun itemsAreNotRedrawnDuringScroll() {
+ val items = (0..20).toList()
+ val redrawCount = Array(6) { 0 }
+ rule.setContent {
+ LazyColumnFor(
+ items = items,
+ modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
+ ) {
+ Spacer(
+ Modifier.size(20.dp)
+ .drawBehind { redrawCount[it]++ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag(LazyColumnForTag)
+ .scrollBy(y = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ redrawCount.forEachIndexed { index, i ->
+ assertWithMessage("Item with index $index was redrawn $i times")
+ .that(i).isEqualTo(1)
+ }
+ }
+ }
+
+ @Test
+ fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+ val items = (0..1).toList()
+ val redrawCount = Array(2) { 0 }
+ var stateUsedInDrawScope by mutableStateOf(false)
+ rule.setContent {
+ LazyColumnFor(
+ items = items,
+ modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
+ ) {
+ Spacer(
+ Modifier.size(50.dp)
+ .drawBehind {
+ redrawCount[it]++
+ if (it == 1) {
+ stateUsedInDrawScope.hashCode()
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ stateUsedInDrawScope = true
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First items is not expected to be redrawn")
+ .that(redrawCount[0]).isEqualTo(1)
+ assertWithMessage("Second items is expected to be redrawn")
+ .that(redrawCount[1]).isEqualTo(2)
+ }
+ }
+
private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
new file mode 100644
index 0000000..c77dee6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -0,0 +1,607 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListsContentPaddingTest {
+ private val LazyListTag = "LazyList"
+ private val ItemTag = "item"
+ private val ContainerTag = "container"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ }
+
+ @Test
+ fun column_contentPaddingIsApplied() {
+ lateinit var state: LazyListState
+ val containerSize = itemSize * 2
+ val smallPaddingSize = itemSize / 4
+ val largePaddingSize = itemSize
+ rule.setContent {
+ LazyColumnFor(
+ items = listOf(1),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(containerSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = smallPaddingSize,
+ top = largePaddingSize,
+ end = smallPaddingSize,
+ bottom = largePaddingSize
+ )
+ ) {
+ Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ .assertTopPositionInRootIsEqualTo(largePaddingSize)
+ .assertWidthIsEqualTo(containerSize - smallPaddingSize * 2)
+ .assertHeightIsEqualTo(itemSize)
+
+ state.scrollBy(largePaddingSize)
+
+ rule.onNodeWithTag(ItemTag)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_contentPaddingIsNotAffectingScrollPosition() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumnFor(
+ items = listOf(1),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = itemSize,
+ bottom = itemSize
+ )
+ ) {
+ Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ }
+ }
+
+ state.assertScrollPosition(0, 0.dp)
+
+ state.scrollBy(itemSize)
+
+ state.assertScrollPosition(0, itemSize)
+ }
+
+ @Test
+ fun column_scrollForwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(padding)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize + padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+ state.scrollBy(padding)
+
+ state.assertScrollPosition(1, padding - itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun column_scrollBackwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize + padding * 2)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize * 1.5f)
+
+ state.assertScrollPosition(1, itemSize * 0.5f)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+ }
+
+ @Test
+ fun column_scrollForwardTillTheEnd() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+ // there are no space to scroll anymore, so it should change nothing
+ state.scrollBy(10.dp)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+ }
+
+ @Test
+ fun column_scrollForwardTillTheEndAndABitBack() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize / 2)
+
+ state.assertScrollPosition(2, itemSize / 2)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ }
+
+ @Test
+ fun column_contentPaddingAndWrapContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyColumnFor(
+ items = listOf(1),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(2.dp)
+ .assertTopPositionInRootIsEqualTo(4.dp)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+ .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+ }
+
+ @Test
+ fun column_contentPaddingAndNoContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyColumnFor(
+ items = listOf(0),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(8.dp)
+ .assertHeightIsEqualTo(12.dp)
+ }
+
+ @Test
+ fun row_contentPaddingIsApplied() {
+ lateinit var state: LazyListState
+ val containerSize = itemSize * 2
+ val smallPaddingSize = itemSize / 4
+ val largePaddingSize = itemSize
+ rule.setContent {
+ LazyRowFor(
+ items = listOf(1),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(containerSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ top = smallPaddingSize,
+ start = largePaddingSize,
+ bottom = smallPaddingSize,
+ end = largePaddingSize
+ )
+ ) {
+ Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ .assertLeftPositionInRootIsEqualTo(largePaddingSize)
+ .assertHeightIsEqualTo(containerSize - smallPaddingSize * 2)
+ .assertWidthIsEqualTo(itemSize)
+
+ state.scrollBy(largePaddingSize)
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_contentPaddingIsNotAffectingScrollPosition() {
+ lateinit var state: LazyListState
+ val itemSize = with(rule.density) {
+ 50.dp.toIntPx().toDp()
+ }
+ rule.setContent {
+ LazyRowFor(
+ items = listOf(1),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = itemSize,
+ end = itemSize
+ )
+ ) {
+ Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ }
+ }
+
+ state.assertScrollPosition(0, 0.dp)
+
+ state.scrollBy(itemSize)
+
+ state.assertScrollPosition(0, itemSize)
+ }
+
+ @Test
+ fun row_scrollForwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyRowFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = padding,
+ end = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(padding)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize + padding)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+ state.scrollBy(padding)
+
+ state.assertScrollPosition(1, padding - itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun row_scrollBackwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyRowFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize + padding * 2)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = padding,
+ end = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize * 1.5f)
+
+ state.assertScrollPosition(1, itemSize * 0.5f)
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+ }
+
+ @Test
+ fun row_scrollForwardTillTheEnd() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyRowFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = padding,
+ end = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+ // there are no space to scroll anymore, so it should change nothing
+ state.scrollBy(10.dp)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+ }
+
+ @Test
+ fun row_scrollForwardTillTheEndAndABitBack() {
+ lateinit var state: LazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyRowFor(
+ items = (0..3).toList(),
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ contentPadding = PaddingValues(
+ start = padding,
+ end = padding
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize / 2)
+
+ state.assertScrollPosition(2, itemSize / 2)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ }
+
+ @Test
+ fun row_contentPaddingAndWrapContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyRowFor(
+ items = listOf(1),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(2.dp)
+ .assertTopPositionInRootIsEqualTo(4.dp)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+ .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+ }
+
+ @Test
+ fun row_contentPaddingAndNoContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyRowFor(
+ items = listOf(0),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(8.dp)
+ .assertHeightIsEqualTo(12.dp)
+ }
+
+ private fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking {
+ smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+ }
+ }
+
+ private fun LazyListState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+ assertThat(firstVisibleItemIndex).isEqualTo(index)
+ assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
index 9999744..6540372 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredSize
@@ -32,6 +31,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -50,9 +50,9 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -171,46 +171,6 @@
.assertIsDisplayed()
}
- @Ignore("This test is not fully working. To be fixed in b/167913500")
- @Test
- fun contentPaddingIsApplied() = with(rule.density) {
- val itemTag = "item"
-
- rule.setContent {
- LazyRowFor(
- items = listOf(1),
- modifier = Modifier.size(100.dp)
- .testTag(LazyRowForTag),
- contentPadding = PaddingValues(
- start = 50.dp,
- top = 10.dp,
- end = 50.dp,
- bottom = 10.dp
- )
- ) {
- Spacer(Modifier.fillParentMaxHeight().preferredWidth(50.dp).testTag(itemTag))
- }
- }
-
- var itemBounds = rule.onNodeWithTag(itemTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
- assertThat(itemBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
- assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(10.dp.toIntPx())
- assertThat(itemBounds.bottom.toIntPx())
- .isWithin1PixelFrom(100.dp.toIntPx() - 10.dp.toIntPx())
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 51.dp, density = rule.density)
-
- itemBounds = rule.onNodeWithTag(itemTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(0)
- assertThat(itemBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
- }
-
@Test
fun lazyRowWrapsContent() = with(rule.density) {
val itemInsideLazyRow = "itemInsideLazyRow"
@@ -815,4 +775,65 @@
assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
}
}
+
+ @Test
+ fun itemsAreNotRedrawnDuringScroll() {
+ val items = (0..20).toList()
+ val redrawCount = Array(6) { 0 }
+ rule.setContent {
+ LazyRowFor(
+ items = items,
+ modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
+ ) {
+ Spacer(
+ Modifier.size(20.dp)
+ .drawBehind { redrawCount[it]++ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag(LazyRowForTag)
+ .scrollBy(x = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ redrawCount.forEachIndexed { index, i ->
+ Truth.assertWithMessage("Item with index $index was redrawn $i times")
+ .that(i).isEqualTo(1)
+ }
+ }
+ }
+
+ @Test
+ fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+ val items = (0..1).toList()
+ val redrawCount = Array(2) { 0 }
+ var stateUsedInDrawScope by mutableStateOf(false)
+ rule.setContent {
+ LazyRowFor(
+ items = items,
+ modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
+ ) {
+ Spacer(
+ Modifier.size(50.dp)
+ .drawBehind {
+ redrawCount[it]++
+ if (it == 1) {
+ stateUsedInDrawScope.hashCode()
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ stateUsedInDrawScope = true
+ }
+
+ rule.runOnIdle {
+ Truth.assertWithMessage("First items is not expected to be redrawn")
+ .that(redrawCount[0]).isEqualTo(1)
+ Truth.assertWithMessage("Second items is expected to be redrawn")
+ .that(redrawCount[1]).isEqualTo(2)
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt
deleted file mode 100644
index 2591fcf..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt
+++ /dev/null
@@ -1,179 +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.foundation.text
-
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Providers
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.InternalTextApi
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.font.ResourceFont
-import androidx.compose.ui.text.font.asFontFamily
-import androidx.compose.ui.text.font.test.R
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-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 com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(InternalTextApi::class)
-class CoreTextFieldMinMaxLineTest {
-
- private val fontFamily = ResourceFont(
- resId = R.font.sample_font,
- weight = FontWeight.Normal,
- style = FontStyle.Normal
- ).asFontFamily()
-
- private val density = Density(density = 1f, fontScale = 1f)
-
- @get:Rule
- val rule = createComposeRule()
-
- @Test(expected = IllegalArgumentException::class)
- fun textField_maxLines_should_be_greater_than_zero() {
- rule.setContent {
- Providers(AmbientDensity provides density) {
- CoreTextField(
- value = TextFieldValue(""),
- onValueChange = {},
- maxLines = 0,
- )
- }
- }
- }
-
- @Test
- fun textField_maxLines_moreThanLineCount_doesNotChangeHeight() {
- val string = "a"
- val fontSize = 10.sp
- val composableWidth = 20.dp // line count will be 1
- val expectedLineCount = 1
- val maxLines = 2 // any number greater than 1
- val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
- val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
- with(density) {
- assertThat(textLayout).isNotNull()
- assertThat(height).isNotNull()
- assertThat(height).isEqualTo(fontSize.toIntPx())
- assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
- }
- }
-
- @Test
- fun textField_maxLines_equalToLineCount_doesNotChangeHeight() {
- val string = "a"
- val fontSize = 10.sp
- val composableWidth = 20.dp // line count will be 1
- val expectedLineCount = 1
- val maxLines = 1 // equal to expectedLineCount
- val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
- val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
- with(density) {
- assertThat(textLayout).isNotNull()
- assertThat(height).isNotNull()
- assertThat(height).isEqualTo(fontSize.toIntPx())
- assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
- }
- }
-
- @Test
- fun textField_maxLines_lessThanLineCount_changesHeight() {
- val density = Density(density = 1f, fontScale = 1f)
- val string = "a".repeat(10)
- val fontSize = 10.sp
- val composableWidth = 20.dp // line count will be 10 * 10 / 20 = 5
- val expectedLineCount = 5
- val maxLines = 1
-
- val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
- val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
- with(density) {
- assertThat(textLayout).isNotNull()
- assertThat(height).isNotNull()
- assertThat(height).isEqualTo(fontSize.toIntPx())
- assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
- }
- }
-
- @Test
- fun textField_maxLines_withEmptyText() {
- val string = ""
- val fontSize = 10.sp
- val composableWidth = 10.dp
- val maxLines = 5
- val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
- val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
- with(density) {
- assertThat(textLayout).isNotNull()
- assertThat(height).isNotNull()
- assertThat(height).isEqualTo(fontSize.toIntPx())
- assertThat(textLayout?.lineCount).isEqualTo(1)
- }
- }
-
- private fun setContentMaxLines(
- string: String,
- textStyle: TextStyle,
- width: Dp,
- maxLines: Int = Int.MAX_VALUE
- ): Pair<TextLayoutResult?, Int?> {
- var textLayout: TextLayoutResult? = null
- var height: Int? = null
-
- rule.setContent {
- Providers(AmbientDensity provides density) {
- CoreTextField(
- value = TextFieldValue(string),
- onValueChange = {},
- textStyle = textStyle,
- onTextLayout = { textLayout = it },
- maxLines = maxLines,
- modifier = Modifier
- .width(width)
- .onGloballyPositioned {
- height = it.size.height
- }
- )
- }
- }
-
- return Pair(textLayout, height)
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index a5f7e13..8297a04 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -43,7 +43,10 @@
import androidx.compose.ui.platform.AmbientView
import androidx.compose.ui.selection.Selection
import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextStyle
@@ -59,6 +62,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -396,6 +400,8 @@
}
}
+private fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
+
private class PointerInputChangeLog : (PointerEvent, PointerEventPass) -> Unit {
val entries = mutableListOf<PointerInputChangeLogEntry>()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index dd838db..1570a3a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -17,12 +17,8 @@
package androidx.compose.foundation.text.selection
import android.view.View
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.window.isPopupLayout
import androidx.test.espresso.Espresso
import androidx.test.espresso.Root
@@ -58,15 +54,3 @@
return matches && popupsMatchedSoFar == index + 1
}
}
-
-internal fun ComposeTestRule.rootWidth(): Dp {
- val nodeInteraction = onRoot()
- val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
-
- @OptIn(ExperimentalLayoutNodeApi::class)
- val owner = node.componentNode.owner as AndroidOwner
-
- return with(owner.density) {
- owner.view.width.toDp()
- }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
new file mode 100644
index 0000000..4eb583c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
Binary files differ
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
new file mode 100644
index 0000000..13c4896
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF0000"
+ android:pathData="L24,0,24,24,0,24z"/>
+</vector>
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 31b3a08..9235b86 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -16,7 +16,7 @@
package androidx.compose.foundation
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
index aa0dd3f..6b94200 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
@@ -21,7 +21,7 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 04594e9..c30a7c6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -289,7 +289,6 @@
isScrollEnabled,
reverseScrolling = reverseScrollDirection
)
- .clipToBounds()
.padding(contentPadding),
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
@@ -334,7 +333,6 @@
isScrollEnabled,
reverseScrolling = reverseScrollDirection
)
- .clipToBounds()
.padding(contentPadding),
horizontalArrangement = horizontalArrangement,
verticalAlignment = verticalAlignment,
@@ -471,7 +469,7 @@
val absScroll = if (isReversed) scroll - side else -scroll
val xOffset = if (isVertical) 0 else absScroll.roundToInt()
val yOffset = if (isVertical) absScroll.roundToInt() else 0
- placeable.placeRelative(xOffset, yOffset)
+ placeable.placeRelativeWithLayer(xOffset, yOffset)
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 8a5801a..b084605 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -45,6 +45,8 @@
val cachingItemContentFactory = remember { CachingItemContentFactory(itemContentFactory) }
cachingItemContentFactory.itemContentFactory = itemContentFactory
+ val startContentPadding = if (isVertical) contentPadding.top else contentPadding.start
+ val endContentPadding = if (isVertical) contentPadding.bottom else contentPadding.end
SubcomposeLayout(
modifier
.scrollable(
@@ -66,6 +68,8 @@
isVertical,
horizontalAlignment,
verticalAlignment,
+ startContentPadding.toIntPx(),
+ endContentPadding.toIntPx(),
itemsCount,
cachingItemContentFactory
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 45ab3e43..456a124 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -292,11 +292,15 @@
isVertical: Boolean,
horizontalAlignment: Alignment.Horizontal,
verticalAlignment: Alignment.Vertical,
+ startContentPadding: Int,
+ endContentPadding: Int,
itemsCount: Int,
itemContentFactory: (Int) -> @Composable () -> Unit
): MeasureResult = with(scope) {
numMeasurePasses++
constraints.assertNotNestingScrollableContainers(isVertical)
+ require(startContentPadding >= 0)
+ require(endContentPadding >= 0)
if (itemsCount <= 0) {
// empty data set. reset the current scroll and report zero size
scrollPosition.update(
@@ -340,27 +344,35 @@
var goingForwardInitialScrollOffset = currentFirstItemScrollOffset
// this will contain all the placeables representing the visible items
- val visibleItemsPlaceables = mutableListOf<Placeable>()
+ val visibleItemsPlaceables = mutableListOf<List<Placeable>>()
- // we had scrolled backward, which means items before current firstItemScrollOffset
- // became visible. compose them and update firstItemScrollOffset
+ // include the start padding so we compose items in the padding area. in the end we
+ // will remove it back from the currentFirstItemScrollOffset calculation
+ currentFirstItemScrollOffset -= startContentPadding
+
+ // define min and max offsets (min offset currently includes startPadding)
+ val minOffset = -startContentPadding
+ val maxOffset = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
+
+ // we had scrolled backward or we compose items in the start padding area, which means
+ // items before current firstItemScrollOffset should be visible. compose them and update
+ // firstItemScrollOffset
while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
val previous = DataIndex(currentFirstItemIndex.value - 1)
val placeables =
subcompose(previous, itemContentFactory(previous.value)).fastMap {
it.measure(childConstraints)
}
- visibleItemsPlaceables.addAll(0, placeables)
- val size = placeables.fastSumBy { if (isVertical) it.height else it.width }
- currentFirstItemScrollOffset += size
+ visibleItemsPlaceables.add(0, placeables)
+ currentFirstItemScrollOffset += placeables.mainAxisSize(isVertical)
currentFirstItemIndex = previous
}
// if we were scrolled backward, but there were not enough items before. this means
// not the whole scroll was consumed
- if (currentFirstItemScrollOffset < 0) {
+ if (currentFirstItemScrollOffset < minOffset) {
scrollDelta += currentFirstItemScrollOffset
goingForwardInitialScrollOffset += currentFirstItemScrollOffset
- currentFirstItemScrollOffset = 0
+ currentFirstItemScrollOffset = minOffset
}
// remembers the composed placeables which we are not currently placing as they are out
@@ -371,7 +383,7 @@
// composing visible items starting from goingForwardInitialIndex until we fill the
// whole viewport
var index = goingForwardInitialIndex
- val maxMainAxis = if (isVertical) constraints.maxHeight else constraints.maxWidth
+ val maxMainAxis = maxOffset + endContentPadding
var mainAxisUsed = -goingForwardInitialScrollOffset
var maxCrossAxis = 0
while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
@@ -386,7 +398,7 @@
}
mainAxisUsed += size
- if (mainAxisUsed < 0f) {
+ if (mainAxisUsed < minOffset) {
// this item is offscreen and will not be placed. advance firstVisibleItemIndex
currentFirstItemIndex = index + 1
currentFirstItemScrollOffset -= size
@@ -397,7 +409,7 @@
}
notUsedButComposedItems.add(placeables)
} else {
- visibleItemsPlaceables.addAll(placeables)
+ visibleItemsPlaceables.add(placeables)
}
index++
@@ -405,8 +417,8 @@
// we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
// lets try to scroll back if we have enough items before firstVisibleItemIndex.
- if (mainAxisUsed < maxMainAxis) {
- val toScrollBack = maxMainAxis - mainAxisUsed
+ if (mainAxisUsed < maxOffset) {
+ val toScrollBack = maxOffset - mainAxisUsed
currentFirstItemScrollOffset -= toScrollBack
mainAxisUsed += toScrollBack
while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
@@ -419,16 +431,16 @@
it.measure(childConstraints)
}
}
- visibleItemsPlaceables.addAll(0, placeables)
- val size = placeables.fastSumBy { if (isVertical) it.height else it.width }
+ visibleItemsPlaceables.add(0, placeables)
+ val size = placeables.mainAxisSize(isVertical)
currentFirstItemScrollOffset += size
currentFirstItemIndex = previous
}
scrollDelta += toScrollBack
- if (currentFirstItemScrollOffset < 0) {
+ if (currentFirstItemScrollOffset < minOffset) {
scrollDelta += currentFirstItemScrollOffset
mainAxisUsed += currentFirstItemScrollOffset
- currentFirstItemScrollOffset = 0
+ currentFirstItemScrollOffset = minOffset
}
}
@@ -446,40 +458,71 @@
// Wrap the content of the children
val layoutWidth = constraints.constrainWidth(
- if (isVertical) maxCrossAxis else mainAxisUsed
+ if (isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
)
val layoutHeight = constraints.constrainHeight(
- if (!isVertical) maxCrossAxis else mainAxisUsed
+ if (!isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
)
+ // the initial offset for placeables in visibleItemsPlaceables
+ val firstPlaceableOffset = -(currentFirstItemScrollOffset + startContentPadding)
+
+ // compensate the content padding we initially added in currentFirstItemScrollOffset.
+ // if the item is fully located in the start padding area we need to use the next
+ // item as a value for currentFirstItemIndex
+ if (startContentPadding > 0) {
+ currentFirstItemScrollOffset += startContentPadding
+ var startPaddingItems = 0
+ while (startPaddingItems < visibleItemsPlaceables.lastIndex) {
+ val size = visibleItemsPlaceables[startPaddingItems].mainAxisSize(isVertical)
+ if (size <= currentFirstItemScrollOffset) {
+ startPaddingItems++
+ currentFirstItemScrollOffset -= size
+ currentFirstItemIndex++
+ } else {
+ break
+ }
+ }
+ }
+
// update state with the new calculated scroll position
scrollPosition.update(
index = currentFirstItemIndex,
scrollOffset = currentFirstItemScrollOffset,
- canScrollForward = mainAxisUsed > maxMainAxis
+ canScrollForward = mainAxisUsed > maxOffset
)
return layout(layoutWidth, layoutHeight) {
- var currentMainAxis = -currentFirstItemScrollOffset
- visibleItemsPlaceables.fastForEach {
- if (isVertical) {
- val x = horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
- if (currentMainAxis + it.height > 0 && currentMainAxis < layoutHeight) {
- it.place(x, currentMainAxis)
+ var currentMainAxis = firstPlaceableOffset
+ visibleItemsPlaceables.fastForEach { placeables ->
+ placeables.fastForEach {
+ if (isVertical) {
+ val x =
+ horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
+ if (currentMainAxis + it.height > minOffset &&
+ currentMainAxis < layoutHeight + endContentPadding
+ ) {
+ it.placeWithLayer(x, currentMainAxis)
+ }
+ currentMainAxis += it.height
+ } else {
+ val y = verticalAlignment.align(it.height, layoutHeight)
+ if (currentMainAxis + it.width > minOffset &&
+ currentMainAxis < layoutWidth + endContentPadding
+ ) {
+ it.placeRelativeWithLayer(currentMainAxis, y)
+ }
+ currentMainAxis += it.width
}
- currentMainAxis += it.height
- } else {
- val y = verticalAlignment.align(it.height, layoutHeight)
- if (currentMainAxis + it.width > 0 && currentMainAxis < layoutWidth) {
- it.placeRelative(currentMainAxis, y)
- }
- currentMainAxis += it.width
}
}
}
}
}
+ private fun List<Placeable>.mainAxisSize(isVertical: Boolean) =
+ fastSumBy { if (isVertical) it.height else it.width }
+
companion object {
/**
* The default [Saver] implementation for [LazyListState].
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 ee9880d6..57b9d1b 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
@@ -213,6 +213,7 @@
val scrollerPosition = rememberSavedInstanceState(saver = TextFieldScrollerPosition.Saver) {
TextFieldScrollerPosition()
}
+
CoreTextField(
value = value,
onValueChange = onValueChange,
@@ -226,14 +227,15 @@
onTextInputStarted = onTextInputStarted,
cursorColor = cursorColor,
imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
- maxLines = if (singleLine) 1 else maxLines,
softWrap = !singleLine,
- modifier = modifier.textFieldScroll(
- orientation,
- scrollerPosition,
- value,
- visualTransformation,
- textLayoutResult
- )
+ modifier = modifier
+ .maxLinesHeight(if (singleLine) 1 else maxLines, textStyle)
+ .textFieldScroll(
+ orientation,
+ scrollerPosition,
+ value,
+ visualTransformation,
+ textLayoutResult
+ )
)
}
\ 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 3d130cb..37b6ebd 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
@@ -126,8 +126,6 @@
* @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.
*/
@Composable
@OptIn(
@@ -147,13 +145,8 @@
onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
cursorColor: Color = Color.Unspecified,
softWrap: Boolean = true,
- maxLines: Int = Int.MAX_VALUE,
imeOptions: ImeOptions = ImeOptions.Default
) {
- require(maxLines > 0) {
- "maxLines should be greater than 0"
- }
-
// 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.
val recompose = invalidate
@@ -416,7 +409,8 @@
onDispose { manager.hideSelectionToolbar() }
- val modifiers = modifier.focusRequester(focusRequester)
+ val modifiers = modifier
+ .focusRequester(focusRequester)
.then(focusObserver)
.then(cursorModifier)
.then(pointerModifier)
@@ -433,7 +427,6 @@
state.textDelegate,
constraints,
layoutDirection,
- maxLines,
state.layoutResult
).let { (width, height, result) ->
if (state.layoutResult != result) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
new file mode 100644
index 0000000..e37d940b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.layout.preferredHeightIn
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.platform.AmbientFontLoader
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.util.annotation.IntRange
+
+/**
+ * Constraint the height of the text field so that it vertically occupies no more than [maxLines]
+ * number of lines.
+ */
+fun Modifier.maxLinesHeight(
+ @IntRange(from = 1) maxLines: Int,
+ textStyle: TextStyle
+) = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "maxLinesHeight"
+ properties["maxLines"] = maxLines
+ properties["textStyle"] = textStyle
+ }
+) {
+ require(maxLines > 0) {
+ "maxLines must be greater than 0"
+ }
+ if (maxLines == Int.MAX_VALUE) return@composed Modifier
+
+ val density = AmbientDensity.current
+ val resourceLoader = AmbientFontLoader.current
+ val layoutDirection = AmbientLayoutDirection.current
+
+ // Difference between the height of two lines paragraph and one line paragraph gives us
+ // an approximation of height of one line
+ val firstLineHeight = remember(density, resourceLoader, textStyle, layoutDirection) {
+ computeSizeForDefaultText(
+ style = resolveDefaults(textStyle, layoutDirection),
+ density = density,
+ resourceLoader = resourceLoader,
+ text = EmptyTextReplacement,
+ maxLines = 1
+ ).height
+ }
+ val firstTwoLinesHeight = remember(density, resourceLoader, textStyle, layoutDirection) {
+ val twoLines = EmptyTextReplacement + "\n" + EmptyTextReplacement
+ computeSizeForDefaultText(
+ style = resolveDefaults(textStyle, layoutDirection),
+ density = density,
+ resourceLoader = resourceLoader,
+ text = twoLines,
+ maxLines = 2
+ ).height
+ }
+ val lineHeight = firstTwoLinesHeight - firstLineHeight
+ val precomputedMaxLinesHeight = firstLineHeight + lineHeight * (maxLines - 1)
+
+ Modifier.preferredHeightIn(
+ max = with(density) { precomputedMaxLinesHeight.toDp() }
+ )
+}
\ No newline at end of file
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 72c2437..895c040 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
@@ -54,7 +54,7 @@
// visible for testing
internal const val DefaultWidthCharCount = 10 // min width for TextField is 10 chars long
-private val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
+internal val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
/**
* Computed the default width and height for TextField.
@@ -67,16 +67,18 @@
*
* Until we have font metrics APIs, use the height of reference text as a workaround.
*/
-internal fun computeSizeForEmptyText(
+internal fun computeSizeForDefaultText(
style: TextStyle,
density: Density,
- resourceLoader: Font.ResourceLoader
+ resourceLoader: Font.ResourceLoader,
+ text: String = EmptyTextReplacement,
+ maxLines: Int = 1
): IntSize {
val paragraph = Paragraph(
- text = EmptyTextReplacement,
+ text = text,
style = style,
spanStyles = listOf(),
- maxLines = 1,
+ maxLines = maxLines,
ellipsis = false,
density = density,
resourceLoader = resourceLoader,
@@ -107,27 +109,10 @@
textDelegate: TextDelegate,
constraints: Constraints,
layoutDirection: LayoutDirection,
- maxLines: Int = Int.MAX_VALUE,
prevResultText: TextLayoutResult? = null
): Triple<Int, Int, TextLayoutResult> {
val layoutResult = textDelegate.layout(constraints, layoutDirection, prevResultText)
-
- val height = constrainWithMaxLines(maxLines, layoutResult.size.height, layoutResult)
- val width = layoutResult.size.width
-
- return Triple(width, height, layoutResult)
- }
-
- private fun constrainWithMaxLines(
- maxLines: Int,
- height: Int,
- layoutResult: TextLayoutResult
- ): Int {
- return if (maxLines == Int.MAX_VALUE || layoutResult.lineCount <= maxLines) {
- height
- } else {
- ceil(layoutResult.getLineBottom(maxLines - 1)).toInt()
- }
+ return Triple(layoutResult.size.width, layoutResult.size.height, layoutResult)
}
/**
@@ -194,7 +179,7 @@
offsetMap.originalToTransformed(value.selection.max) - 1
)
} else {
- val defaultSize = computeSizeForEmptyText(
+ val defaultSize = computeSizeForDefaultText(
textDelegate.style,
textDelegate.density,
textDelegate.resourceLoader
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 905cd82..d51b0511 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
@@ -38,6 +38,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
@@ -75,6 +76,9 @@
}
)
+ val cursorOffset = scrollerPosition.getOffsetToFollow(textFieldValue.selection)
+ scrollerPosition.previousSelection = textFieldValue.selection
+
val transformedText = visualTransformation.filter(
AnnotatedString(textFieldValue.text)
)
@@ -82,14 +86,14 @@
Orientation.Vertical ->
VerticalScrollLayoutModifier(
scrollerPosition,
- textFieldValue,
+ cursorOffset,
transformedText,
textLayoutResult
)
Orientation.Horizontal ->
HorizontalScrollLayoutModifier(
scrollerPosition,
- textFieldValue,
+ cursorOffset,
transformedText,
textLayoutResult
)
@@ -108,7 +112,7 @@
private data class VerticalScrollLayoutModifier(
val scrollerPosition: TextFieldScrollerPosition,
- val textFieldValue: TextFieldValue,
+ val cursorOffset: Int,
val transformedText: TransformedText,
val textLayoutResult: Ref<TextLayoutResult?>
) : LayoutModifier {
@@ -122,9 +126,9 @@
return layout(placeable.width, height) {
val cursorRect = getCursorRectInScroller(
- textFieldValue = textFieldValue,
+ cursorOffset = cursorOffset,
transformedText = transformedText,
- textLayoutResult = textLayoutResult,
+ textLayoutResult = textLayoutResult.value,
rtl = false,
textFieldWidth = placeable.width
)
@@ -144,7 +148,7 @@
private data class HorizontalScrollLayoutModifier(
val scrollerPosition: TextFieldScrollerPosition,
- val textFieldValue: TextFieldValue,
+ val cursorOffset: Int,
val transformedText: TransformedText,
val textLayoutResult: Ref<TextLayoutResult?>
) : LayoutModifier {
@@ -158,9 +162,9 @@
return layout(width, placeable.height) {
val cursorRect = getCursorRectInScroller(
- textFieldValue = textFieldValue,
+ cursorOffset = cursorOffset,
transformedText = transformedText,
- textLayoutResult = textLayoutResult,
+ textLayoutResult = textLayoutResult.value,
rtl = layoutDirection == LayoutDirection.Rtl,
textFieldWidth = placeable.width
)
@@ -179,14 +183,14 @@
}
private fun Density.getCursorRectInScroller(
- textFieldValue: TextFieldValue,
+ cursorOffset: Int,
transformedText: TransformedText,
- textLayoutResult: Ref<TextLayoutResult?>,
+ textLayoutResult: TextLayoutResult?,
rtl: Boolean,
textFieldWidth: Int
): Rect {
- val cursorRect = textLayoutResult.value?.getCursorRect(
- transformedText.offsetMap.originalToTransformed(textFieldValue.selection.min)
+ val cursorRect = textLayoutResult?.getCursorRect(
+ transformedText.offsetMap.originalToTransformed(cursorOffset)
) ?: Rect.Zero
val thickness = DefaultCursorThickness.toIntPx()
@@ -226,6 +230,12 @@
*/
private var previousCursorRect: Rect = Rect.Zero
+ /**
+ * Keeps the previous selection data in TextFieldValue in order to identify what has changed
+ * in the new selection, and decide which selection offset (start, end) to follow.
+ */
+ var previousSelection: TextRange = TextRange.Zero
+
fun update(
orientation: Orientation,
cursorRect: Rect,
@@ -257,6 +267,14 @@
}
}
+ fun getOffsetToFollow(selection: TextRange): Int {
+ return when {
+ selection.start != previousSelection.start -> selection.start
+ selection.end != previousSelection.end -> selection.end
+ else -> selection.min
+ }
+ }
+
companion object {
val Saver = Saver<TextFieldScrollerPosition, Float>(
save = { it.offset },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
index 4c51c92..74891fb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
@@ -84,7 +84,7 @@
}
private fun computeMinSize(): IntSize {
- return computeSizeForEmptyText(
+ return computeSizeForDefaultText(
style = resolveDefaults(style, layoutDirection),
density = density,
resourceLoader = resourceLoader
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 67e7bb8..89b5fd5 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
@@ -406,20 +406,13 @@
}
internal fun getHandlePosition(isStartHandle: Boolean): Offset {
- return if (isStartHandle)
- getSelectionHandleCoordinates(
- textLayoutResult = state?.layoutResult!!,
- offset = offsetMap.originalToTransformed(value.selection.start),
- isStart = true,
- areHandlesCrossed = value.selection.reversed
- )
- else
- getSelectionHandleCoordinates(
- textLayoutResult = state?.layoutResult!!,
- offset = offsetMap.originalToTransformed(value.selection.end),
- isStart = false,
- areHandlesCrossed = value.selection.reversed
- )
+ val offset = if (isStartHandle) value.selection.start else value.selection.end
+ return getSelectionHandleCoordinates(
+ textLayoutResult = state?.layoutResult!!,
+ offset = offsetMap.originalToTransformed(offset),
+ isStart = isStartHandle,
+ areHandlesCrossed = value.selection.reversed
+ )
}
/**
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
index e57a8bce..087f17a 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
@@ -451,7 +451,7 @@
val containerSize: Int,
val minHeight: Float
) {
- private val contentSize = adapter.maxScrollOffset(containerSize) + containerSize
+ private val contentSize get() = adapter.maxScrollOffset(containerSize) + containerSize
private val visiblePart get() = containerSize.toFloat() / contentSize
val size
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index efd7cab..d2ed8a5 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -26,6 +27,7 @@
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
@@ -51,6 +53,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
import org.jetbrains.skija.Surface
import org.junit.Assert.assertEquals
import org.junit.Ignore
@@ -223,6 +226,35 @@
}
}
+ @Test(timeout = 3000)
+ fun `dynamically change content then drag slider to the end`() {
+ runBlocking(Dispatchers.Main) {
+ val isContentVisible = mutableStateOf(false)
+ rule.setContent {
+ TestBox(
+ size = 100.dp,
+ scrollbarWidth = 10.dp
+ ) {
+ if (isContentVisible.value) {
+ repeat(10) {
+ Box(Modifier.size(20.dp).testTag("box$it"))
+ }
+ }
+ }
+ }
+ onFrame()
+
+ isContentVisible.value = true
+ onFrame()
+
+ rule.onNodeWithTag("scrollbar").performGesture {
+ swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
+ }
+ onFrame()
+ rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
+ }
+ }
+
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Test(timeout = 3000)
@@ -327,6 +359,8 @@
// TODO(demin): move to DesktopComposeTestRule?
private suspend fun onFrame() {
+ // TODO(demin): probably we don't need `yield` after we fix https://github.com/JetBrains/compose-jb/issues/137
+ yield()
(rule as DesktopComposeTestRule).owners?.onFrame(canvas, 100, 100, 0)
rule.awaitIdle()
}
@@ -366,6 +400,31 @@
}
}
+ @Composable
+ private fun TestBox(
+ size: Dp,
+ scrollbarWidth: Dp,
+ scrollableContent: @Composable ColumnScope.() -> Unit
+ ) = withTestEnvironment {
+ Box(Modifier.size(size)) {
+ val state = rememberScrollState()
+
+ ScrollableColumn(
+ Modifier.fillMaxSize().testTag("column"),
+ state,
+ content = scrollableContent
+ )
+
+ VerticalScrollbar(
+ adapter = rememberScrollbarAdapter(state),
+ modifier = Modifier
+ .width(scrollbarWidth)
+ .fillMaxHeight()
+ .testTag("scrollbar")
+ )
+ }
+ }
+
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Composable
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index 1114272..d3b5a40 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -28,19 +28,8 @@
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.withRunningRecomposer
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.autofill.Autofill
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
-import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.pointer.ConsumedData
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -50,33 +39,15 @@
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.InternalCoreApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.OwnedLayer
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientViewConfiguration
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
-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
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Duration
-import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Uptime
import androidx.compose.ui.unit.milliseconds
-import androidx.compose.ui.platform.WindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.runBlockingTest
@@ -137,7 +108,6 @@
pointerInputFilter = currentComposer
.materialize(Modifier.pointerInput(gestureDetector)) as
PointerInputFilter
- LayoutNode(0, 0, width, height, pointerInputFilter!! as Modifier)
}
}
yield()
@@ -348,24 +318,6 @@
}
}
- @Suppress("SameParameterValue")
- @OptIn(ExperimentalLayoutNodeApi::class)
- private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
- LayoutNode().apply {
- this.modifier = modifier
- measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("not supported") {
- override fun measure(
- measureScope: MeasureScope,
- measurables: List<Measurable>,
- constraints: Constraints
- ): MeasureResult =
- measureScope.layout(x2 - x, y2 - y) {}
- }
- attach(MockOwner())
- measure(Constraints.fixed(x2 - x, y2 - y))
- place(x, y)
- }
-
internal class TestFrameClock : MonotonicFrameClock {
private val frameCh = Channel<Long>()
@@ -379,130 +331,6 @@
onFrame(frameCh.receive())
}
- @OptIn(
- ExperimentalFocus::class,
- ExperimentalLayoutNodeApi::class,
- InternalCoreApi::class
- )
- private class MockOwner(
- val position: IntOffset = IntOffset.Zero,
- override val root: LayoutNode = LayoutNode()
- ) : Owner {
- val onRequestMeasureParams = mutableListOf<LayoutNode>()
- val onAttachParams = mutableListOf<LayoutNode>()
- val onDetachParams = mutableListOf<LayoutNode>()
-
- override val hapticFeedBack: HapticFeedback
- get() = TODO("Not yet implemented")
- override val clipboardManager: ClipboardManager
- get() = TODO("Not yet implemented")
- override val textToolbar: TextToolbar
- get() = TODO("Not yet implemented")
- override val autofillTree: AutofillTree
- get() = TODO("Not yet implemented")
- override val autofill: Autofill?
- 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
- get() = TODO("Not yet implemented")
- override val windowManager: WindowManager
- get() = TODO("Not yet implemented")
- override val fontLoader: Font.ResourceLoader
- get() = TODO("Not yet implemented")
- override val layoutDirection: LayoutDirection
- get() = LayoutDirection.Ltr
- override var showLayoutBounds: Boolean = false
- override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
-
- override fun onRequestMeasure(layoutNode: LayoutNode) {
- onRequestMeasureParams += layoutNode
- }
-
- override fun onRequestRelayout(layoutNode: LayoutNode) {
- }
-
- override val hasPendingMeasureOrLayout = false
-
- override fun onAttach(node: LayoutNode) {
- onAttachParams += node
- }
-
- override fun onDetach(node: LayoutNode) {
- onDetachParams += node
- }
-
- override fun calculatePosition(): IntOffset = position
-
- override fun requestFocus(): Boolean = false
-
- @ExperimentalKeyInput
- override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
- override fun measureAndLayout() {
- }
-
- override fun createLayer(
- drawBlock: (Canvas) -> Unit,
- invalidateParentLayer: () -> Unit
- ): OwnedLayer {
- return object : OwnedLayer {
- override val layerId: Long
- get() = 0
-
- override fun updateLayerProperties(
- scaleX: Float,
- scaleY: Float,
- alpha: Float,
- translationX: Float,
- translationY: Float,
- shadowElevation: Float,
- rotationX: Float,
- rotationY: Float,
- rotationZ: Float,
- cameraDistance: Float,
- transformOrigin: TransformOrigin,
- shape: Shape,
- clip: Boolean
- ) {
- }
-
- override fun move(position: IntOffset) {
- }
-
- override fun resize(size: IntSize) {
- }
-
- override fun drawLayer(canvas: Canvas) {
- drawBlock(canvas)
- }
-
- override fun updateDisplayList() {
- }
-
- override fun invalidate() {
- }
-
- override fun destroy() {
- }
-
- override fun getMatrix(matrix: Matrix) {
- }
- }
- }
-
- override fun onSemanticsChange() {
- }
-
- override val measureIteration: Long = 0
- override val viewConfiguration: ViewConfiguration
- get() = TestViewConfiguration()
- }
-
@OptIn(ExperimentalComposeApi::class)
class EmptyApplier : Applier<Unit> {
override val current: Unit = Unit
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 1723a18..a10c5d0 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
@@ -43,7 +43,6 @@
import androidx.compose.ui.text.input.buildTextFieldValue
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
@@ -75,8 +74,6 @@
private lateinit var multiParagraphIntrinsics: MultiParagraphIntrinsics
private lateinit var textLayoutResult: TextLayoutResult
- private val layoutDirection = LayoutDirection.Ltr
-
/**
* Test implementation of offset map which doubles the offset in transformed text.
*/
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
index acb51e1..4c14493 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
@@ -41,7 +41,6 @@
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
import org.junit.Rule
@@ -303,7 +302,7 @@
@Composable
fun RegularItem() {
- Box(Modifier.graphicsLayer().size(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
+ Box(Modifier.size(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
}
fun prepareForToggle() {
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 72d901a..54eda68 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
@@ -44,8 +44,8 @@
import androidx.compose.ui.focusRequester
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
/**
@@ -132,7 +132,7 @@
onNavigate: (Demo) -> Unit
) {
val primary = MaterialTheme.colors.primary
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
val title = demo.title
var currentIndex = 0
val pattern = filterText.toRegex(option = RegexOption.IGNORE_CASE)
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 4f210a1..9a6d909a 100644
--- a/compose/integration-tests/macrobenchmark/build.gradle
+++ b/compose/integration-tests/macrobenchmark/build.gradle
@@ -30,6 +30,7 @@
android.defaultConfig.minSdkVersion 28
dependencies {
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":benchmark:benchmark-macro"))
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index 8123740..a9c3920 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -19,9 +19,10 @@
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.MacrobenchmarkConfig
import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,41 +32,40 @@
* Macrobenchmark used for local validation of performance numbers coming from MacrobenchmarkRule.
*/
@LargeTest
+@SdkSuppress(minSdkVersion = 29)
@RunWith(Parameterized::class)
class ProcessSpeedProfileValidation(
private val compilationMode: CompilationMode,
- private val killProcess: Boolean
+ private val startupMode: StartupMode
) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
- @Ignore("Not running the test in CI")
- fun start() {
- val config = MacrobenchmarkConfig(
+ fun start() = benchmarkRule.measureStartupRepeated(
+ MacrobenchmarkConfig(
packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
- killProcessEachIteration = killProcess,
- iterations = 10
- )
- benchmarkRule.measureRepeated(config) {
- pressHome()
- launchPackageAndWait()
- }
+ iterations = 3
+ ),
+ startupMode
+ ) {
+ pressHome()
+ launchPackageAndWait()
}
companion object {
private const val PACKAGE_NAME = "androidx.compose.integration.demos"
- @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+ @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
@JvmStatic
fun kilProcessParameters(): List<Array<Any>> {
val compilationModes = listOf(
CompilationMode.None,
CompilationMode.SpeedProfile(warmupIterations = 3)
)
- val processKillOptions = listOf(true, false)
+ val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
return compilationModes.zip(processKillOptions).map {
arrayOf(it.first, it.second)
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
index ae2d39a..ace7a4c 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
@@ -17,30 +17,41 @@
package androidx.compose.integration.macrobenchmark
import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.benchmark.macro.StartupMode
import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@LargeTest
-@RunWith(AndroidJUnit4::class)
-class StartupDemosMacrobenchmark {
+@SdkSuppress(minSdkVersion = 29)
+@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
+class StartupDemosMacrobenchmark(
+ @Suppress("unused") private val ignored: Boolean
+) {
+
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
- @Ignore("Not running the test in CI")
@Test
fun compiledColdStartup() = benchmarkRule.measureStartup(
profileCompiled = true,
- coldLaunch = true
+ startupMode = StartupMode.COLD
)
- @Ignore("Not running the test in CI")
@Test
fun uncompiledColdStartup() = benchmarkRule.measureStartup(
profileCompiled = false,
- coldLaunch = true
+ startupMode = StartupMode.COLD
)
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun startupDemosParameters(): List<Array<Any>> {
+ return listOf(arrayOf(false))
+ }
+ }
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
index 2a9b90d94..fe30427 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
@@ -16,36 +16,40 @@
package androidx.compose.integration.macrobenchmark
+import android.content.Intent
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.MacrobenchmarkConfig
import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
+const val TargetPackage = "androidx.compose.integration.demos"
+
/**
* Simplified interface for standardizing e.g. package,
* compilation types, and iteration count across project
*/
fun MacrobenchmarkRule.measureStartup(
profileCompiled: Boolean,
- coldLaunch: Boolean,
- setupBlock: MacrobenchmarkScope.() -> Unit = {},
- measureBlock: MacrobenchmarkScope.() -> Unit = {
- pressHome()
- launchPackageAndWait()
- }
-) = measureRepeated(
+ startupMode: StartupMode,
+ iterations: Int = 5,
+ setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
MacrobenchmarkConfig(
- packageName = "androidx.compose.integration.demos",
+ packageName = TargetPackage,
metrics = listOf(StartupTimingMetric()),
compilationMode = if (profileCompiled) {
CompilationMode.SpeedProfile(warmupIterations = 3)
} else {
CompilationMode.None
},
- killProcessEachIteration = coldLaunch,
- iterations = 10
+ iterations = iterations
),
- setupBlock,
- measureBlock
-)
+ startupMode = startupMode
+) {
+ pressHome()
+ val intent = Intent()
+ intent.setPackage(TargetPackage)
+ setupIntent(intent)
+ launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/compose/material/material/OWNERS b/compose/material/OWNERS
similarity index 100%
rename from compose/material/material/OWNERS
rename to compose/material/OWNERS
diff --git a/compose/material/material-ripple/api/current.txt b/compose/material/material-ripple/api/current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+ @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+ method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+ }
+
+ public final class RippleAnimationKt {
+ }
+
+ public final class RippleKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+ method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+ field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+ }
+
+ public static final class RippleTheme.Companion {
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+ }
+
+ public final class RippleThemeKt {
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+ }
+
+}
+
diff --git a/compose/material/material-ripple/api/public_plus_experimental_current.txt b/compose/material/material-ripple/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/public_plus_experimental_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+ @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+ method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+ }
+
+ public final class RippleAnimationKt {
+ }
+
+ public final class RippleKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+ method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+ field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+ }
+
+ public static final class RippleTheme.Companion {
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+ }
+
+ public final class RippleThemeKt {
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+ }
+
+}
+
diff --git a/compose/material/material-ripple/api/res-current.txt b/compose/material/material-ripple/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material/material-ripple/api/res-current.txt
diff --git a/compose/material/material-ripple/api/restricted_current.txt b/compose/material/material-ripple/api/restricted_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/restricted_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+ @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+ method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+ }
+
+ public final class RippleAnimationKt {
+ }
+
+ public final class RippleKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+ }
+
+ @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+ method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+ field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+ }
+
+ public static final class RippleTheme.Companion {
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+ method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+ }
+
+ public final class RippleThemeKt {
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+ }
+
+}
+
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
new file mode 100644
index 0000000..296fd0d
--- /dev/null
+++ b/compose/material/material-ripple/build.gradle
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+import androidx.build.AndroidXUiPlugin
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+}
+
+AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ if(!AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block below
+ */
+ api project(":compose:foundation:foundation")
+ api project(":compose:runtime:runtime")
+
+ implementation(KOTLIN_STDLIB_COMMON)
+ implementation project(":compose:animation:animation")
+ implementation project(":compose:ui:ui-util")
+
+ testImplementation(ANDROIDX_TEST_RULES)
+ testImplementation(ANDROIDX_TEST_RUNNER)
+ testImplementation(JUNIT)
+ testImplementation(TRUTH)
+ }
+}
+
+if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+ kotlin {
+ android()
+ jvm("desktop")
+
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ implementation(KOTLIN_STDLIB_COMMON)
+ api project(":compose:foundation:foundation")
+ api project(":compose:runtime:runtime")
+
+ implementation project(":compose:animation:animation")
+ implementation project(":compose:ui:ui-util")
+ }
+
+ desktopMain.dependencies {
+ implementation(KOTLIN_STDLIB)
+ }
+
+ test.dependencies {
+ implementation(ANDROIDX_TEST_RULES)
+ implementation(ANDROIDX_TEST_RUNNER)
+ implementation(JUNIT)
+ implementation(TRUTH)
+ }
+ }
+ }
+}
+
+androidx {
+ name = "Compose Material Ripple"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.Compose.MATERIAL
+ inceptionYear = "2020"
+ description = "Material ripple used to build interactive components"
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ useIR = true
+ }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += ["-Xallow-jvm-ir-dependencies"]
+ }
+}
diff --git a/compose/material/material-ripple/src/androidMain/AndroidManifest.xml b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..0c2bb3c
--- /dev/null
+++ b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest package="androidx.compose.material.ripple" />
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
index 8a6dfd3..a4942eb 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
@@ -13,17 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.compose.ui.test
-import org.jetbrains.skiko.Library
+package androidx.compose.material.ripple
-fun initCompose() {
- ComposeInit
-}
-
-private object ComposeInit {
- init {
- Library.load("/", "skiko")
- System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
- }
-}
\ No newline at end of file
+@RequiresOptIn(
+ "This ripple API is experimental and is likely to change or to be removed in" +
+ " the future."
+)
+public annotation class ExperimentalRippleApi
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
similarity index 64%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index f2a65ca..59d79ee 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -26,8 +26,6 @@
import androidx.compose.foundation.IndicationInstance
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
@@ -35,7 +33,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
@@ -48,120 +46,134 @@
import androidx.compose.ui.util.nativeClass
/**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
*
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
*
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
+ *
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
*
* @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
* ripples always animate from the target layout center, bounded ripples animate from the touch
* position.
- * @param radius Effects grow up to this size. If null is provided the size would be calculated
+ * @param radius the radius for the ripple. If `null` is provided then the size will be calculated
* based on the target layout size.
- * @param color The Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @param color the color of the ripple. This color is usually the same color used by the text or
+ * iconography in the component. This color will then have [RippleTheme.rippleAlpha] applied to
+ * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color
+ * used will be [RippleTheme.defaultColor] instead.
*/
-@Suppress("ComposableNaming")
@Deprecated(
- "Replaced with rememberRippleIndication",
+ "Replaced with rememberRipple",
ReplaceWith(
- "rememberRippleIndication(bounded, radius, color)",
- "androidx.compose.material.ripple.rememberRippleIndication"
+ "rememberRipple(bounded, radius, color)",
+ "androidx.compose.material.ripple.rememberRipple"
)
)
@Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun RippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRippleIndication(
bounded: Boolean = true,
radius: Dp? = null,
color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
val theme = AmbientRippleTheme.current
val clock = AmbientAnimationClock.current.asDisposableClock()
val resolvedColor = color.useOrElse { theme.defaultColor() }
val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
colorState.value = resolvedColor
- val interactionOpacity = theme.rippleOpacity()
+ val rippleAlpha = theme.rippleAlpha()
return remember(bounded, radius, theme, clock) {
- RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+ Ripple(bounded, radius ?: Dp.Unspecified, colorState, rippleAlpha, clock)
}
}
/**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
*
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
*
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
+ *
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
*
* @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
* ripples always animate from the target layout center, bounded ripples animate from the touch
* position.
- * @param radius Effects grow up to this size. If null is provided the size would be calculated
- * based on the target layout size.
- * @param color The Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be
+ * calculated based on the target layout size.
+ * @param color the color of the ripple. This color is usually the same color used by the text or
+ * iconography in the component. This color will then have [RippleTheme.rippleAlpha] applied to
+ * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color
+ * used will be [RippleTheme.defaultColor] instead.
*/
@Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun rememberRippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRipple(
bounded: Boolean = true,
- radius: Dp? = null,
+ radius: Dp = Dp.Unspecified,
color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
val theme = AmbientRippleTheme.current
val clock = AmbientAnimationClock.current.asDisposableClock()
val resolvedColor = color.useOrElse { theme.defaultColor() }
val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
colorState.value = resolvedColor
- val interactionOpacity = theme.rippleOpacity()
+ val rippleAlpha = theme.rippleAlpha()
return remember(bounded, radius, theme, clock) {
- RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+ Ripple(bounded, radius, colorState, rippleAlpha, clock)
}
}
/**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
*
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
*
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
*/
@Stable
-@OptIn(ExperimentalMaterialApi::class)
-class RippleIndication internal constructor(
+@ExperimentalRippleApi
+private class Ripple(
private val bounded: Boolean,
- private val radius: Dp? = null,
- private var color: State<Color>,
- private val rippleOpacity: RippleOpacity,
+ private val radius: Dp,
+ private val color: State<Color>,
+ private val rippleAlpha: RippleAlpha,
private val clock: AnimationClockObservable
) : Indication {
override fun createInstance(): IndicationInstance {
- return RippleIndicationInstance(bounded, radius, color, rippleOpacity, clock)
+ return RippleIndicationInstance(bounded, radius, color, rippleAlpha, clock)
}
// to force stability on this indication we need equals and hashcode, there's no value in
@@ -170,12 +182,12 @@
if (this === other) return true
if (this.nativeClass() != other?.nativeClass()) return false
- other as RippleIndication
+ other as Ripple
if (bounded != other.bounded) return false
if (radius != other.radius) return false
if (color != other.color) return false
- if (rippleOpacity != other.rippleOpacity) return false
+ if (rippleAlpha != other.rippleAlpha) return false
if (clock != other.clock) return false
return true
@@ -183,24 +195,24 @@
override fun hashCode(): Int {
var result = bounded.hashCode()
- result = 31 * result + (radius?.hashCode() ?: 0)
+ result = 31 * result + radius.hashCode()
result = 31 * result + color.hashCode()
- result = 31 * result + rippleOpacity.hashCode()
+ result = 31 * result + rippleAlpha.hashCode()
result = 31 * result + clock.hashCode()
return result
}
}
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
private class RippleIndicationInstance constructor(
private val bounded: Boolean,
- private val radius: Dp? = null,
- private var color: State<Color>,
- private val rippleOpacity: RippleOpacity,
+ private val radius: Dp,
+ private val color: State<Color>,
+ private val rippleAlpha: RippleAlpha,
private val clock: AnimationClockObservable
) : IndicationInstance {
- private val stateLayer = StateLayer(clock, bounded, rippleOpacity)
+ private val stateLayer = StateLayer(clock, bounded, rippleAlpha)
private val ripples = mutableStateListOf<RippleAnimation>()
private var currentPressPosition: Offset? = null
@@ -208,8 +220,12 @@
override fun ContentDrawScope.drawIndication(interactionState: InteractionState) {
val color = color.value
- val targetRadius =
- radius?.toPx() ?: getRippleEndRadius(bounded, size)
+ // TODO: b/174310811 use library function instead of manual logic here
+ val targetRadius = if (radius == Dp.Unspecified) {
+ getRippleEndRadius(bounded, size)
+ } else {
+ radius.toPx()
+ }
drawContent()
with(stateLayer) {
drawStateLayer(interactionState, targetRadius, color)
@@ -250,7 +266,7 @@
private fun DrawScope.drawRipples(color: Color) {
ripples.fastForEach {
with(it) {
- val alpha = rippleOpacity.opacityForInteraction(Interaction.Pressed)
+ val alpha = rippleAlpha.alphaForInteraction(Interaction.Pressed)
if (alpha != 0f) {
draw(color.copy(alpha = alpha))
}
@@ -287,13 +303,13 @@
* @see IncomingStateLayerAnimationSpecs
* @see OutgoingStateLayerAnimationSpecs
*/
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
private class StateLayer(
clock: AnimationClockObservable,
private val bounded: Boolean,
- private val rippleOpacity: RippleOpacity
+ private val rippleAlpha: RippleAlpha
) {
- private val animatedOpacity = AnimatedFloatModel(0f, clock)
+ private val animatedAlpha = AnimatedFloatModel(0f, clock)
private var previousInteractions: Set<Interaction> = emptySet()
private var lastDrawnInteraction: Interaction? = null
@@ -318,15 +334,15 @@
if (interaction in previousInteractions) continue
// Move to the next interaction if this is not an interaction we show a state layer for
- val targetOpacity = rippleOpacity.opacityForInteraction(interaction)
- if (targetOpacity == 0f) continue
+ val targetAlpha = rippleAlpha.alphaForInteraction(interaction)
+ if (targetAlpha == 0f) continue
// TODO: consider defaults - these will be used for a custom Interaction that we are
// not aware of, but has an alpha that should be shown because of a custom RippleTheme.
val incomingAnimationSpec = IncomingStateLayerAnimationSpecs[interaction]
?: TweenSpec(durationMillis = 15, easing = LinearEasing)
- animatedOpacity.animateTo(targetOpacity, incomingAnimationSpec)
+ animatedAlpha.animateTo(targetAlpha, incomingAnimationSpec)
lastDrawnInteraction = interaction
handled = true
@@ -342,7 +358,7 @@
val outgoingAnimationSpec = OutgoingStateLayerAnimationSpecs[previousInteraction]
?: TweenSpec(durationMillis = 15, easing = LinearEasing)
- animatedOpacity.animateTo(0f, outgoingAnimationSpec)
+ animatedAlpha.animateTo(0f, outgoingAnimationSpec)
lastDrawnInteraction = null
}
@@ -350,10 +366,10 @@
previousInteractions = currentInteractions
- val opacity = animatedOpacity.value
+ val alpha = animatedAlpha.value
- if (opacity > 0f) {
- val modulatedColor = color.copy(alpha = opacity)
+ if (alpha > 0f) {
+ val modulatedColor = color.copy(alpha = alpha)
if (bounded) {
clipRect {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
similarity index 93%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
index d1f06c2..18c5b76 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
@@ -41,19 +41,18 @@
import kotlin.math.max
/**
- * [RippleAnimation]s are drawn as part of [RippleIndication] as a visual indicator for an
- * [androidx.compose.foundation.Interaction.Pressed] state.
+ * [RippleAnimation]s are drawn as part of [Ripple] as a visual indicator for an
+ * different [androidx.compose.foundation.Interaction]s.
*
* Use [androidx.compose.foundation.clickable] or [androidx.compose.foundation.indication] to add a
- * [RippleIndication] to your component, which contains a RippleAnimation for pressed states, and
+ * ripple to your component, which contains a RippleAnimation for pressed states, and
* a state layer for other states.
*
* This is a default implementation based on the Material Design specification.
*
- * A circular ripple effect whose origin starts at the input touch point and
- * whose radius expands from 60% of the final value. The ripple origin
- * animates to the center of its target layout for the bounded version
- * and stays in the center for the unbounded one.
+ * Draws a circular ripple effect with an origin starting at the input touch point and with a
+ * radius expanding from 60% of the final value. The ripple origin animates to the center of its
+ * target layout for the bounded version and stays in the center for the unbounded one.
*
* @param size The size of the target layout.
* @param startPosition The position the animation will start from.
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
new file mode 100644
index 0000000..d637ef9
--- /dev/null
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.ripple
+
+import androidx.compose.foundation.Interaction
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ProvidableAmbient
+import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.luminance
+
+/**
+ * Defines the appearance for Ripples. You can define a new theme and apply it using
+ * [AmbientRippleTheme]. See [defaultRippleColor] and [defaultRippleAlpha] for default values
+ * that can be used when creating your own [RippleTheme].
+ *
+ * @see rememberRipple
+ */
+@ExperimentalRippleApi
+public interface RippleTheme {
+ /**
+ * @return the default ripple color at the call site's position in the hierarchy.
+ * This color will be used when a color is not explicitly set in the ripple itself.
+ * @see defaultRippleColor
+ */
+ @Composable
+ public fun defaultColor(): Color
+
+ /**
+ * @return the [RippleAlpha] used to calculate the alpha for the ripple depending on the
+ * [Interaction] for a given component. This will be set as the alpha channel for
+ * [defaultColor] or the color explicitly provided to the ripple.
+ * @see defaultRippleAlpha
+ */
+ @Composable
+ public fun rippleAlpha(): RippleAlpha
+
+ public companion object {
+ /**
+ * Represents the default color that will be used for a ripple if a color has not been
+ * explicitly set on the ripple instance.
+ *
+ * @param contentColor the color of content (text or iconography) in the component that
+ * contains the ripple.
+ * @param lightTheme whether the theme is light or not
+ */
+ @ExperimentalRippleApi
+ public fun defaultRippleColor(
+ contentColor: Color,
+ lightTheme: Boolean
+ ): Color {
+ val contentLuminance = contentColor.luminance()
+ // If we are on a colored surface (typically indicated by low luminance content), the
+ // ripple color should be white.
+ return if (!lightTheme && contentLuminance < 0.5) {
+ Color.White
+ // Otherwise use contentColor
+ } else {
+ contentColor
+ }
+ }
+
+ /**
+ * Represents the default [RippleAlpha] that will be used for a ripple to indicate different
+ * states.
+ *
+ * @param contentColor the color of content (text or iconography) in the component that
+ * contains the ripple.
+ * @param lightTheme whether the theme is light or not
+ */
+ @ExperimentalRippleApi
+ public fun defaultRippleAlpha(contentColor: Color, lightTheme: Boolean): RippleAlpha {
+ return when {
+ lightTheme -> {
+ if (contentColor.luminance() > 0.5) {
+ LightThemeHighContrastRippleAlpha
+ } else {
+ LightThemeLowContrastRippleAlpha
+ }
+ }
+ else -> {
+ DarkThemeRippleAlpha
+ }
+ }
+ }
+ }
+}
+
+/**
+ * RippleAlpha defines the alpha of the ripple / state layer for a given [Interaction].
+ */
+@ExperimentalRippleApi
+public fun interface RippleAlpha {
+ /**
+ * @return the alpha of the ripple for the given [interaction]. Return `0f` if this
+ * particular interaction should not show a corresponding ripple / state layer.
+ */
+ public fun alphaForInteraction(interaction: Interaction): Float
+}
+
+/**
+ * Ambient used for providing [RippleTheme] down the tree.
+ *
+ * See [RippleTheme.defaultRippleColor] and [RippleTheme.defaultRippleAlpha] functions for the
+ * default implementations for color and alpha.
+ */
+@ExperimentalRippleApi
+public val AmbientRippleTheme: ProvidableAmbient<RippleTheme> = staticAmbientOf { DebugRippleTheme }
+
+@Suppress("unused")
+@OptIn(ExperimentalRippleApi::class)
+private sealed class DefaultRippleAlpha(
+ val pressed: Float,
+ val focused: Float,
+ val dragged: Float,
+ val hovered: Float
+) : RippleAlpha {
+ override fun alphaForInteraction(interaction: Interaction): Float = when (interaction) {
+ Interaction.Pressed -> pressed
+ Interaction.Dragged -> dragged
+ else -> 0f
+ }
+}
+
+/**
+ * Alpha values for high luminance content in a light theme.
+ *
+ * This content will typically be placed on colored surfaces, so it is important that the
+ * contrast here is higher to meet accessibility standards, and increase legibility.
+ *
+ * These levels are typically used for text / iconography in primary colored tabs /
+ * bottom navigation / etc.
+ */
+private object LightThemeHighContrastRippleAlpha : DefaultRippleAlpha(
+ pressed = 0.24f,
+ focused = 0.24f,
+ dragged = 0.16f,
+ hovered = 0.08f
+)
+
+/**
+ * Alpha levels for low luminance content in a light theme.
+ *
+ * This content will typically be placed on grayscale surfaces, so the contrast here can be lower
+ * without sacrificing accessibility and legibility.
+ *
+ * These levels are typically used for body text on the main surface (white in light theme, grey
+ * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
+ */
+private object LightThemeLowContrastRippleAlpha : DefaultRippleAlpha(
+ pressed = 0.12f,
+ focused = 0.12f,
+ dragged = 0.08f,
+ hovered = 0.04f
+)
+
+/**
+ * Alpha levels for all content in a dark theme.
+ */
+private object DarkThemeRippleAlpha : DefaultRippleAlpha(
+ pressed = 0.10f,
+ focused = 0.12f,
+ dragged = 0.08f,
+ hovered = 0.04f
+)
+
+/**
+ * Simple debug indication that will assume black content color and light theme. You should
+ * instead provide your own theme with meaningful values - this exists as an alternative to
+ * crashing if no theme is provided.
+ */
+@ExperimentalRippleApi
+@Immutable
+private object DebugRippleTheme : RippleTheme {
+ @Composable
+ override fun defaultColor() = RippleTheme.defaultRippleColor(Color.Black, lightTheme = true)
+
+ @Composable
+ override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha(
+ Color.Black,
+ lightTheme = true
+ )
+}
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt b/compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
rename to compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -765,32 +765,3 @@
}
-package androidx.compose.material.ripple {
-
- public final class RippleAnimationKt {
- }
-
- @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
- method public androidx.compose.foundation.IndicationInstance createInstance();
- }
-
- public final class RippleIndicationKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
- method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
- method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
- method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
- }
-
- public final class RippleThemeKt {
- method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
- }
-
-}
-
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -765,32 +765,3 @@
}
-package androidx.compose.material.ripple {
-
- public final class RippleAnimationKt {
- }
-
- @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
- method public androidx.compose.foundation.IndicationInstance createInstance();
- }
-
- public final class RippleIndicationKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
- method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
- method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
- method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
- }
-
- public final class RippleThemeKt {
- method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
- }
-
-}
-
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -765,32 +765,3 @@
}
-package androidx.compose.material.ripple {
-
- public final class RippleAnimationKt {
- }
-
- @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
- method public androidx.compose.foundation.IndicationInstance createInstance();
- }
-
- public final class RippleIndicationKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
- method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
- }
-
- @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
- method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
- method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
- }
-
- public final class RippleThemeKt {
- method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
- }
-
-}
-
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 2053ed0..96b2108 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -17,7 +17,6 @@
import androidx.build.AndroidXUiPlugin
import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -42,6 +41,7 @@
api project(":compose:animation:animation-core")
api project(":compose:foundation:foundation")
api project(":compose:material:material-icons-core")
+ api project(":compose:material:material-ripple")
api project(":compose:runtime:runtime")
api project(":compose:ui:ui")
api project(":compose:ui:ui-text")
@@ -86,6 +86,7 @@
api project(":compose:animation:animation-core")
api project(":compose:foundation:foundation")
api project(":compose:material:material-icons-core")
+ api project(":compose:material:material-ripple")
api project(":compose:runtime:runtime")
api project(":compose:ui:ui")
api project(":compose:ui:ui-text")
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 35c4da7..077d3a1 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
@@ -32,7 +32,7 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -76,7 +76,7 @@
.selectable(
selected = selected,
onClick = onSelected,
- indication = rememberRippleIndication(bounded = false)
+ indication = rememberRipple(bounded = false)
)
) {
Icon(icon, tint = tabTintColor)
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 4cc00ec..05b8283 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
@@ -26,6 +26,7 @@
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -58,7 +59,7 @@
TopAppBar(title = { Text("Title") })
}
.assertHeightIsEqualTo(appBarHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -169,7 +170,7 @@
BottomAppBar {}
}
.assertHeightIsEqualTo(appBarHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
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 d18dde2..c219aec 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
@@ -32,6 +32,7 @@
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.isSelectable
import androidx.compose.ui.test.junit4.createComposeRule
@@ -63,7 +64,7 @@
rule.setMaterialContentForSizeAssertions {
BottomNavigationSample()
}
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
.assertHeightIsEqualTo(height)
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
index a938532..1d4aa77 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material
import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,7 +42,7 @@
Divider()
}
.assertHeightIsEqualTo(defaultHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -51,7 +52,7 @@
.setMaterialContentForSizeAssertions {
Divider(thickness = height)
}
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
.assertHeightIsEqualTo(height)
}
@@ -65,6 +66,6 @@
Divider(startIndent = indent, thickness = height)
}
.assertHeightIsEqualTo(height)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index b8766c6..c3c6fcd 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -27,6 +27,9 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
@@ -35,7 +38,9 @@
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
@@ -412,4 +417,82 @@
assertThat(drawerState.value).isEqualTo(BottomDrawerValue.Closed)
}
}
+
+ @Test
+ @LargeTest
+ fun modalDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen() {
+ lateinit var drawerState: DrawerState
+ rule.setMaterialContent {
+ drawerState = rememberDrawerState(DrawerValue.Closed)
+ ModalDrawerLayout(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag("drawer"))
+ },
+ bodyContent = emptyContent()
+ )
+ }
+
+ // Drawer should start in closed state and have no dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+
+ // When the drawer state is set to Opened
+ rule.runOnIdle {
+ drawerState.open()
+ }
+ // Then the drawer should be opened and have dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+
+ // When the drawer state is set to Closed using dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .performSemanticsAction(SemanticsActions.Dismiss)
+ // Then the drawer should be closed and have no dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+ }
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen() {
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawerLayout(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag("drawer"))
+ },
+ bodyContent = emptyContent()
+ )
+ }
+
+ // Drawer should start in closed state and have no dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+
+ // When the drawer state is set to Opened
+ rule.runOnIdle {
+ drawerState.open()
+ }
+ // Then the drawer should be opened and have dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+
+ // When the drawer state is set to Closed using dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .performSemanticsAction(SemanticsActions.Dismiss)
+ // Then the drawer should be closed and have no dismiss action
+ rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+ }
}
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 fe7511d..809ca2f 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
@@ -26,6 +26,7 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.node.Ref
import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
@@ -57,7 +58,7 @@
ListItem(text = { Text("Primary text") })
}
.assertHeightIsEqualTo(expectedHeightNoIcon)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -71,7 +72,7 @@
)
}
.assertHeightIsEqualTo(expectedHeightSmallIcon)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -85,7 +86,7 @@
)
}
.assertHeightIsEqualTo(expectedHeightLargeIcon)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -99,7 +100,7 @@
)
}
.assertHeightIsEqualTo(expectedHeightNoIcon)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -115,7 +116,7 @@
)
}
.assertHeightIsEqualTo(expectedHeightWithIcon)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -130,7 +131,7 @@
)
}
.assertHeightIsEqualTo(expectedHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -145,7 +146,7 @@
)
}
.assertHeightIsEqualTo(expectedHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -161,7 +162,7 @@
)
}
.assertHeightIsEqualTo(expectedHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
@@ -177,7 +178,7 @@
)
}
.assertHeightIsEqualTo(expectedHeight)
- .assertWidthFillsRoot()
+ .assertWidthIsEqualTo(rule.rootWidth())
}
@Test
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
similarity index 86%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
rename to compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index 6e5dc86..2bd7a2d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.material.ripple
+package androidx.compose.material
import android.os.Build
+import androidx.compose.foundation.Indication
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.indication
@@ -26,12 +27,11 @@
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.GOLDEN_MATERIAL
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.getValue
@@ -61,11 +61,15 @@
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * Test for the [RippleTheme] provided by [MaterialTheme], to verify colors and opacity in
+ * different configurations.
+ */
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
-class RippleIndicationTest {
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class, ExperimentalRippleApi::class)
+class MaterialRippleThemeTest {
@get:Rule
val rule = createComposeRule()
@@ -89,7 +93,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_bounded_light_highluminance_pressed",
+ "ripple_bounded_light_highluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
)
}
@@ -110,7 +114,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_bounded_light_highluminance_dragged",
+ "ripple_bounded_light_highluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
)
}
@@ -131,7 +135,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_bounded_light_lowluminance_pressed",
+ "ripple_bounded_light_lowluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
)
}
@@ -152,7 +156,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_bounded_light_lowluminance_dragged",
+ "ripple_bounded_light_lowluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
)
}
@@ -173,7 +177,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_bounded_dark_highluminance_pressed",
+ "ripple_bounded_dark_highluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
)
}
@@ -194,7 +198,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_bounded_dark_highluminance_dragged",
+ "ripple_bounded_dark_highluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
)
}
@@ -215,7 +219,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_bounded_dark_lowluminance_pressed",
+ "ripple_bounded_dark_lowluminance_pressed",
// Low luminance content in dark theme should use a white ripple by default
calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
)
@@ -237,7 +241,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_bounded_dark_lowluminance_dragged",
+ "ripple_bounded_dark_lowluminance_dragged",
// Low luminance content in dark theme should use a white ripple by default
calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
)
@@ -259,7 +263,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_unbounded_light_highluminance_pressed",
+ "ripple_unbounded_light_highluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
)
}
@@ -280,7 +284,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_unbounded_light_highluminance_dragged",
+ "ripple_unbounded_light_highluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
)
}
@@ -301,7 +305,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_unbounded_light_lowluminance_pressed",
+ "ripple_unbounded_light_lowluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
)
}
@@ -322,7 +326,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_unbounded_light_lowluminance_dragged",
+ "ripple_unbounded_light_lowluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
)
}
@@ -343,7 +347,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_unbounded_dark_highluminance_pressed",
+ "ripple_unbounded_dark_highluminance_pressed",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
)
}
@@ -364,7 +368,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_unbounded_dark_highluminance_dragged",
+ "ripple_unbounded_dark_highluminance_dragged",
calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
)
}
@@ -385,7 +389,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_unbounded_dark_lowluminance_pressed",
+ "ripple_unbounded_dark_lowluminance_pressed",
// Low luminance content in dark theme should use a white ripple by default
calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
)
@@ -407,7 +411,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_unbounded_dark_lowluminance_dragged",
+ "ripple_unbounded_dark_lowluminance_dragged",
// Low luminance content in dark theme should use a white ripple by default
calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
)
@@ -427,17 +431,15 @@
override fun defaultColor() = rippleColor
@Composable
- override fun rippleOpacity() = object : RippleOpacity {
- override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
- }
+ override fun rippleAlpha() = RippleAlpha { rippleAlpha }
}
rule.setContent {
- Providers(AmbientRippleTheme provides rippleTheme) {
- MaterialTheme {
+ MaterialTheme {
+ Providers(AmbientRippleTheme provides rippleTheme) {
Surface(contentColor = contentColor) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- RippleBox(interactionState, rememberRippleIndication())
+ RippleBox(interactionState, rememberRipple())
}
}
}
@@ -449,7 +451,7 @@
assertRippleMatches(
interactionState,
Interaction.Pressed,
- "rippleindication_customtheme_pressed",
+ "ripple_customtheme_pressed",
expectedColor
)
}
@@ -468,17 +470,15 @@
override fun defaultColor() = rippleColor
@Composable
- override fun rippleOpacity() = object : RippleOpacity {
- override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
- }
+ override fun rippleAlpha() = RippleAlpha { rippleAlpha }
}
rule.setContent {
- Providers(AmbientRippleTheme provides rippleTheme) {
- MaterialTheme {
+ MaterialTheme {
+ Providers(AmbientRippleTheme provides rippleTheme) {
Surface(contentColor = contentColor) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- RippleBox(interactionState, rememberRippleIndication())
+ RippleBox(interactionState, rememberRipple())
}
}
}
@@ -490,7 +490,7 @@
assertRippleMatches(
interactionState,
Interaction.Dragged,
- "rippleindication_customtheme_dragged",
+ "ripple_customtheme_dragged",
expectedColor
)
}
@@ -504,9 +504,7 @@
override fun defaultColor() = color
@Composable
- override fun rippleOpacity() = object : RippleOpacity {
- override fun opacityForInteraction(interaction: Interaction) = alpha
- }
+ override fun rippleAlpha() = RippleAlpha { alpha }
}
val initialColor = Color.Red
@@ -515,11 +513,11 @@
var rippleTheme by mutableStateOf(createRippleTheme(initialColor, initialAlpha))
rule.setContent {
- Providers(AmbientRippleTheme provides rippleTheme) {
- MaterialTheme {
+ MaterialTheme {
+ Providers(AmbientRippleTheme provides rippleTheme) {
Surface(contentColor = Color.Black) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- RippleBox(interactionState, rememberRippleIndication())
+ RippleBox(interactionState, rememberRipple())
}
}
}
@@ -616,14 +614,14 @@
}
/**
- * Generic Button like component that allows injecting a [RippleIndication] and also includes
+ * Generic Button like component that allows injecting an [Indication] and also includes
* padding around the rippled surface, so screenshots will include some dead space for clarity.
*
* @param interactionState the [InteractionState] that is used to drive the ripple state
- * @param rippleIndication [RippleIndication] placed inside the surface
+ * @param ripple ripple [Indication] placed inside the surface
*/
@Composable
-private fun RippleBox(interactionState: InteractionState, rippleIndication: RippleIndication) {
+private fun RippleBox(interactionState: InteractionState, ripple: Indication) {
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
Surface(
Modifier.padding(25.dp),
@@ -632,7 +630,7 @@
Box(
Modifier.preferredWidth(80.dp).preferredHeight(50.dp).indication(
interactionState = interactionState,
- indication = rippleIndication
+ indication = ripple
)
)
}
@@ -659,7 +657,7 @@
MaterialTheme(colors) {
Surface(contentColor = contentColor) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- RippleBox(interactionState, rememberRippleIndication(bounded))
+ RippleBox(interactionState, rememberRipple(bounded))
}
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
index fcd18b4..d55c5dc 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
@@ -22,20 +22,20 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.LastBaseline
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.platform.AndroidOwner
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.getAlignmentLinePosition
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
fun ComposeTestRule.setMaterialContent(
modifier: Modifier = Modifier,
@@ -61,39 +61,9 @@
fun SemanticsNodeInteraction.assertIsSquareWithSize(expectedSize: Dp) =
assertWidthIsEqualTo(expectedSize).assertHeightIsEqualTo(expectedSize)
-fun SemanticsNodeInteraction.assertWidthFillsRoot(): SemanticsNodeInteraction {
- val node = fetchSemanticsNode("Failed to assertWidthFillsScreen")
- @OptIn(ExperimentalLayoutNodeApi::class)
- val owner = node.componentNode.owner as AndroidOwner
- val rootViewWidth = owner.view.width
+fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
- with(owner.density) {
- node.boundsInRoot.width.toDp().assertIsEqualTo(rootViewWidth.toDp())
- }
- return this
-}
-
-fun ComposeTestRule.rootWidth(): Dp {
- val nodeInteraction = onRoot()
- val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
- @OptIn(ExperimentalLayoutNodeApi::class)
- val owner = node.componentNode.owner as AndroidOwner
-
- return with(owner.density) {
- owner.view.width.toDp()
- }
-}
-
-fun ComposeTestRule.rootHeight(): Dp {
- val nodeInteraction = onRoot()
- val node = nodeInteraction.fetchSemanticsNode("Failed to get screen height")
- @OptIn(ExperimentalLayoutNodeApi::class)
- val owner = node.componentNode.owner as AndroidOwner
-
- return with(owner.density) {
- owner.view.height.toDp()
- }
-}
+fun ComposeTestRule.rootHeight(): Dp = onRoot().getUnclippedBoundsInRoot().height
/**
* Constant to emulate very big but finite constraints
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 f33a0a9..6cbca52 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
@@ -21,8 +21,10 @@
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.selection.TextSelectionColors
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.height
@@ -36,6 +38,7 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.common.truth.Truth
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -59,6 +62,29 @@
val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
@Test
+ fun rememberTextSelectionColors() {
+ var lightTextSelectionColors: TextSelectionColors? = null
+ var darkTextSelectionColors: TextSelectionColors? = null
+ var lightPrimary: Color = Color.Unspecified
+ var darkPrimary: Color = Color.Unspecified
+ rule.setContent {
+ val lightColors = lightColors()
+ val darkColors = darkColors()
+ lightPrimary = lightColors.primary
+ darkPrimary = darkColors.primary
+ lightTextSelectionColors = rememberTextSelectionColors(lightColors)
+ darkTextSelectionColors = rememberTextSelectionColors(darkColors)
+ }
+
+ Truth.assertThat(lightTextSelectionColors!!.handleColor).isEqualTo(lightPrimary)
+ Truth.assertThat(darkTextSelectionColors!!.handleColor).isEqualTo(darkPrimary)
+ Truth.assertThat(lightTextSelectionColors!!.backgroundColor)
+ .isEqualTo(lightPrimary.copy(alpha = 0.325f))
+ Truth.assertThat(darkTextSelectionColors!!.backgroundColor)
+ .isEqualTo(darkPrimary.copy(alpha = 0.375f))
+ }
+
+ @Test
fun text_lightThemeSelectionColors() {
rule.setContent {
TextTestContent(lightColors())
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index 29fdbcd..64e7730 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -217,6 +217,35 @@
}
@Test
+ fun menu_positioning_top() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowBounds = IntBounds(0, 0, screenWidth, screenHeight)
+ val anchorPosition = IntOffset(0, 0)
+ val anchorSize = IntSize(50, 20)
+ val popupSize = IntSize(150, 500)
+
+ // The min margin above and below the menu, relative to the screen.
+ val MenuVerticalMargin = 32.dp
+ val verticalMargin = with(density) { MenuVerticalMargin.toIntPx() }
+
+ val position = DropdownMenuPositionProvider(
+ Position(0.dp, 0.dp),
+ density
+ ).calculatePosition(
+ IntBounds(anchorPosition, anchorSize),
+ windowBounds,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(position.y).isEqualTo(
+ verticalMargin
+ )
+ }
+
+ @Test
fun menu_positioning_callback() {
val screenWidth = 500
val screenHeight = 1000
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
index 7f909009..e32b647 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
@@ -159,6 +159,16 @@
}
@Test
+ fun slider_semantics_focusable() {
+ rule.setMaterialContent {
+ Slider(value = 0f, onValueChange = {}, modifier = Modifier.testTag(tag))
+ }
+
+ rule.onNodeWithTag(tag)
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Focused))
+ }
+
+ @Test
fun slider_drag() {
val state = mutableStateOf(0f)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
index 5cfd7af..30d12b7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
@@ -30,7 +30,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.emptyContent
@@ -153,7 +153,7 @@
// The color of the Ripple should always the selected color, as we want to show the color
// before the item is considered selected, and hence before the new contentColor is
// provided by BottomNavigationTransition.
- val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+ val ripple = rememberRipple(bounded = false, color = selectedContentColor)
// TODO This composable has magic behavior within a Row; reconsider this behavior later
Box(
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 1ae6d72..6b8cfc3 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
@@ -35,7 +35,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.triStateToggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -141,7 +141,7 @@
onClick = onClick,
enabled = enabled,
interactionState = interactionState,
- indication = rememberRippleIndication(
+ indication = rememberRipple(
bounded = false,
radius = CheckboxRippleRadius
)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 1162f52..5cb78fb 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -42,6 +42,8 @@
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -386,7 +388,14 @@
maxWidth = constraints.maxWidth.toDp(),
maxHeight = constraints.maxHeight.toDp()
)
- }.offset(x = { drawerState.offset.value }).padding(end = VerticalDrawerPadding),
+ }
+ .semantics {
+ if (drawerState.isOpen) {
+ dismiss(action = { drawerState.close(); true })
+ }
+ }
+ .offset(x = { drawerState.offset.value })
+ .padding(end = VerticalDrawerPadding),
shape = drawerShape,
color = drawerBackgroundColor,
contentColor = drawerContentColor,
@@ -500,7 +509,13 @@
maxWidth = constraints.maxWidth.toDp(),
maxHeight = constraints.maxHeight.toDp()
)
- }.offset(y = { drawerState.offset.value }),
+ }
+ .semantics {
+ if (drawerState.isOpen) {
+ dismiss(action = { drawerState.close(); true })
+ }
+ }
+ .offset(y = { drawerState.offset.value }),
shape = drawerShape,
color = drawerBackgroundColor,
contentColor = drawerContentColor,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
index c79d3b5..c2b3ad0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
@@ -22,7 +22,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -68,7 +68,7 @@
onClick = onClick,
enabled = enabled,
interactionState = interactionState,
- indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+ indication = rememberRipple(bounded = false, radius = RippleRadius)
)
.then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
@@ -108,7 +108,7 @@
onValueChange = onCheckedChange,
enabled = enabled,
interactionState = interactionState,
- indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+ indication = rememberRipple(bounded = false, radius = RippleRadius)
).then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
) { content() }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
index 5cb947f..4cd516e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
@@ -21,6 +21,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.useOrElse
import androidx.compose.ui.selection.TextSelectionColors
import androidx.compose.ui.util.annotation.VisibleForTesting
import kotlin.math.max
@@ -39,9 +40,12 @@
// Test with ContentAlpha.medium to ensure that the selection background is accessible in the
// 'worst case' scenario. We explicitly don't test with ContentAlpha.disabled, as disabled
// text shouldn't be selectable / is noted as disabled for accessibility purposes.
- val textColorWithLowestAlpha = contentColorFor(backgroundColor).copy(
- alpha = ContentAlpha.medium
- )
+ val textColorWithLowestAlpha = colors.contentColorFor(backgroundColor)
+ .useOrElse {
+ AmbientContentColor.current
+ }.copy(
+ alpha = ContentAlpha.medium
+ )
return remember(primaryColor, backgroundColor, textColorWithLowestAlpha) {
TextSelectionColors(
handleColor = colors.primary,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
index e65309a..0a1c321 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
@@ -18,9 +18,13 @@
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.Indication
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposableContract
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.remember
import androidx.compose.ui.selection.AmbientTextSelectionColors
@@ -50,6 +54,7 @@
* @param typography A set of text styles to be used as this hierarchy's typography system
* @param shapes A set of shapes to be used by the components in this hierarchy
*/
+@OptIn(ExperimentalRippleApi::class)
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors,
@@ -63,15 +68,16 @@
colors.copy()
}.apply { updateColorsFrom(colors) }
val indicationFactory: @Composable () -> Indication = remember {
- { rememberRippleIndication() }
+ @Composable { rememberRipple() }
}
val selectionColors = rememberTextSelectionColors(rememberedColors)
Providers(
AmbientColors provides rememberedColors,
AmbientContentAlpha provides ContentAlpha.high,
AmbientIndication provides indicationFactory,
- AmbientTextSelectionColors provides selectionColors,
+ AmbientRippleTheme provides MaterialRippleTheme,
AmbientShapes provides shapes,
+ AmbientTextSelectionColors provides selectionColors,
AmbientTypography provides typography
) {
ProvideTextStyle(value = typography.body1, content = content)
@@ -111,3 +117,19 @@
val shapes: Shapes
get() = AmbientShapes.current
}
+
+@OptIn(ExperimentalRippleApi::class)
+@Immutable
+private object MaterialRippleTheme : RippleTheme {
+ @Composable
+ override fun defaultColor() = RippleTheme.defaultRippleColor(
+ contentColor = AmbientContentColor.current,
+ lightTheme = MaterialTheme.colors.isLight
+ )
+
+ @Composable
+ override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
+ contentColor = AmbientContentColor.current,
+ lightTheme = MaterialTheme.colors.isLight
+ )
+}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 2a09612..f075e67 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -33,7 +33,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredSizeIn
import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Providers
@@ -179,7 +179,7 @@
enabled = enabled,
onClick = onClick,
interactionState = interactionState,
- indication = rememberRippleIndication(true)
+ indication = rememberRipple(true)
)
.fillMaxWidth()
// Preferred min and max width used during the intrinsic measurement.
@@ -318,7 +318,7 @@
} ?: toLeft
// Compute vertical position.
- val toBottom = parentGlobalBounds.bottom + contentOffsetY
+ val toBottom = maxOf(parentGlobalBounds.bottom + contentOffsetY, verticalMargin)
val toTop = parentGlobalBounds.top - contentOffsetY - popupContentSize.height
val toCenter = parentGlobalBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowGlobalBounds.height - popupContentSize.height - verticalMargin
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 8a8a6eb..6e9d2ed 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
@@ -32,7 +32,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
@@ -89,7 +89,7 @@
onClick = onClick,
enabled = enabled,
interactionState = interactionState,
- indication = rememberRippleIndication(
+ indication = rememberRipple(
bounded = false,
radius = RadioButtonRippleRadius
)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index 05b57ec..31d5c18 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.animation.FlingConfig
import androidx.compose.foundation.animation.defaultFlingConfig
import androidx.compose.foundation.animation.fling
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.indication
import androidx.compose.foundation.layout.Box
@@ -41,7 +42,7 @@
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.SliderConstants.InactiveTrackColorAlpha
import androidx.compose.material.SliderConstants.TickColorAlpha
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -257,13 +258,15 @@
shape = CircleShape,
color = thumbColor,
elevation = elevation,
- modifier = Modifier.indication(
- interactionState = interactionState,
- indication = rememberRippleIndication(
- bounded = false,
- radius = ThumbRippleRadius
+ modifier = Modifier
+ .focusable(interactionState = interactionState)
+ .indication(
+ interactionState = interactionState,
+ indication = rememberRipple(
+ bounded = false,
+ radius = ThumbRippleRadius
+ )
)
- )
) {
Spacer(Modifier.preferredSize(thumbSize, thumbSize))
}
@@ -357,7 +360,7 @@
1f -> 100
else -> (fraction * 100).roundToInt().coerceIn(1, 99)
}
- return semantics {
+ return semantics(mergeDescendants = true) {
accessibilityValue = Strings.TemplatePercent.format(percent)
accessibilityValueRange = AccessibilityRangeInfo(coerced, valueRange, steps)
setProgress(
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 4ad2c29..9881050 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
@@ -30,7 +30,7 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -171,7 +171,7 @@
.offset(x = { thumbValue.value })
.indication(
interactionState = interactionState,
- indication = rememberRippleIndication(bounded = false, radius = ThumbRippleRadius)
+ indication = rememberRipple(bounded = false, radius = ThumbRippleRadius)
)
.size(ThumbDiameter),
content = emptyContent()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index 63d8945..175501d 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -37,7 +37,7 @@
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.emptyContent
@@ -143,7 +143,7 @@
// The color of the Ripple should always the selected color, as we want to show the color
// before the item is considered selected, and hence before the new contentColor is
// provided by TabTransition.
- val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+ val ripple = rememberRipple(bounded = false, color = selectedContentColor)
TabTransition(selectedContentColor, unselectedContentColor, selected) {
Column(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
index 0cd8808..4486035 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
@@ -16,6 +16,9 @@
package androidx.compose.material
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxWidth
@@ -305,7 +308,7 @@
// Scrolls to the tab with [tabPosition], trying to place it in the center of the
// screen or as close to the center as possible.
val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions)
- scrollState.smoothScrollTo(calculatedOffset)
+ scrollState.smoothScrollTo(calculatedOffset, spec = ScrollableTabRowScrollSpec)
}
}
}
@@ -334,3 +337,11 @@
}
private val ScrollableTabRowMinimumTabWidth = 90.dp
+
+/**
+ * [AnimationSpec] used when scrolling to a tab that is not fully visible.
+ */
+private val ScrollableTabRowScrollSpec: AnimationSpec<Float> = tween(
+ durationMillis = 250,
+ easing = FastOutSlowInEasing
+)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
deleted file mode 100644
index 1dfa397..0000000
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:OptIn(ExperimentalMaterialApi::class)
-
-package androidx.compose.material.ripple
-
-import androidx.compose.foundation.Interaction
-import androidx.compose.material.AmbientContentColor
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.luminance
-
-/**
- * Defines the appearance and the behavior for [RippleIndication]s.
- *
- * You can define a new theme and apply it via [AmbientRippleTheme].
- */
-@ExperimentalMaterialApi
-interface RippleTheme {
- /**
- * @return the default [RippleIndication] color at the call site's position in the hierarchy.
- * This color will be used when a color is not explicitly set in the [RippleIndication] itself.
- */
- @Composable
- fun defaultColor(): Color
-
- /**
- * @return the [RippleOpacity] used to calculate the opacity for the ripple depending on the
- * [Interaction] for a given component. This will be set as the alpha channel for
- * [defaultColor] or the color explicitly provided to the [RippleIndication].
- */
- @Composable
- fun rippleOpacity(): RippleOpacity
-}
-
-// TODO: can be a fun interface when we rebase to use Kotlin 1.4
-/**
- * RippleOpacity defines the opacity of the ripple / state layer for a given [Interaction].
- */
-@ExperimentalMaterialApi
-interface RippleOpacity {
- /**
- * @return the opacity of the ripple for the given [interaction]. Return `0f` if this
- * particular interaction should not show a corresponding ripple / state layer.
- */
- fun opacityForInteraction(interaction: Interaction): Float
-}
-
-/**
- * Ambient used for providing [RippleTheme] down the tree.
- */
-@ExperimentalMaterialApi
-val AmbientRippleTheme = staticAmbientOf<RippleTheme> { DefaultRippleTheme }
-
-private object DefaultRippleTheme : RippleTheme {
- @Composable
- override fun defaultColor(): Color {
- val contentColor = AmbientContentColor.current
- val lightTheme = MaterialTheme.colors.isLight
- val contentLuminance = contentColor.luminance()
- // If we are on a colored surface (typically indicated by low luminance content), the
- // ripple color should be white.
- return if (!lightTheme && contentLuminance < 0.5) {
- Color.White
- // Otherwise use contentColor
- } else {
- contentColor
- }
- }
-
- @Composable
- override fun rippleOpacity(): RippleOpacity {
- val lightTheme = MaterialTheme.colors.isLight
- val contentLuminance = AmbientContentColor.current.luminance()
- return when {
- lightTheme -> {
- if (contentLuminance > 0.5) {
- LightThemeHighContrastRippleOpacity
- } else {
- LightThemeReducedContrastRippleOpacity
- }
- }
- else -> {
- DarkThemeRippleOpacity
- }
- }
- }
-}
-
-@Suppress("unused")
-private sealed class DefaultRippleOpacity(
- val pressed: Float,
- val focused: Float,
- val dragged: Float,
- val hovered: Float
-) : RippleOpacity {
- override fun opacityForInteraction(interaction: Interaction): Float = when (interaction) {
- Interaction.Pressed -> pressed
- Interaction.Dragged -> dragged
- else -> 0f
- }
-}
-
-/**
- * Opacity values for high luminance content in a light theme.
- *
- * This content will typically be placed on colored surfaces, so it is important that the
- * contrast here is higher to meet accessibility standards, and increase legibility.
- *
- * These levels are typically used for text / iconography in primary colored tabs /
- * bottom navigation / etc.
- */
-private object LightThemeHighContrastRippleOpacity : DefaultRippleOpacity(
- pressed = 0.24f,
- focused = 0.24f,
- dragged = 0.16f,
- hovered = 0.08f
-)
-
-/**
- * Alpha levels for low luminance content in a light theme.
- *
- * This content will typically be placed on grayscale surfaces, so the contrast here can be lower
- * without sacrificing accessibility and legibility.
- *
- * These levels are typically used for body text on the main surface (white in light theme, grey
- * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
- */
-private object LightThemeReducedContrastRippleOpacity : DefaultRippleOpacity(
- pressed = 0.12f,
- focused = 0.12f,
- dragged = 0.08f,
- hovered = 0.04f
-)
-
-/**
- * Alpha levels for all content in a dark theme.
- */
-private object DarkThemeRippleOpacity : DefaultRippleOpacity(
- pressed = 0.10f,
- focused = 0.12f,
- dragged = 0.08f,
- hovered = 0.04f
-)
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
index 764c108..853e0ac 100644
--- a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
+++ b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
@@ -66,38 +66,50 @@
}.toTypedArray()
}
+ /**
+ * In light theme the default background color is white, with black text and a medium content
+ * alpha of 74%.
+ */
@Test
- fun assertContrast_blackTextOnWhiteBackground() {
+ fun assertContrast_lightTheme_backgroundBackground() {
assertContrastRatio(
selectionColor = color,
textColor = Color.Black,
+ textAlpha = 0.74f,
backgroundColor = Color.White
)
}
+ /**
+ * In dark theme the default background color is #121212, with white text and a medium content
+ * alpha of 60%.
+ */
@Test
- fun assertContrast_whiteTextOnBlackBackground() {
+ fun assertContrast_darkTheme_backgroundBackground() {
assertContrastRatio(
selectionColor = color,
textColor = Color.White,
- backgroundColor = Color.Black
+ textAlpha = 0.6f,
+ backgroundColor = Color(0xFF121212)
)
}
@Test
- fun assertContrast_blackTextOnPrimaryBackground() {
+ fun assertContrast_lightTheme_primaryBackground() {
assertContrastRatio(
selectionColor = color,
textColor = Color.Black,
+ textAlpha = 0.74f,
backgroundColor = color
)
}
@Test
- fun assertContrast_whiteTextOnPrimaryBackground() {
+ fun assertContrast_darkTheme_primaryBackground() {
assertContrastRatio(
selectionColor = color,
textColor = Color.White,
+ textAlpha = 0.6f,
backgroundColor = color
)
}
@@ -106,11 +118,13 @@
private fun assertContrastRatio(
selectionColor: Color,
textColor: Color,
+ textAlpha: Float,
backgroundColor: Color
) {
+ val textColorWithAlpha = textColor.copy(alpha = textAlpha)
val selectionBackgroundColor = calculateSelectionBackgroundColor(
selectionColor = selectionColor,
- textColor = textColor,
+ textColor = textColorWithAlpha,
backgroundColor = backgroundColor
)
@@ -118,7 +132,7 @@
// using the default alpha for consistency with the spec.
val minimumCompositeBackground = selectionBackgroundColor.copy(alpha = MinimumSelectionAlpha)
.compositeOver(backgroundColor)
- val minimumCompositeTextColor = textColor.compositeOver(minimumCompositeBackground)
+ val minimumCompositeTextColor = textColorWithAlpha.compositeOver(minimumCompositeBackground)
val minimumContrastRatio = calculateContrastRatio(
foreground = minimumCompositeTextColor,
background = minimumCompositeBackground
@@ -131,7 +145,7 @@
// Otherwise, ensure that the value we choose is accessible
val compositeBackground = selectionBackgroundColor.compositeOver(backgroundColor)
- val compositeTextColor = textColor.compositeOver(compositeBackground)
+ val compositeTextColor = textColorWithAlpha.compositeOver(compositeBackground)
val contrastRatio = calculateContrastRatio(
foreground = compositeTextColor,
background = compositeBackground
@@ -143,7 +157,7 @@
// If we searched for a new alpha, this alpha should be the maximal alpha that meets
// contrast requirements, so the contrast ratio should be just above 4.5f in order to
// maximize alpha
- assertThat(contrastRatio).isWithin(0.05f).of(RequiredContrastRatio)
+ assertThat(contrastRatio).isWithin(0.1f).of(RequiredContrastRatio)
}
}
diff --git a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
index 8cb5760..711bd90 100644
--- a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
+++ b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
@@ -18,6 +18,7 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
+import java.awt.DisplayMode
import java.awt.GraphicsEnvironment
// TODO implement local Recomposer in each Window, so each Window can have own MonotonicFrameClock.
@@ -32,14 +33,17 @@
if (GraphicsEnvironment.isHeadless()) {
yield()
} else {
- val defaultRefreshRate = GraphicsEnvironment
- .getLocalGraphicsEnvironment()
- .defaultScreenDevice
- .displayMode
- .refreshRate
- delay(1000L / defaultRefreshRate)
+ delay(1000L / getFramesPerSecond())
}
return onFrame(System.nanoTime())
}
+
+ private fun getFramesPerSecond(): Int {
+ val refreshRate = GraphicsEnvironment
+ .getLocalGraphicsEnvironment()
+ .screenDevices.maxOfOrNull { it.displayMode.refreshRate }
+ ?: DisplayMode.REFRESH_RATE_UNKNOWN
+ return if (refreshRate != DisplayMode.REFRESH_RATE_UNKNOWN) refreshRate else 60
+ }
}
}
\ No newline at end of file
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index ac887e3..1711449 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -32,7 +32,7 @@
kotlinPlugin project(":compose:compiler:compiler")
implementation(KOTLIN_STDLIB)
- implementation project(":annotation:annotation-sampled")
+ compileOnly project(":annotation:annotation-sampled")
implementation project(":compose:foundation:foundation")
implementation project(":compose:material:material")
implementation project(":compose:runtime:runtime-rxjava3")
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 835bdc3..61b92204 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -84,7 +84,7 @@
}
public final class Composer<N> {
- ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+ ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,22 +98,16 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
- method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
- method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
- method public androidx.compose.runtime.Applier<N> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method public int getCurrentCompoundKeyHash();
method public boolean getDefaultsInvalid();
- method public boolean getInserting();
method public boolean getSkipping();
method public androidx.compose.runtime.SlotTable getSlotTable();
method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
- method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,17 +117,13 @@
method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
- property public final androidx.compose.runtime.Applier<N> applier;
property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
property public final int currentCompoundKeyHash;
property public final boolean defaultsInvalid;
- property public final boolean inserting;
property public final boolean skipping;
property public final androidx.compose.runtime.SlotTable slotTable;
}
@@ -273,19 +263,6 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
}
- public final class ObserverMap<K, V> {
- ctor public ObserverMap();
- method public void add(K key, V value);
- method public void clear();
- method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
- method public boolean contains(K key, V value);
- method public operator java.util.List<V> get(Iterable<? extends K> keys);
- method public java.util.List<V> getValueOf(K key);
- method public void remove(K key);
- method public void remove(K key, V value);
- method public void removeValue(V value);
- }
-
public final class ProduceStateKt {
method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,12 +336,7 @@
}
public final class SkippableUpdater<T> {
- ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
@androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -545,16 +517,11 @@
}
public final class Updater<T> {
- ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
}
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 835bdc3..61b92204 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -84,7 +84,7 @@
}
public final class Composer<N> {
- ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+ ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,22 +98,16 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
- method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
- method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
- method public androidx.compose.runtime.Applier<N> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method public int getCurrentCompoundKeyHash();
method public boolean getDefaultsInvalid();
- method public boolean getInserting();
method public boolean getSkipping();
method public androidx.compose.runtime.SlotTable getSlotTable();
method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
- method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,17 +117,13 @@
method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
- property public final androidx.compose.runtime.Applier<N> applier;
property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
property public final int currentCompoundKeyHash;
property public final boolean defaultsInvalid;
- property public final boolean inserting;
property public final boolean skipping;
property public final androidx.compose.runtime.SlotTable slotTable;
}
@@ -273,19 +263,6 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
}
- public final class ObserverMap<K, V> {
- ctor public ObserverMap();
- method public void add(K key, V value);
- method public void clear();
- method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
- method public boolean contains(K key, V value);
- method public operator java.util.List<V> get(Iterable<? extends K> keys);
- method public java.util.List<V> getValueOf(K key);
- method public void remove(K key);
- method public void remove(K key, V value);
- method public void removeValue(V value);
- }
-
public final class ProduceStateKt {
method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,12 +336,7 @@
}
public final class SkippableUpdater<T> {
- ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
@androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -545,16 +517,11 @@
}
public final class Updater<T> {
- ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
}
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 3d9fadf..a649fe2 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -84,7 +84,7 @@
}
public final class Composer<N> {
- ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+ ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,23 +98,20 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
- method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
- method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
+ method @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
+ method @kotlin.PublishedApi internal void emitNode(Object? node);
method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
- method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
+ method @kotlin.PublishedApi internal void endNode();
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
- method public androidx.compose.runtime.Applier<N> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method public int getCurrentCompoundKeyHash();
method public boolean getDefaultsInvalid();
- method public boolean getInserting();
method public boolean getSkipping();
method public androidx.compose.runtime.SlotTable getSlotTable();
method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
- method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
+ method @kotlin.PublishedApi internal Object? nextSlot();
method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -124,20 +121,19 @@
method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
+ method @kotlin.PublishedApi internal void startNode();
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
- method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal void updateValue(Object? value);
- method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
- property public final androidx.compose.runtime.Applier<N> applier;
+ method @kotlin.PublishedApi internal void updateValue(Object? value);
+ method @kotlin.PublishedApi internal N! useNode();
property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
property public final int currentCompoundKeyHash;
property public final boolean defaultsInvalid;
- property public final boolean inserting;
property public final boolean skipping;
property public final androidx.compose.runtime.SlotTable slotTable;
+ field @kotlin.PublishedApi internal boolean inserting;
}
public final class ComposerKt {
@@ -294,19 +290,6 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
}
- public final class ObserverMap<K, V> {
- ctor public ObserverMap();
- method public void add(K key, V value);
- method public void clear();
- method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
- method public boolean contains(K key, V value);
- method public operator java.util.List<V> get(Iterable<? extends K> keys);
- method public java.util.List<V> getValueOf(K key);
- method public void remove(K key);
- method public void remove(K key, V value);
- method public void removeValue(V value);
- }
-
@kotlin.PublishedApi internal final class PreCommitScopeImpl implements androidx.compose.runtime.CommitScope androidx.compose.runtime.CompositionLifecycleObserver {
ctor public PreCommitScopeImpl(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> onCommit);
method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
@@ -385,12 +368,8 @@
}
public final class SkippableUpdater<T> {
- ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
+ ctor @kotlin.PublishedApi internal SkippableUpdater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
@androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -573,16 +552,12 @@
}
public final class Updater<T> {
- ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
- method public androidx.compose.runtime.Composer<?> getComposer();
- method public T! getNode();
+ ctor @kotlin.PublishedApi internal Updater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
- property public final androidx.compose.runtime.Composer<?> composer;
- property public final T! node;
}
}
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 57fdeed..5a834fb 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -46,7 +46,6 @@
implementation "androidx.annotation:annotation:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3"
implementation("androidx.core:core-ktx:1.1.0")
- implementation(KOTLIN_REFLECT)
implementation(KOTLIN_STDLIB)
testImplementation KOTLIN_TEST_JUNIT
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 563fabe0..3cc12dd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
* IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
* `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
*/
- const val version: Int = 1900
+ const val version: Int = 2000
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index b3ea2bc..206e8f4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -61,7 +61,6 @@
* changed. It is used to determine how to update the nodes and the slot table when changes to the
* structure of the tree is detected.
*/
-@OptIn(InternalComposeApi::class)
private class Pending(
val keyInfos: MutableList<KeyInfo>,
val startIndex: Int
@@ -351,8 +350,7 @@
/**
* An adapter that applies changes to the tree using the Applier abstraction.
*/
- @ComposeCompilerApi
- val applier: Applier<N>,
+ @PublishedApi internal val applier: Applier<N>,
/**
* Parent of this composition; a [Recomposer] for root-level compositions.
@@ -395,7 +393,6 @@
private var reader: SlotReader = slotTable.openReader().also { it.close() }
- @OptIn(InternalComposeApi::class)
internal val insertTable = SlotTable()
private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
@@ -538,7 +535,6 @@
* Start the composition. This should be called, and only be called, as the first group in
* the composition.
*/
- @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
private fun startRoot() {
reader = slotTable.openReader()
startGroup(rootKey)
@@ -560,8 +556,6 @@
* End the composition. This should be called, and only be called, to end the first group in
* the composition.
*/
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
private fun endRoot() {
endGroup()
parentReference.doneComposing()
@@ -575,7 +569,6 @@
/**
* Discard a pending composition because an error was encountered during composition
*/
- @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun abortRoot() {
cleanUpCompose()
pendingStack.clear()
@@ -596,8 +589,8 @@
* first composition this is always true. During recomposition this is true when new nodes
* are being scheduled to be added to the tree.
*/
- @ComposeCompilerApi
- var inserting: Boolean = false
+ @PublishedApi
+ internal var inserting: Boolean = false
private set
/**
@@ -763,7 +756,6 @@
* Apply the changes to the tree that were collected during the last composition.
*/
@InternalComposeApi
- @OptIn(ComposeCompilerApi::class)
fun applyChanges() {
trace("Compose:applyChanges") {
invalidateStack.clear()
@@ -812,7 +804,6 @@
}
@ExperimentalComposeApi
- @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
internal fun dispose() {
trace("Compose:Composer.dispose") {
parentReference.unregisterComposer(this)
@@ -845,19 +836,15 @@
*
* @param key The key for the group
*/
- @ComposeCompilerApi
internal fun startGroup(key: Int) = start(key, null, false, null)
- @ComposeCompilerApi
internal fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null)
/**
* End the current group.
*/
- @ComposeCompilerApi
internal fun endGroup() = end(false)
- @OptIn(InternalComposeApi::class)
private fun skipGroup() {
groupNodeCount += reader.skipGroup()
}
@@ -869,8 +856,8 @@
* current position if found, if no such node is found the composition switches into insert
* mode and a the node is scheduled to be inserted at the current location.
*/
- @ComposeCompilerApi
- fun startNode() {
+ @PublishedApi
+ internal fun startNode() {
start(nodeKey, null, true, null)
nodeExpected = true
}
@@ -880,9 +867,7 @@
* call when the composer is inserting.
*/
@Suppress("UNUSED")
- @ComposeCompilerApi
- @OptIn(ExperimentalComposeApi::class, InternalComposeApi::class)
- fun <T : N> createNode(factory: () -> T) {
+ internal fun <T : N> createNode(factory: () -> T) {
validateNodeExpected()
check(inserting) { "createNode() can only be called when inserting" }
val insertIndex = nodeIndexStack.peek()
@@ -900,9 +885,8 @@
* Schedule the given node to be inserted. This is only valid to call when the composer is
* inserting.
*/
- @ComposeCompilerApi
- @OptIn(ExperimentalComposeApi::class)
- fun emitNode(node: Any?) {
+ @PublishedApi
+ internal fun emitNode(node: Any?) {
validateNodeExpected()
check(inserting) { "emitNode() called when not inserting" }
val insertIndex = nodeIndexStack.peek()
@@ -921,8 +905,8 @@
* valid to call when the composition is not inserting. This must be called at the same
* location as [emitNode] or [createNode] as called even if the value is unused.
*/
- @ComposeCompilerApi
- fun useNode(): N {
+ @PublishedApi
+ internal fun useNode(): N {
validateNodeExpected()
check(!inserting) { "useNode() called while inserting" }
val result = reader.node
@@ -933,15 +917,14 @@
/**
* Called to end the node group.
*/
- @ComposeCompilerApi
- fun endNode() = end(true)
+ @PublishedApi
+ internal fun endNode() = end(true)
/**
* Schedule a change to be applied to a node's property. This change will be applied to the
* node that is the current node in the tree which was either created by [createNode],
* emitted by [emitNode] or returned by [useNode].
*/
- @OptIn(ExperimentalComposeApi::class)
internal fun <V, T> apply(value: V, block: T.(V) -> Unit) {
recordApplierOperation { applier, _, _ ->
@Suppress("UNCHECKED_CAST")
@@ -954,16 +937,14 @@
* use the key stored at the current location in the slot table to avoid allocating a new key.
*/
@ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
fun joinKey(left: Any?, right: Any?): Any =
getKey(reader.groupObjectKey, left, right) ?: JoinedKey(left, right)
/**
* Return the next value in the slot table and advance the current location.
*/
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
- fun nextSlot(): Any? = if (inserting) {
+ @PublishedApi
+ internal fun nextSlot(): Any? = if (inserting) {
validateNodeNotExpected()
EMPTY
} else reader.next()
@@ -1099,9 +1080,7 @@
*
* @param value the value to schedule to be written to the slot table.
*/
- @OptIn(InternalComposeApi::class)
@PublishedApi
- @ComposeCompilerApi
internal fun updateValue(value: Any?) {
if (inserting) {
writer.update(value)
@@ -1137,8 +1116,6 @@
/**
* Return the current ambient scope which was provided by a parent group.
*/
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
private fun currentAmbientScope(): AmbientMap {
if (inserting && hasProvider) {
var current = writer.parent
@@ -1173,8 +1150,6 @@
* compose which might be inserting the sub-composition. In that case the current scope
* is the correct scope.
*/
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
private fun ambientScopeAt(location: Int): AmbientMap {
if (isComposing) {
// The sub-composer is being composed as part of a nested composition then use the
@@ -1204,7 +1179,6 @@
* scope followed by the map used to augment the parent scope. Both are needed to detect
* inserts, updates and deletes to the providers.
*/
- @ComposeCompilerApi
private fun updateProviderMapGroup(
parentScope: AmbientMap,
currentProviders: AmbientMap
@@ -1217,8 +1191,6 @@
return providerScope
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
internal fun startProviders(values: Array<out ProvidedValue<*>>) {
val parentScope = currentAmbientScope()
startGroup(providerKey, provider)
@@ -1268,22 +1240,18 @@
start(ambientMapKey, ambientMap, false, providers)
}
- @ComposeCompilerApi
internal fun endProviders() {
endGroup()
endGroup()
providersInvalid = providersInvalidStack.pop().asBool()
}
- @ComposeCompilerApi
@PublishedApi
internal fun <T> consume(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
/**
* Create or use a memoized `CompositionReference` instance at this position in the slot table.
*/
- @ComposeCompilerApi
- @OptIn(ExperimentalComposeApi::class)
internal fun buildReference(): CompositionReference {
startGroup(referenceKey, reference)
@@ -1304,10 +1272,8 @@
private fun <T> resolveAmbient(key: Ambient<T>, scope: AmbientMap): T =
if (scope.contains(key)) scope.getValueOf(key) else parentReference.getAmbient(key)
- @ComposeCompilerApi
internal fun <T> parentAmbient(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
- @ComposeCompilerApi
private fun <T> parentAmbient(key: Ambient<T>, location: Int): T =
resolveAmbient(key, ambientScopeAt(location))
@@ -1324,7 +1290,6 @@
if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null
}
- @OptIn(InternalComposeApi::class)
private fun ensureWriter() {
if (writer.closed) {
writer = insertTable.openWriter()
@@ -1337,7 +1302,6 @@
/**
* Start the reader group updating the data of the group if necessary
*/
- @OptIn(InternalComposeApi::class)
private fun startReaderGroup(isNode: Boolean, data: Any?) {
if (isNode) {
reader.startNode()
@@ -1351,8 +1315,6 @@
}
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
validateNodeNotExpected()
@@ -1478,7 +1440,6 @@
groupNodeCount = 0
}
- @OptIn(InternalComposeApi::class)
private fun exitGroup(expectedNodeCount: Int, inserting: Boolean) {
// Restore the parent's state updating them if they have changed based on changes in the
// children. For example, if a group generates nodes then the number of generated nodes will
@@ -1493,8 +1454,6 @@
this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun end(isNode: Boolean) {
// All the changes to the group (or node) have been recorded. All new nodes have been
// inserted but it has yet to determine which need to be removed or moved. Note that the
@@ -1649,7 +1608,6 @@
* called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
* are invalid it will call [skipReaderToGroupEnd].
*/
- @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun recomposeToGroupEnd() {
val wasComposing = isComposing
isComposing = true
@@ -1738,7 +1696,6 @@
* updates that count and then updates any parent groups that include the nodes this group
* emits.
*/
- @OptIn(InternalComposeApi::class)
private fun updateNodeCountOverrides(group: Int, newCount: Int) {
// The value of group can be negative which indicates it is tracking an inserted group
// instead of an existing group. The index is a virtual index calculated by
@@ -1778,7 +1735,6 @@
* [recomposeIndex] allows the calculation to exit early if there is no node group between
* [group] and [recomposeGroup].
*/
- @OptIn(InternalComposeApi::class)
private fun nodeIndexOf(
groupLocation: Int,
group: Int,
@@ -1814,7 +1770,6 @@
return index
}
- @OptIn(InternalComposeApi::class)
private fun updatedNodeCount(group: Int): Int {
if (group < 0) return nodeCountVirtualOverrides?.let { it[group] } ?: 0
val nodeCounts = nodeCountOverrides
@@ -1825,7 +1780,6 @@
return reader.nodeCount(group)
}
- @OptIn(InternalComposeApi::class)
private fun updateNodeCount(group: Int, count: Int) {
if (updatedNodeCount(group) != count) {
if (group < 0) {
@@ -1856,7 +1810,6 @@
* Records the operations necessary to move the applier the node affected by the previous
* group to the new group.
*/
- @OptIn(InternalComposeApi::class)
private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) {
val reader = reader
val nearestCommonRoot = reader.nearestCommonRootOf(
@@ -1876,7 +1829,6 @@
doRecordDownsFor(newGroup, nearestCommonRoot)
}
- @OptIn(InternalComposeApi::class)
private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) {
if (group > 0 && group != nearestCommonRoot) {
doRecordDownsFor(reader.parent(group), nearestCommonRoot)
@@ -1889,7 +1841,6 @@
* for [group]. Passing in the [recomposeGroup] and [recomposeKey] allows this method to exit
* early.
*/
- @OptIn(InternalComposeApi::class)
private fun compoundKeyOf(group: Int, recomposeGroup: Int, recomposeKey: Int): Int {
return if (group == recomposeGroup) recomposeKey else (
compoundKeyOf(
@@ -1909,12 +1860,10 @@
* back into a known good state after a period of time when snapshot changes were not
* being observed.
*/
- @OptIn(InternalComposeApi::class)
internal fun invalidateAll() {
slotTable.slots.forEach { (it as? RecomposeScope)?.invalidate() }
}
- @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
internal fun invalidate(scope: RecomposeScope): InvalidationResult {
if (scope.defaultsInScope) {
scope.defaultsInvalid = true
@@ -1940,7 +1889,6 @@
* composition is not inserting.
*/
@ComposeCompilerApi
- @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
fun skipCurrentGroup() {
if (invalidations.isEmpty()) {
skipGroup()
@@ -1956,7 +1904,6 @@
}
}
- @OptIn(InternalComposeApi::class)
private fun skipReaderToGroupEnd() {
groupNodeCount = reader.parentNodes
reader.skipToGroupEnd()
@@ -1994,8 +1941,6 @@
addRecomposeScope()
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
private fun addRecomposeScope() {
if (inserting) {
val scope = RecomposeScope(this)
@@ -2016,7 +1961,6 @@
* [endRestartGroup]).
*/
@ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
fun endRestartGroup(): ScopeUpdateScope? {
// This allows for the invalidate stack to be out of sync since this might be called during exception stack
// unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
@@ -2045,7 +1989,6 @@
* which must be applied by [applyChanges] to build the tree implied by [block].
*/
@InternalComposeApi
- @OptIn(ComposeCompilerApi::class)
fun composeInitial(block: @Composable () -> Unit) {
trace("Compose:recompose") {
var complete = false
@@ -2070,7 +2013,6 @@
* applied by [applyChanges] to have an effect.
*/
@InternalComposeApi
- @OptIn(ComposeCompilerApi::class)
fun recompose(): Boolean {
if (invalidations.isNotEmpty()) {
trace("Compose:recompose") {
@@ -2095,15 +2037,12 @@
internal fun hasInvalidations() = invalidations.isNotEmpty()
- @OptIn(InternalComposeApi::class)
@Suppress("UNCHECKED_CAST")
private var SlotWriter.node
get() = node(currentGroup) as N
set(value) { updateParentNode(value) }
- @OptIn(InternalComposeApi::class)
@Suppress("UNCHECKED_CAST")
private val SlotReader.node get() = node(parent) as N
- @OptIn(InternalComposeApi::class)
@Suppress("UNCHECKED_CAST")
private fun SlotReader.nodeAt(index: Int) = node(index) as N
@@ -2169,7 +2108,6 @@
private var pendingUps = 0
private var downNodes = Stack<N>()
- @OptIn(ExperimentalComposeApi::class)
private fun realizeUps() {
val count = pendingUps
if (count > 0) {
@@ -2178,7 +2116,6 @@
}
}
- @OptIn(ExperimentalComposeApi::class)
private fun realizeDowns(nodes: Array<N>) {
record { applier, _, _ ->
for (index in nodes.indices) {
@@ -2214,7 +2151,6 @@
pendingInsertUps++
}
- @OptIn(ExperimentalComposeApi::class)
private fun realizeInsertUps() {
if (pendingInsertUps > 0) {
val count = pendingInsertUps
@@ -2259,7 +2195,6 @@
*/
private val startedGroups = IntStack()
- @OptIn(InternalComposeApi::class)
private fun realizeOperationLocation(forParent: Boolean = false) {
val location = if (forParent) reader.parent else reader.currentGroup
val distance = location - writersReaderDelta
@@ -2270,7 +2205,6 @@
}
}
- @OptIn(InternalComposeApi::class)
private fun recordInsert(anchor: Anchor) {
if (insertFixups.isEmpty()) {
recordSlotEditingOperation { _, slots, _ ->
@@ -2294,7 +2228,6 @@
}
}
- @OptIn(InternalComposeApi::class)
private fun recordFixup(change: Change<N>) {
realizeInsertUps()
val anchor = insertAnchor
@@ -2312,7 +2245,6 @@
* writer and reader are tracking the same slot we advance the [writersReaderDelta] to
* account for the removal.
*/
- @OptIn(InternalComposeApi::class)
private fun recordDelete() {
recordSlotEditingOperation(change = removeCurrentGroupInstance)
writersReaderDelta += reader.groupSize
@@ -2321,7 +2253,6 @@
/**
* Called when reader current is moved directly, such as when a group moves, to [location].
*/
- @OptIn(InternalComposeApi::class)
private fun recordReaderMoving(location: Int) {
val distance = reader.currentGroup - writersReaderDelta
@@ -2329,7 +2260,6 @@
writersReaderDelta = location - distance
}
- @OptIn(InternalComposeApi::class)
private fun recordSlotEditing() {
// During initial composition (when the slot table is empty), no group needs
// to be started.
@@ -2350,7 +2280,6 @@
}
}
- @OptIn(InternalComposeApi::class)
private fun recordEndGroup() {
val location = reader.parent
val currentStartedGroup = startedGroups.peekOr(-1)
@@ -2376,7 +2305,6 @@
cleanUpCompose()
}
- @OptIn(ExperimentalComposeApi::class)
private fun cleanUpCompose() {
pending = null
nodeIndex = 0
@@ -2421,7 +2349,6 @@
}
}
- @OptIn(ExperimentalComposeApi::class)
private fun realizeMovement() {
val count = previousCount
previousCount = 0
@@ -2504,8 +2431,6 @@
invalidate(scope)
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
override fun <T> getAmbient(key: Ambient<T>): T {
val anchor = scope.anchor
return if (anchor != null && anchor.valid) {
@@ -2517,8 +2442,6 @@
}
}
- @ComposeCompilerApi
- @OptIn(InternalComposeApi::class)
override fun getAmbientScope(): AmbientMap {
return ambientScopeAt(scope.anchor?.toIndexFor(slotTable) ?: 0)
}
@@ -2540,7 +2463,6 @@
}
}
- @OptIn(ExperimentalComposeApi::class)
private fun updateCompoundKeyWhenWeEnterGroup(groupKey: Int, dataKey: Any?) {
if (dataKey == null)
updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey)
@@ -2568,8 +2490,10 @@
}
@Suppress("UNCHECKED_CAST")
-/*inline */ class Updater<T>(val composer: Composer<*>, val node: T) {
- @OptIn(ComposeCompilerApi::class)
+/*inline */ class Updater<T> @PublishedApi internal constructor(
+ @PublishedApi internal val composer: Composer<*>,
+ @PublishedApi internal val node: T
+) {
inline fun set(
value: Int,
/*crossinline*/
@@ -2583,7 +2507,6 @@
}
}
- @OptIn(ComposeCompilerApi::class)
inline fun <reified V> set(
value: V,
/*crossinline*/
@@ -2597,7 +2520,6 @@
}
}
- @OptIn(ComposeCompilerApi::class)
inline fun update(
value: Int,
/*crossinline*/
@@ -2611,7 +2533,6 @@
}
}
- @OptIn(ComposeCompilerApi::class)
inline fun <reified V> update(
value: V,
/*crossinline*/
@@ -2632,8 +2553,10 @@
}
}
-class SkippableUpdater<T>(val composer: Composer<*>, val node: T) {
- @OptIn(ComposeCompilerApi::class)
+class SkippableUpdater<T> @PublishedApi internal constructor(
+ @PublishedApi internal val composer: Composer<*>,
+ @PublishedApi internal val node: T
+) {
inline fun update(block: Updater<T>.() -> Unit) {
composer.startReplaceableGroup(0x1e65194f)
Updater(composer, node).block()
@@ -2641,7 +2564,6 @@
}
}
-@OptIn(InternalComposeApi::class)
private fun SlotWriter.removeCurrentGroup(lifecycleManager: LifecycleManager) {
// Notify the lifecycle manager of any observers leaving the slot table
// The notification order should ensure that listeners are notified of leaving
@@ -2896,7 +2818,6 @@
return realFn(composer, 1)
}
-@OptIn(InternalComposeApi::class)
private fun SlotReader.distanceFrom(index: Int, root: Int): Int {
var count = 0
var current = index
@@ -2908,7 +2829,6 @@
}
// find the nearest common root
-@OptIn(InternalComposeApi::class)
private fun SlotReader.nearestCommonRootOf(a: Int, b: Int, common: Int): Int {
// Early outs, to avoid calling distanceFrom in trivial cases
if (a == b) return a // A group is the nearest common root of itself
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
index d027d69..67ebb1b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
@@ -95,6 +95,32 @@
currentComposer.endNode()
}
+/**
+ * Emits a node into the composition of type [T]. Nodes emitted inside of [content] will become
+ * children of the emitted node.
+ *
+ * This function will throw a runtime exception if [E] is not a subtype of the applier of the
+ * [currentComposer].
+ *
+ * @sample androidx.compose.runtime.samples.CustomTreeComposition
+ *
+ * @param ctor A function which will create a new instance of [T]. This function is NOT
+ * guaranteed to be called in place.
+ * @param update A function to perform updates on the node. This will run every time emit is
+ * executed. This function is called in place and will be inlined.
+ * @param skippableUpdate A function to perform updates on the node. Unlike [update], this
+ * function is Composable and will therefore be skipped unless it has been invalidated by some
+ * other mechanism. This can be useful to perform expensive calculations for updating the node
+ * where the calculations are likely to have the same inputs over time, so the function's
+ * execution can be skipped.
+ * @param content the composable content that will emit the "children" of this node.
+ *
+ * @see Updater
+ * @see SkippableUpdater
+ * @see Applier
+ * @see emit
+ * @see compositionFor
+ */
@Suppress("ComposableNaming")
@OptIn(ComposeCompilerApi::class)
@Composable @ComposableContract(readonly = true)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index bfc4e99..31c52b0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -32,19 +32,6 @@
internal expect inline fun <R> synchronized(lock: Any, block: () -> R): R
-expect open class WeakReference<T> : Reference<T> {
- constructor(referent: T)
- constructor(referent: T, q: ReferenceQueue<in T>?)
-}
-
-expect abstract class Reference<T> {
- open fun get(): T?
-}
-
-expect open class ReferenceQueue<T>() {
- open fun poll(): Reference<out T>?
-}
-
expect class AtomicReference<V>(value: V) {
fun get(): V
fun set(value: V)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
deleted file mode 100644
index 1616cfc..0000000
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.runtime
-
-/**
- * A map from a key to a set of values used for keeping the relation between some
- * entities and a models changes of which this entities are observing.
- *
- * Two main differences from a regular Map<K, Set<V>>:
- * 1) Object.hashCode is not used, so the values can be mutable and change their hashCode value
- * 2) Objects are stored with WeakReference to prevent leaking them.
-*/
-class ObserverMap<K : Any, V : Any> {
- private val keyToValue =
- hashMapOf<IdentityWeakReference<K>, MutableSet<IdentityWeakReference<V>>>()
- private val valueToKey =
- hashMapOf<IdentityWeakReference<V>, MutableSet<IdentityWeakReference<K>>>()
- private val keyQueue = ReferenceQueue<K>()
- private val valueQueue = ReferenceQueue<V>()
-
- /**
- * Adds a [value] into a set associated with this [key].
- */
- fun add(key: K, value: V) {
- clearReferences()
- val weakKey = IdentityWeakReference(key, keyQueue)
- val weakValue = IdentityWeakReference(value, valueQueue)
- addToSet(keyToValue, weakKey, weakValue)
- addToSet(valueToKey, weakValue, weakKey)
- }
-
- /**
- * Removes all the values associated with this [key].
- *
- * @return the list of values removed from the set as a result of this operation.
- */
- fun remove(key: K) {
- clearReferences()
- val weakKey = IdentityWeakReference(key)
- removeFromSet(keyToValue, valueToKey, weakKey)
- }
-
- /**
- * Removes exact [value] from the set associated with this [key].
- */
- fun remove(key: K, value: V) {
- clearReferences()
- val weakKey = IdentityWeakReference(key)
- val weakValue = IdentityWeakReference(value)
- keyToValue[weakKey]?.remove(weakValue)
- valueToKey[weakValue]?.remove(weakKey)
- }
-
- /**
- * Returns `true` when the map contains the given key and value
- */
- fun contains(key: K, value: V): Boolean {
- clearReferences()
- val set = keyToValue[IdentityWeakReference(key)]
- return set?.contains(IdentityWeakReference(value)) ?: false
- }
-
- /**
- * Clears all the keys and values from the map.
- */
- fun clear() {
- keyToValue.clear()
- valueToKey.clear()
- clearReferences()
- }
-
- /**
- * @return a list of values associated with the provided [keys].
- */
- operator fun get(keys: Iterable<K>): List<V> {
- clearReferences()
- val set = mutableSetOf<IdentityWeakReference<V>>()
- keys.forEach { key ->
- val weakKey = IdentityWeakReference(key)
- keyToValue[weakKey]?.let(set::addAll)
- }
- return set.mapNotNull { it.get() }
- }
-
- /**
- * @return a list of values associated with the provided [key]
- */
- fun getValueOf(key: K): List<V> {
- clearReferences()
- val weakKey = IdentityWeakReference(key)
- return keyToValue[weakKey]?.mapNotNull { it.get() }?.toList() ?: emptyList<V>()
- }
-
- /**
- * Clears all the values that match the given [predicate] from all the sets.
- */
- @Suppress("UNCHECKED_CAST")
- fun clearValues(predicate: (V) -> Boolean) {
- clearReferences()
- val matching = mutableListOf<V>()
- valueToKey.keys.forEach { value ->
- val v = value.get()
- if (v != null && predicate(v)) {
- matching.add(v)
- }
- }
- matching.forEach { removeValue(it) }
- }
-
- /**
- * Removes all values matching [value].
- */
- fun removeValue(value: V) {
- clearReferences()
- val weakValue = IdentityWeakReference(value)
- valueToKey.remove(weakValue)?.forEach { key ->
- val valueSet = keyToValue[key]!!
- valueSet.remove(weakValue)
- if (valueSet.isEmpty()) {
- keyToValue.remove(key)
- }
- }
- }
-
- private fun clearReferences() {
- pollQueue(keyQueue, keyToValue, valueToKey)
- pollQueue(valueQueue, valueToKey, keyToValue)
- }
-
- private fun <T, U> pollQueue(
- queue: ReferenceQueue<T>,
- keyMap: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
- valueMap: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>
- ) {
- do {
- val ref = queue.poll()
- if (ref != null) {
- @Suppress("UNCHECKED_CAST")
- val weakKey = ref as IdentityWeakReference<T>
- removeFromSet(keyMap, valueMap, weakKey)
- }
- } while (ref != null)
- }
-
- private fun <T, U> addToSet(
- map: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
- key: IdentityWeakReference<T>,
- value: IdentityWeakReference<U>
- ) {
- var set = map[key]
- if (set == null) {
- set = hashSetOf()
- map.put(key, set)
- }
- set.add(value)
- }
-
- private fun <T, U> removeFromSet(
- mapFromKey: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
- mapToKey: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>,
- key: IdentityWeakReference<T>
- ) {
- mapFromKey.remove(key)?.forEach { value ->
- mapToKey[value]?.remove(key)
- }
- }
-}
-
-private class IdentityWeakReference<T>(value: T, queue: ReferenceQueue<T>? = null) :
- WeakReference<T>(value, queue) {
- val hash = identityHashCode(value)
-
- override fun equals(other: Any?): Boolean {
- if (other !is IdentityWeakReference<*>) {
- return false
- }
- return hash == other.hash && get() === other.get()
- }
-
- override fun hashCode(): Int = hash
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 044bbab..e760c75 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -1177,7 +1177,7 @@
private fun RecomposeScope?.replacableWith(other: RecomposeScope) =
this == null || !this.valid || this == other || this.anchor == other.anchor
-@ComposeCompilerApi
+@OptIn(ComposeCompilerApi::class)
private typealias CLambda = ComposableLambda<Any, Any, Any, Any, Any, Any, Any, Any, Any, Any,
Any, Any, Any, Any, Any, Any, Any, Any, Any>
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
index 927fa46..830f020 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
@@ -45,10 +45,4 @@
}
}
-internal actual typealias Reference<T> = java.lang.ref.Reference<T>
-
-internal actual typealias ReferenceQueue<T> = java.lang.ref.ReferenceQueue<T>
-
-internal actual typealias WeakReference<T> = java.lang.ref.WeakReference<T>
-
internal actual typealias TestOnly = org.jetbrains.annotations.TestOnly
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
deleted file mode 100644
index 8828ff6..0000000
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.runtime
-
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class ObserverMapTests {
-
- private val node1 = 1
- private val node2 = 2
- private lateinit var map: ObserverMap<TestModel, Int>
-
- @BeforeTest
- fun setup() {
- map = ObserverMap()
- }
-
- @Test
- fun testMapContainsPreviouslyAddedModel() {
- val model = TestModel()
- map.add(model, node1)
-
- map.assertNodes(model, node1)
- }
-
- @Test
- fun testMapAssociateBothNodesWithTheModel() {
- val model = TestModel()
- map.add(model, node1)
- map.add(model, node2)
-
- map.assertNodes(model, node1, node2)
- }
-
- @Test
- fun testMapContainsModelWithChangedHashCode() {
- val model = TestModel("Original")
- map.add(model, node1)
- model.content = "Changed"
-
- map.assertNodes(model, node1)
- }
-
- @Test
- fun testMapRemovesTheModel() {
- val model = TestModel()
- map.add(model, node1)
- map.add(model, node2)
-
- map.remove(model)
-
- map.assertNodes(model)
- }
-
- @Test
- fun testMapRemovesTheNode() {
- val model = TestModel()
- map.add(model, node1)
- map.add(model, node2)
-
- map.remove(model, node1)
-
- map.assertNodes(model, node2)
- }
-
- @Test
- fun testMapClearsAllTheModels() {
- val model1 = TestModel("Test1")
- val model2 = TestModel("Test2")
- map.add(model1, node1)
- map.add(model2, node2)
-
- map.clear()
-
- map.assertNodes(model1)
- map.assertNodes(model2)
- }
-
- @Test
- fun testMapClearsTheValuesByPredicate() {
- val model1 = TestModel("Test1")
- val model2 = TestModel("Test2")
- val node3 = 3
- map.add(model1, node1)
- map.add(model2, node2)
- map.add(model2, node3)
-
- map.clearValues { it == node1 || it == node3 }
-
- map.assertNodes(model1)
- map.assertNodes(model2, node2)
- }
-
- @Test
- fun testGetForMultipleModels() {
- val model1 = TestModel("Test1")
- val model2 = TestModel("Test2")
- val model3 = TestModel("Test2")
- val node3 = 3
- val node4 = 4
- map.add(model1, node1)
- map.add(model1, node2)
- map.add(model2, node3)
- map.add(model3, node4)
-
- map.assertNodes(listOf(model1, model2, model3), node1, node2, node3, node4)
- }
-
- @Test
- fun testGetFiltersDuplicates() {
- val model1 = TestModel("Test1")
- val model2 = TestModel("Test2")
- map.add(model1, node1)
- map.add(model2, node1)
-
- map.assertNodes(listOf(model1, model2), node1)
- }
-
- @Test
- fun testRemoveValue() {
- val model1 = TestModel("Test1")
- val model2 = TestModel("Test2")
- map.add(model1, node1)
- map.add(model2, node1)
- map.add(model2, node2)
-
- map.removeValue(node1)
- map.assertNodes(listOf(model2), node2)
- }
-
- private data class TestModel(var content: String = "Test")
-
- private fun ObserverMap<TestModel, Int>.assertNodes(
- model: TestModel,
- vararg nodes: Int
- ) {
- assertNodes(listOf(model), *nodes)
- }
-
- private fun ObserverMap<TestModel, Int>.assertNodes(
- models: List<TestModel>,
- vararg nodes: Int
- ) {
- val expected = nodes.toList().sorted()
- val actual = get(models).sorted()
- assertEquals(expected, actual)
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 50d3a44..2b56325 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -885,6 +885,10 @@
public final class CanvasDrawScopeKt {
}
+ public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+ method public void drawContent();
+ }
+
public interface DrawContext {
method public androidx.compose.ui.graphics.Canvas getCanvas();
method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 50d3a44..2b56325 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -885,6 +885,10 @@
public final class CanvasDrawScopeKt {
}
+ public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+ method public void drawContent();
+ }
+
public interface DrawContext {
method public androidx.compose.ui.graphics.Canvas getCanvas();
method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index f7b2958..f6b1064 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -941,6 +941,10 @@
public final class CanvasDrawScopeKt {
}
+ public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+ method public void drawContent();
+ }
+
public interface DrawContext {
method public androidx.compose.ui.graphics.Canvas getCanvas();
method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
new file mode 100644
index 0000000..3ce1cd9
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.ui.graphics.drawscope
+
+/**
+ * Receiver scope for drawing content into a layout, where the content can
+ * be drawn between other canvas operations. If [drawContent] is not called,
+ * the contents of the layout will not be drawn.
+ */
+interface ContentDrawScope : DrawScope {
+ /**
+ * Causes child drawing operations to run during the `onPaint` lambda.
+ */
+ fun drawContent()
+}
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index cc099459..e6bda1c2 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -2,7 +2,7 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
package androidx.compose.ui.test.junit4.android {
+ public final class AndroidOwnerRegistryKt {
+ }
+
public final class ComposeIdlingResourceKt {
- method public static void registerComposeWithEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
- method public static void unregisterComposeFromEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void registerComposeWithEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void unregisterComposeFromEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
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 cc099459..e6bda1c2 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
@@ -2,7 +2,7 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
package androidx.compose.ui.test.junit4.android {
+ public final class AndroidOwnerRegistryKt {
+ }
+
public final class ComposeIdlingResourceKt {
- method public static void registerComposeWithEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
- method public static void unregisterComposeFromEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void registerComposeWithEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void unregisterComposeFromEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index cc099459..e6bda1c2 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -2,7 +2,7 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
package androidx.compose.ui.test.junit4.android {
+ public final class AndroidOwnerRegistryKt {
+ }
+
public final class ComposeIdlingResourceKt {
- method public static void registerComposeWithEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
- method public static void unregisterComposeFromEspresso();
- method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void registerComposeWithEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated public static void unregisterComposeFromEspresso();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
index 4a4d79e..2c038c3 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
@@ -26,16 +26,21 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
@LargeTest
class AndroidOwnerRegistryTest {
+ private val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+ private val androidOwnerRegistry = AndroidOwnerRegistry()
+
@get:Rule
- val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+ val testRule: RuleChain = RuleChain
+ .outerRule { base, _ -> androidOwnerRegistry.getStatementFor(base) }
+ .around(activityRule)
private val onRegistrationChangedListener =
object : AndroidOwnerRegistry.OnRegistrationChangedListener {
@@ -47,24 +52,14 @@
@Before
fun setUp() {
- assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
- AndroidOwnerRegistry.setupRegistry()
- AndroidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
- }
-
- @After
- fun tearDown() {
- AndroidOwnerRegistry.removeOnRegistrationChangedListener(onRegistrationChangedListener)
- AndroidOwnerRegistry.tearDownRegistry()
- assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+ androidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
}
@Test
fun registryIsSetUpAndEmpty() {
- assertThat(AndroidOwnerRegistry.isSetUp).isTrue()
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.isSetUp).isTrue()
+ assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
}
@Test
@@ -75,8 +70,8 @@
val owner = activity.findOwner()
// Then it is registered
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
- assertThat(AndroidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
+ assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
+ assertThat(androidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
// And our listener was notified
assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
listOf(Pair(owner, true))
@@ -95,8 +90,8 @@
activity.setContentView(View(activity))
// Then it is not registered now
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
- assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.getOwners()).isEmpty()
// But our listener was notified of addition and removal
assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
listOf(
@@ -115,11 +110,11 @@
val owner = activity.findOwner()
// When we tear down the registry
- AndroidOwnerRegistry.tearDownRegistry()
+ androidOwnerRegistry.tearDownRegistry()
// Then the registry is empty
- assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
- assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+ assertThat(androidOwnerRegistry.getOwners()).isEmpty()
// And our listener was notified of addition and removal
assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
listOf(
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 7bee9b6..0adcd1b 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -16,21 +16,25 @@
package androidx.compose.ui.test.junit4
-import android.os.Handler
import android.os.Looper
+import androidx.activity.ComponentActivity
import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.State
+import androidx.compose.runtime.dispatch.withFrameNanos
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -39,6 +43,12 @@
import androidx.test.espresso.Espresso.onIdle
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -51,14 +61,12 @@
private val rectSize = Size(50.0f, 50.0f)
}
- private val handler = Handler(Looper.getMainLooper())
-
private var animationRunning = false
private val recordedAnimatedValues = mutableListOf<Float>()
- private var hasRecomposed = false
@get:Rule
- val rule = createComposeRule()
+ val rule = createAndroidComposeRule<ComponentActivity>()
+ private val composeIdlingResource = rule.composeIdlingResource
/**
* High level test to only verify that [ComposeTestRule.runOnIdle] awaits animations.
@@ -110,82 +118,79 @@
* key moments during the animation kick-off process.
*/
@Test
+ @Ignore("b/173798666: Idleness not detected after Snapshot.sendApplyNotifications()")
fun testAnimationIdle_detailed() {
- var wasIdleAfterCommit = false
- var wasIdleAfterRecompose = false
var wasIdleBeforeKickOff = false
- var wasIdleBeforeCommit = false
+ var wasIdleBeforeApplySnapshot = false
+ var wasIdleAfterApplySnapshot = false
val animationState = mutableStateOf(AnimationStates.From)
- rule.setContent { Ui(animationState) }
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ Ui(animationState)
+ }
- rule.runOnIdle {
- // Record idleness after this frame is committed. The mutation we're about to make
- // will trigger a commit of the frame, which is posted at the front of the handler's
- // queue. By posting a message at the front of the queue here, it will be executed
- // right after the frame commit.
- handler.postAtFrontOfQueue {
- wasIdleAfterCommit = ComposeIdlingResource.isIdle()
- }
+ runBlocking(scope.coroutineContext) {
+ // Verify that we're on the main thread, which is important for isIdle() later
+ assertThat(Looper.myLooper()).isEqualTo(Looper.getMainLooper())
+ }
- // Record idleness after the next recomposition. Since we can't get a signal from the
- // recomposer, keep polling until we detect we have been recomposed.
- hasRecomposed = false
- handler.pollUntil({ hasRecomposed }) {
- wasIdleAfterRecompose = ComposeIdlingResource.isIdle()
- }
-
+ val wasIdleAfterRecompose = rule.runOnIdle {
// Record idleness before kickoff of animation
- wasIdleBeforeKickOff = ComposeIdlingResource.isIdle()
+ wasIdleBeforeKickOff = composeIdlingResource.isIdle()
// Kick off the animation
animationRunning = true
animationState.value = AnimationStates.To
- // Record idleness after kickoff of animation, but before the frame is committed
- wasIdleBeforeCommit = ComposeIdlingResource.isIdle()
+ // Record idleness after kickoff of animation, but before the snapshot is applied
+ wasIdleBeforeApplySnapshot = composeIdlingResource.isIdle()
+
+ // Apply the snapshot
+ @OptIn(ExperimentalComposeApi::class)
+ Snapshot.sendApplyNotifications()
+
+ // Record idleness after this snapshot is applied
+ wasIdleAfterApplySnapshot = composeIdlingResource.isIdle()
+
+ // Record idleness after the first recomposition
+ @OptIn(ExperimentalCoroutinesApi::class)
+ scope.async(start = CoroutineStart.UNDISPATCHED) {
+ // Await a single recomposition
+ withFrameNanos {}
+ composeIdlingResource.isIdle()
+ }
+ }.let {
+ runBlocking {
+ it.await()
+ }
}
- // Verify that animation is kicked off
- assertThat(animationRunning).isTrue()
// Wait until it is finished
- onIdle()
- // Verify it was finished
- assertThat(animationRunning).isFalse()
+ rule.runOnIdle {
+ // Verify it was finished
+ assertThat(animationRunning).isFalse()
- // Before the animation is kicked off, it is still idle
- assertThat(wasIdleBeforeKickOff).isTrue()
- // After animation is kicked off, but before the frame is committed, it must be busy
- assertThat(wasIdleBeforeCommit).isFalse()
- // After the frame is committed, it must still be busy
- assertThat(wasIdleAfterCommit).isFalse()
- // After recomposition, it must still be busy
- assertThat(wasIdleAfterRecompose).isFalse()
- }
-
- private fun Handler.pollUntil(condition: () -> Boolean, onDone: () -> Unit) {
- object : Runnable {
- override fun run() {
- if (condition()) {
- onDone()
- } else {
- this@pollUntil.post(this)
- }
- }
- }.run()
+ // Before the animation is kicked off, it is still idle
+ assertThat(wasIdleBeforeKickOff).isTrue()
+ // After animation is kicked off, but before the frame is committed, it must be busy
+ assertThat(wasIdleBeforeApplySnapshot).isFalse()
+ // After the frame is committed, it must still be busy
+ assertThat(wasIdleAfterApplySnapshot).isFalse()
+ // After recomposition, it must still be busy
+ assertThat(wasIdleAfterRecompose).isFalse()
+ }
}
@Composable
private fun Ui(animationState: State<AnimationStates>) {
- hasRecomposed = true
Box(modifier = Modifier.background(color = Color.Yellow).fillMaxSize()) {
- hasRecomposed = true
val state = transition(
definition = animationDefinition,
toState = animationState.value,
onStateChangeFinished = { animationRunning = false }
)
- hasRecomposed = true
Canvas(modifier = Modifier.fillMaxSize()) {
recordedAnimatedValues.add(state[x])
drawRect(Color.Cyan, Offset(state[x], 0f), rectSize)
@@ -217,4 +222,4 @@
x using snap()
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
index 77901b8..2e199ea 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
@@ -16,26 +16,43 @@
package androidx.compose.ui.test.junit4
+import androidx.activity.ComponentActivity
import androidx.compose.testutils.expectError
import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
import androidx.compose.ui.test.onNodeWithTag
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
@MediumTest
@RunWith(AndroidJUnit4::class)
class SynchronizationMethodsTest {
- val rule = createComposeRule()
+ // Note: don't add `@get:Rule` to avoid the Rule from being applied. Except for the
+ // AndroidOwnerRegistry, it doesn't need to be initialized in these tests.
+ private val rule = createAndroidComposeRule<ComponentActivity>()
+ private val androidOwnerRegistry = rule.composeIdlingResource.androidOwnerRegistry
+
+ @get:Rule
+ val registryRule: TestRule = RuleChain.outerRule { base, _ ->
+ androidOwnerRegistry.getStatementFor(base)
+ }
+
+ @Before
+ fun addMockResumedOwner() {
+ androidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
+ }
@Test
fun runOnUiThread() {
@@ -58,72 +75,50 @@
@Test
fun runOnIdle() {
- withAndroidOwnerRegistry {
- val result = rule.runOnIdle { "Hello" }
- assertThat(result).isEqualTo("Hello")
- }
+ val result = rule.runOnIdle { "Hello" }
+ assertThat(result).isEqualTo("Hello")
}
@Test
fun runOnIdle_void() {
- withAndroidOwnerRegistry {
- var called = false
- rule.runOnIdle { called = true }
- assertThat(called).isTrue()
- }
+ var called = false
+ rule.runOnIdle { called = true }
+ assertThat(called).isTrue()
}
@Test
fun runOnIdle_nullable() {
- withAndroidOwnerRegistry {
- val result: String? = rule.runOnIdle { null }
- assertThat(result).isEqualTo(null)
- }
+ val result: String? = rule.runOnIdle { null }
+ assertThat(result).isEqualTo(null)
}
@Test
fun runOnIdle_assert_fails() {
- withAndroidOwnerRegistry {
- rule.runOnIdle {
- expectError<IllegalStateException> {
- rule.onNodeWithTag("dummy").assertExists()
- }
+ rule.runOnIdle {
+ expectError<IllegalStateException> {
+ rule.onNodeWithTag("dummy").assertExists()
}
}
}
@Test
fun runOnIdle_waitForIdle_fails() {
- withAndroidOwnerRegistry {
- rule.runOnIdle {
- expectError<IllegalStateException> {
- rule.waitForIdle()
- }
+ rule.runOnIdle {
+ expectError<IllegalStateException> {
+ rule.waitForIdle()
}
}
}
@Test
fun runOnIdle_runOnIdle_fails() {
- withAndroidOwnerRegistry {
- rule.runOnIdle {
- expectError<IllegalStateException> {
- rule.runOnIdle {}
- }
+ rule.runOnIdle {
+ expectError<IllegalStateException> {
+ rule.runOnIdle {}
}
}
}
- private fun withAndroidOwnerRegistry(block: () -> Unit) {
- try {
- AndroidOwnerRegistry.setupRegistry()
- AndroidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
- block()
- } finally {
- AndroidOwnerRegistry.tearDownRegistry()
- }
- }
-
private fun mockResumedAndroidOwner(): AndroidOwner {
val lifecycle = mock<Lifecycle>()
doReturn(Lifecycle.State.RESUMED).whenever(lifecycle).currentState
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index f3ff936..a6fd566 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -39,7 +39,6 @@
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import androidx.test.espresso.Espresso.onIdle
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -67,6 +66,7 @@
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
private val clockTestRule = rule.clockTestRule
+ private val composeIdlingResource = rule.composeIdlingResource
/**
* Tests if advancing the clock manually works when the clock is paused, and that idleness is
@@ -87,7 +87,7 @@
animationState.value = AnimationStates.To
// Changes need to trickle down the animation system, so compose should be non-idle
- assertThat(ComposeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdle()).isFalse()
}
// Await recomposition
@@ -101,7 +101,7 @@
// Advance first half of the animation (.5 sec)
rule.runOnIdle {
clockTestRule.advanceClock(halfDuration)
- assertThat(ComposeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdle()).isFalse()
}
// Await next animation frame
@@ -115,7 +115,7 @@
// Advance second half of the animation (.5 sec)
rule.runOnIdle {
clockTestRule.advanceClock(halfDuration)
- assertThat(ComposeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdle()).isFalse()
}
// Await next animation frame
@@ -150,7 +150,7 @@
animationState.value = AnimationStates.To
// Changes need to trickle down the animation system, so compose should be non-idle
- assertThat(ComposeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdle()).isFalse()
}
// Perform a single recomposition by awaiting the same signal as the Recomposer
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
index 0f7da5e..c417e42 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
@@ -20,10 +20,8 @@
import androidx.compose.animation.core.rootAnimationClockFactory
import androidx.compose.ui.test.ExperimentalTesting
import androidx.compose.ui.test.TestAnimationClock
-import androidx.test.espresso.IdlingResource
-import androidx.compose.ui.test.junit4.android.registerTestClock
-import androidx.compose.ui.test.junit4.android.unregisterTestClock
import androidx.compose.ui.test.junit4.android.AndroidTestAnimationClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -31,30 +29,15 @@
/**
* A [TestRule] to monitor and take over the animation clock in the composition. It substitutes
* the ambient animation clock provided at the root of the composition tree with a
- * [TestAnimationClock] and registers it with [registerTestClock].
- *
- * Usually you don't need to create this rule by yourself, it is done for you in
- * [ComposeTestRule]. If you don't use [ComposeTestRule], use this rule in your test and make
- * sure it is run _before_ your activity is created.
- *
- * If your app provides a custom animation clock somewhere in your composition, make sure to have
- * it implement [TestAnimationClock] and register it with [registerTestClock]. Alternatively,
- * if you use Espresso you can create your own [IdlingResource] to let Espresso await your
- * animations. Otherwise, built in steps that make sure the UI is stable when performing actions
- * or assertions will fail to work.
+ * [TestAnimationClock].
*/
@ExperimentalTesting
-internal class AndroidAnimationClockTestRule : AnimationClockTestRule {
+internal class AndroidAnimationClockTestRule(
+ private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
/** Backing property for [clock] */
private val _clock = AndroidTestAnimationClock()
-
- /**
- * The ambient animation clock that is provided at the root of the composition tree.
- * Typically, apps will only use this clock. If your app provides another clock in the tree,
- * make sure to let it implement [TestAnimationClock] and register it with
- * [registerTestClock].
- */
override val clock: TestAnimationClock get() = _clock
override fun apply(base: Statement, description: Description?): Statement {
@@ -65,7 +48,7 @@
private inner class AnimationClockStatement(private val base: Statement) : Statement() {
override fun evaluate() {
val oldFactory = rootAnimationClockFactory
- registerTestClock(clock)
+ composeIdlingResource.registerTestClock(clock)
rootAnimationClockFactory = { clock }
try {
base.evaluate()
@@ -74,13 +57,20 @@
_clock.dispose()
} finally {
rootAnimationClockFactory = oldFactory
- unregisterTestClock(clock)
+ composeIdlingResource.unregisterTestClock(clock)
}
}
}
}
}
+@Deprecated(
+ message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+ "it from your ComposeTestRule instead",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
@ExperimentalTesting
+@Suppress("DocumentExceptions")
actual fun createAnimationClockRule(): AnimationClockTestRule =
- AndroidAnimationClockTestRule()
+ throw UnsupportedOperationException()
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 8f8fe80..7fcac78 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
@@ -17,6 +17,7 @@
package androidx.compose.ui.test.junit4
import androidx.activity.ComponentActivity
+import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.text.blinkingCursorEnabled
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Recomposer
@@ -27,11 +28,7 @@
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.FirstDrawRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
-import androidx.compose.ui.test.junit4.android.registerComposeWithEspresso
-import androidx.compose.ui.test.junit4.android.unregisterComposeFromEspresso
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.input.textInputServiceFactory
import androidx.compose.ui.unit.Density
@@ -141,18 +138,20 @@
activityProvider: (R) -> A
) : this(activityRule, activityProvider, false)
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val composeIdlingResource = ComposeIdlingResource()
+
@ExperimentalTesting
override val clockTestRule: AnimationClockTestRule =
if (!driveClockByMonotonicFrameClock) {
- AndroidAnimationClockTestRule()
+ AndroidAnimationClockTestRule(composeIdlingResource)
} else {
- MonotonicFrameClockTestRule()
+ MonotonicFrameClockTestRule(composeIdlingResource)
}
internal var disposeContentHook: (() -> Unit)? = null
- private val idleAwaiter = IdleAwaiter()
- private val testOwner = AndroidTestOwner(idleAwaiter)
+ private val testOwner = AndroidTestOwner(composeIdlingResource)
private val testContext = createTestContext(testOwner)
private var activity: A? = null
@@ -179,7 +178,8 @@
@Suppress("NAME_SHADOWING")
@OptIn(ExperimentalTesting::class)
return RuleChain
- .outerRule(clockTestRule)
+ .outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
+ .around(clockTestRule)
.around { base, _ -> AndroidComposeStatement(base) }
.around(activityRule)
.apply(base, description)
@@ -216,12 +216,12 @@
}
override fun waitForIdle() {
- idleAwaiter.waitForIdle()
+ composeIdlingResource.waitForIdle()
}
@ExperimentalTesting
override suspend fun awaitIdle() {
- idleAwaiter.awaitIdle()
+ composeIdlingResource.awaitIdle()
}
override fun <T> runOnUiThread(action: () -> T): T {
@@ -242,54 +242,38 @@
override fun evaluate() {
@Suppress("DEPRECATION_ERROR")
val oldTextInputFactory = textInputServiceFactory
- beforeEvaluate()
try {
+ @Suppress("DEPRECATION_ERROR")
+ blinkingCursorEnabled = false
+ @Suppress("DEPRECATION_ERROR")
+ textInputServiceFactory = {
+ TextInputServiceForTests(it)
+ }
base.evaluate()
} finally {
- afterEvaluate()
+ @Suppress("DEPRECATION_ERROR")
+ blinkingCursorEnabled = true
@Suppress("DEPRECATION_ERROR")
textInputServiceFactory = oldTextInputFactory
- }
- }
-
- @OptIn(InternalTextApi::class)
- private fun beforeEvaluate() {
- @Suppress("DEPRECATION_ERROR")
- blinkingCursorEnabled = false
- AndroidOwnerRegistry.setupRegistry()
- FirstDrawRegistry.setupRegistry()
- registerComposeWithEspresso()
- @Suppress("DEPRECATION_ERROR")
- textInputServiceFactory = {
- TextInputServiceForTests(it)
- }
- }
-
- @OptIn(InternalTextApi::class)
- private fun afterEvaluate() {
- @Suppress("DEPRECATION_ERROR")
- blinkingCursorEnabled = true
- AndroidOwnerRegistry.tearDownRegistry()
- FirstDrawRegistry.tearDownRegistry()
- unregisterComposeFromEspresso()
- // Dispose the content
- if (disposeContentHook != null) {
- runOnUiThread {
- // NOTE: currently, calling dispose after an exception that happened during
- // composition is not a safe call. Compose runtime should fix this, and then
- // this call will be okay. At the moment, however, calling this could
- // itself produce an exception which will then obscure the original
- // exception. To fix this, we will just wrap this call in a try/catch of
- // its own
- try {
- disposeContentHook!!()
- } catch (e: Exception) {
- // ignore
+ // Dispose the content
+ if (disposeContentHook != null) {
+ runOnUiThread {
+ // NOTE: currently, calling dispose after an exception that happened during
+ // composition is not a safe call. Compose runtime should fix this, and then
+ // this call will be okay. At the moment, however, calling this could
+ // itself produce an exception which will then obscure the original
+ // exception. To fix this, we will just wrap this call in a try/catch of
+ // its own
+ try {
+ disposeContentHook!!()
+ } catch (e: Exception) {
+ // ignore
+ }
+ disposeContentHook = null
}
- disposeContentHook = null
}
+ activity = null
}
- activity = null
}
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index 19e890a..404b507 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -23,13 +23,14 @@
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.InternalTestingApi
import androidx.compose.ui.test.TestOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import androidx.compose.ui.text.input.EditOperation
import androidx.compose.ui.text.input.ImeAction
@OptIn(InternalTestingApi::class)
-internal class AndroidTestOwner(private val idleAwaiter: IdleAwaiter) : TestOwner {
+internal class AndroidTestOwner(
+ private val composeIdlingResource: ComposeIdlingResource
+) : TestOwner {
@SuppressLint("DocumentExceptions")
override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
@@ -65,16 +66,7 @@
}
override fun getOwners(): Set<Owner> {
- // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
- // structure. In case of multiple AndroidOwners, add a fake root
- idleAwaiter.waitForIdle()
-
- return AndroidOwnerRegistry.getOwners().also {
- // TODO(b/153632210): This check should be done by callers of collectOwners
- check(it.isNotEmpty()) {
- "No compose views found in the app. Is your Activity resumed?"
- }
- }
+ return composeIdlingResource.getOwners()
}
private fun AndroidOwner.getTextInputServiceOrDie(): TextInputServiceForTests {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
index ac434d4..8aa2f09 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
@@ -22,14 +22,15 @@
import androidx.compose.animation.core.rootAnimationClockFactory
import androidx.compose.ui.test.ExperimentalTesting
import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.registerTestClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.unregisterTestClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import kotlinx.coroutines.CoroutineScope
import org.junit.runner.Description
import org.junit.runners.model.Statement
@ExperimentalTesting
-internal class MonotonicFrameClockTestRule : AnimationClockTestRule {
+internal class MonotonicFrameClockTestRule(
+ private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
private lateinit var _clock: InternalClock
override val clock: TestAnimationClock get() = _clock
@@ -41,7 +42,7 @@
private fun getOrCreateClock(scope: CoroutineScope): TestAnimationClock {
if (!this::_clock.isInitialized) {
_clock = InternalClock(MonotonicFrameAnimationClock(scope))
- registerTestClock(_clock)
+ composeIdlingResource.registerTestClock(_clock)
}
return _clock
}
@@ -55,7 +56,7 @@
base.evaluate()
} finally {
rootAnimationClockFactory = oldFactory
- unregisterTestClock(clock)
+ composeIdlingResource.unregisterTestClock(clock)
}
}
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index ae51818..0138a73 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -17,16 +17,25 @@
package androidx.compose.ui.test.junit4.android
import android.view.View
+import androidx.annotation.VisibleForTesting
import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.test.ExperimentalTesting
import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.junit.runners.model.Statement
import java.util.Collections
import java.util.WeakHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.resume
+import kotlin.time.ExperimentalTime
/**
* Registry where all [AndroidOwner]s should be registered while they are attached to the window.
* This registry is used by the testing library to query the owners's state.
*/
-internal object AndroidOwnerRegistry {
+internal class AndroidOwnerRegistry {
private val owners = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
private val registryListeners = mutableSetOf<OnRegistrationChangedListener>()
@@ -39,13 +48,14 @@
/**
* Sets up this registry to be notified of any [AndroidOwner] created
*/
- internal fun setupRegistry() {
+ private fun setupRegistry() {
AndroidOwner.onAndroidOwnerCreatedCallback = ::onAndroidOwnerCreated
}
/**
* Cleans up the changes made by [setupRegistry]. Call this after your test has run.
*/
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun tearDownRegistry() {
AndroidOwner.onAndroidOwnerCreatedCallback = null
synchronized(owners) {
@@ -124,6 +134,19 @@
}
}
+ fun getStatementFor(base: Statement): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ try {
+ setupRegistry()
+ base.evaluate()
+ } finally {
+ tearDownRegistry()
+ }
+ }
+ }
+ }
+
/**
* Interface to be implemented by components that want to be notified when an [AndroidOwner]
* registers or unregisters at this registry.
@@ -132,7 +155,7 @@
fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean)
}
- private class OwnerAttachedListener(
+ private inner class OwnerAttachedListener(
private val owner: AndroidOwner
) : View.OnAttachStateChangeListener {
@@ -147,4 +170,75 @@
unregisterOwner(owner)
}
}
-}
\ No newline at end of file
+}
+
+private val AndroidOwnerRegistry.hasAndroidOwners: Boolean get() = getOwners().isNotEmpty()
+
+private fun AndroidOwnerRegistry.ensureAndroidOwnerRegistryIsSetUp() {
+ check(isSetUp) {
+ "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
+ "with composables"
+ }
+}
+
+internal fun AndroidOwnerRegistry.waitForAndroidOwners() {
+ ensureAndroidOwnerRegistryIsSetUp()
+
+ if (!hasAndroidOwners) {
+ val latch = CountDownLatch(1)
+ val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+ override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+ if (hasAndroidOwners) {
+ latch.countDown()
+ }
+ }
+ }
+ try {
+ addOnRegistrationChangedListener(listener)
+ if (!hasAndroidOwners) {
+ latch.await(2, TimeUnit.SECONDS)
+ }
+ } finally {
+ removeOnRegistrationChangedListener(listener)
+ }
+ }
+}
+
+@ExperimentalTesting
+@OptIn(ExperimentalTime::class)
+internal suspend fun AndroidOwnerRegistry.awaitAndroidOwners() {
+ ensureAndroidOwnerRegistryIsSetUp()
+
+ if (!hasAndroidOwners) {
+ suspendCancellableCoroutine<Unit> { continuation ->
+ // Make sure we only resume once
+ val didResume = AtomicBoolean(false)
+ fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
+ if (didResume.compareAndSet(false, true)) {
+ removeOnRegistrationChangedListener(listener)
+ continuation.resume(Unit)
+ }
+ }
+
+ // Usually we resume if an AndroidOwner is registered while the listener is added
+ val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+ override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+ if (hasAndroidOwners) {
+ resume(this)
+ }
+ }
+ }
+
+ addOnRegistrationChangedListener(listener)
+ continuation.invokeOnCancellation {
+ removeOnRegistrationChangedListener(listener)
+ }
+
+ // Sometimes the AndroidOwner was registered before we added
+ // the listener, in which case we missed our signal
+ if (hasAndroidOwners) {
+ resume(listener)
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index d792d7c..ee8879a 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -18,15 +18,24 @@
import android.os.Handler
import android.os.Looper
+import androidx.annotation.VisibleForTesting
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.test.ExperimentalTesting
+import androidx.compose.ui.test.TestAnimationClock
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.isOnUiThread
+import androidx.compose.ui.test.junit4.runOnUiThread
+import androidx.test.espresso.AppNotIdleException
+import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
-import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.runOnUiThread
-import java.util.concurrent.atomic.AtomicBoolean
+import androidx.test.espresso.IdlingResourceTimeoutException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.junit.runners.model.Statement
import java.util.concurrent.atomic.AtomicInteger
/**
@@ -46,48 +55,79 @@
* [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
* wait for Compose being idle you can use this function.
*/
-fun registerComposeWithEspresso() {
- ComposeIdlingResource.registerSelfIntoEspresso()
- FirstDrawIdlingResource.registerSelfIntoEspresso()
-}
+@Deprecated(
+ message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+ "createAndroidComposeRule() and registration will happen when needed",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun registerComposeWithEspresso(): Unit = throw UnsupportedOperationException(
+ "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
/**
* Unregisters resource registered as part of [registerComposeWithEspresso].
*/
-fun unregisterComposeFromEspresso() {
- ComposeIdlingResource.unregisterSelfFromEspresso()
- FirstDrawIdlingResource.unregisterSelfFromEspresso()
-}
+@Deprecated(
+ message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+ "createAndroidComposeRule() and registration will happen when needed",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun unregisterComposeFromEspresso(): Unit = throw UnsupportedOperationException(
+ "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
/**
* Registers the given [clock] so Espresso can await the animations subscribed to that clock.
*/
+@Deprecated(
+ message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+ "member function ComposeIdlingResource.registerTestClock(clock) instead",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("composeIdlingResource.registerTestClock(clock)")
+)
@ExperimentalTesting
-fun registerTestClock(clock: TestAnimationClock) {
- ComposeIdlingResource.registerTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun registerTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+ "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+ "directly on an instance of ComposeIdlingResource instead"
+)
/**
* Unregisters the [clock] that was registered with [registerTestClock].
*/
+@Deprecated(
+ message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+ "member function ComposeIdlingResource.unregisterTestClock(clock) instead",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("composeIdlingResource.unregisterTestClock(clock)")
+)
@ExperimentalTesting
-fun unregisterTestClock(clock: TestAnimationClock) {
- ComposeIdlingResource.unregisterTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun unregisterTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+ "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+ "directly on an instance of ComposeIdlingResource instead"
+)
/**
* Provides an idle check to be registered into Espresso.
*
* This makes sure that Espresso is able to wait for any pending changes in Compose. This
* resource is automatically registered when any compose testing APIs are used including
- * [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
- * wait for Compose being idle you can register this yourself via [registerSelfIntoEspresso].
+ * [createAndroidComposeRule].
*/
-internal object ComposeIdlingResource : BaseIdlingResource(), IdlingResourceWithDiagnostics {
+internal class ComposeIdlingResource : IdlingResource, IdlingResourceWithDiagnostics {
override fun getName(): String = "ComposeIdlingResource"
private var isIdleCheckScheduled = false
+ private var resourceCallback: IdlingResource.ResourceCallback? = null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val androidOwnerRegistry = AndroidOwnerRegistry()
@OptIn(ExperimentalTesting::class)
private val clocks = mutableSetOf<TestAnimationClock>()
@@ -98,6 +138,9 @@
private var hadNoSnapshotChanges = true
private var hadNoRecomposerChanges = true
private var lastCompositionAwaiters = 0
+ private var hadNoPendingMeasureLayout = true
+ // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+// private var hadNoPendingDraw = true
private var compositionAwaiters = AtomicInteger(0)
@@ -112,13 +155,25 @@
hadNoRecomposerChanges = !Recomposer.current().hasInvalidations()
hadAnimationClocksIdle = areAllClocksIdle()
lastCompositionAwaiters = compositionAwaiters.get()
+ val owners = androidOwnerRegistry.getUnfilteredOwners()
+ hadNoPendingMeasureLayout = !owners.any { it.hasPendingMeasureOrLayout }
+ // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+// hadNoPendingDraw = !owners.any {
+// val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
+// it.view.isDirty && (hasContent || it.view.isLayoutRequested)
+// }
check(lastCompositionAwaiters >= 0) {
"More CompositionAwaiters were removed then added ($lastCompositionAwaiters)"
}
- hadNoSnapshotChanges && hadNoRecomposerChanges && hadAnimationClocksIdle &&
- lastCompositionAwaiters == 0
+ hadNoSnapshotChanges &&
+ hadNoRecomposerChanges &&
+ hadAnimationClocksIdle &&
+ lastCompositionAwaiters == 0 &&
+ // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+ hadNoPendingMeasureLayout /*&&
+ hadNoPendingDraw*/
}
}
@@ -151,6 +206,14 @@
}
}
+ private fun transitionToIdle() {
+ resourceCallback?.onTransitionToIdle()
+ }
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+ resourceCallback = callback
+ }
+
/**
* Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] should report
* busy to Espresso while that [CompositionAwaiter] is checking idleness.
@@ -168,14 +231,14 @@
}
@OptIn(ExperimentalTesting::class)
- internal fun registerTestClock(clock: TestAnimationClock) {
+ fun registerTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.add(clock)
}
}
@OptIn(ExperimentalTesting::class)
- internal fun unregisterTestClock(clock: TestAnimationClock) {
+ fun unregisterTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.remove(clock)
}
@@ -194,6 +257,9 @@
val hadRunningAnimations = !hadAnimationClocksIdle
val numCompositionAwaiters = lastCompositionAwaiters
val wasAwaitingCompositions = numCompositionAwaiters > 0
+ val hadPendingMeasureLayout = !hadNoPendingMeasureLayout
+ // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+// val hadPendingDraw = !hadNoPendingDraw
val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges &&
!hadRunningAnimations && !wasAwaitingCompositions
@@ -217,60 +283,159 @@
" infinite re-compositions happening in the tested code.\n"
message += "- Debug: hadRecomposerChanges = $hadRecomposerChanges, "
message += "hadSnapshotChanges = $hadSnapshotChanges, "
- message += "numCompositionAwaiters = $numCompositionAwaiters"
+ message += "numCompositionAwaiters = $numCompositionAwaiters, "
+ message += "hadPendingMeasureLayout = $hadPendingMeasureLayout"
+ // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+// message += ", hadPendingDraw = $hadPendingDraw"
}
return message
}
-}
-private object FirstDrawIdlingResource : BaseIdlingResource() {
- override fun getName(): String = "FirstDrawIdlingResource"
+ fun waitForIdle() {
+ check(!isOnUiThread()) {
+ "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
+ "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
+ "from the main thread. Did you nest such a function inside " +
+ "runOnIdle {}, runOnUiThread {} or setContent {}?"
+ }
- override fun isIdleNow(): Boolean {
- return FirstDrawRegistry.haveAllDrawn().also {
- if (!it) {
- FirstDrawRegistry.setOnDrawnCallback(::transitionToIdle)
+ // First wait until we have an AndroidOwner (in case an Activity is being started)
+ androidOwnerRegistry.waitForAndroidOwners()
+ // Then await composition(s)
+ runEspressoOnIdle()
+
+ // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
+ // Activity that is about to be paused, in cases where a new Activity is being started.
+ // That means that AndroidOwnerRegistry.getOwners() may still return an empty list
+ // between now and when the new Activity has created its AndroidOwner, even though
+ // waitForAndroidOwners() suggests that we are now guaranteed one.
+ }
+
+ @ExperimentalTesting
+ suspend fun awaitIdle() {
+ // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
+ // runEspressoOnIdle() and replace it with a suspend fun that loops while the
+ // snapshot or the recomposer has pending changes, clocks are busy or owners have
+ // pending measures or layouts; and do the await on AndroidUiDispatcher.Main
+ // We use Espresso to wait for composition, measure, layout and draw,
+ // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
+ withContext(Dispatchers.IO) {
+ // First wait until we have an AndroidOwner (in case an Activity is being started)
+ androidOwnerRegistry.awaitAndroidOwners()
+ // Then await composition(s)
+ runEspressoOnIdle()
+ }
+ }
+
+ fun getOwners(): Set<Owner> {
+ // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
+ // structure. In case of multiple AndroidOwners, add a fake root
+ waitForIdle()
+
+ return androidOwnerRegistry.getOwners().also {
+ // TODO(b/153632210): This check should be done by callers of collectOwners
+ check(it.isNotEmpty()) {
+ "No compose views found in the app. Is your Activity resumed?"
}
}
}
- override fun unregisterSelfFromEspresso() {
- super.unregisterSelfFromEspresso()
- FirstDrawRegistry.setOnDrawnCallback(null)
+ fun getStatementFor(base: Statement): Statement {
+ return androidOwnerRegistry.getStatementFor(
+ object : Statement() {
+ override fun evaluate() {
+ try {
+ IdlingRegistry.getInstance().register(this@ComposeIdlingResource)
+ base.evaluate()
+ } finally {
+ IdlingRegistry.getInstance().unregister(this@ComposeIdlingResource)
+ }
+ }
+ }
+ )
}
}
-internal sealed class BaseIdlingResource : IdlingResource {
- private val isRegistered = AtomicBoolean(false)
- private var resourceCallback: IdlingResource.ResourceCallback? = null
+// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
+// and blocking await Espresso.onIdle().
+internal fun ComposeIdlingResource.runEspressoOnIdle() {
+ val compositionAwaiter = CompositionAwaiter(this)
+ try {
+ compositionAwaiter.start()
+ Espresso.onIdle()
+ } catch (e: Throwable) {
+ compositionAwaiter.cancel()
- final override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
- resourceCallback = callback
- }
-
- protected fun transitionToIdle() {
- resourceCallback?.onTransitionToIdle()
- }
-
- /**
- * Registers this resource into Espresso.
- *
- * Can be called multiple times.
- */
- internal fun registerSelfIntoEspresso() {
- if (isRegistered.compareAndSet(false, true)) {
- IdlingRegistry.getInstance().register(this)
+ // Happens on the global time out, usually when global idling time out is less
+ // or equal to dynamic idling time out or when the timeout is not due to individual
+ // idling resource. This does not necessary mean that it can't be due to idling
+ // resource being busy. So we try to check if it failed due to compose being busy and
+ // add some extra information to the developer.
+ val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
+ if (appNotIdleMaybe != null) {
+ rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
}
- }
- /**
- * Unregisters this resource from Espresso.
- *
- * Can be called multiple times.
- */
- internal open fun unregisterSelfFromEspresso() {
- if (isRegistered.compareAndSet(true, false)) {
- IdlingRegistry.getInstance().unregister(this)
+ // Happens on idling resource taking too long. Espresso gives out which resources caused
+ // it but it won't allow us to give any extra information. So we check if it was our
+ // resource and give more info if we can.
+ val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
+ if (resourceNotIdleMaybe != null) {
+ rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
}
+
+ // No match, rethrow
+ throw e
}
}
+
+private fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
+ var diagnosticInfo = ""
+ val listOfIdlingResources = mutableListOf<String>()
+ IdlingRegistry.getInstance().resources.forEach { resource ->
+ if (resource is IdlingResourceWithDiagnostics) {
+ val message = resource.getDiagnosticMessageIfBusy()
+ if (message != null) {
+ diagnosticInfo += "$message \n"
+ }
+ }
+ listOfIdlingResources.add(resource.name)
+ }
+ if (diagnosticInfo.isNotEmpty()) {
+ val prefix = if (wasGlobalTimeout) {
+ "Global time out"
+ } else {
+ "Idling resource timed out"
+ }
+ throw ComposeNotIdleException(
+ "$prefix: possibly due to compose being busy.\n" +
+ diagnosticInfo +
+ "All registered idling resources: " +
+ listOfIdlingResources.joinToString(", "),
+ e
+ )
+ }
+ // No extra info, re-throw the original exception
+ throw e
+}
+
+/**
+ * Tries to find if the given exception or any of its cause is of the type of the provided
+ * throwable T. Returns null if there is no match. This is required as some exceptions end up
+ * wrapped in Runtime or Concurrent exceptions.
+ */
+private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
+ var causeToCheck: Throwable? = e
+ while (causeToCheck != null) {
+ if (causeToCheck is T) {
+ return causeToCheck
+ }
+ causeToCheck = causeToCheck.cause
+ }
+ return null
+}
+
+/**
+ * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
+ */
+class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
index af9d0553a..dd380e9 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
@@ -24,7 +24,7 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.test.junit4.runOnUiThread
-internal class CompositionAwaiter {
+internal class CompositionAwaiter(private val composeIdlingResource: ComposeIdlingResource) {
private enum class State {
Initialized, Running, Finished, Cancelled
@@ -62,11 +62,11 @@
}
private fun startIdlingResource() {
- ComposeIdlingResource.addCompositionAwaiter()
+ composeIdlingResource.addCompositionAwaiter()
}
private fun stopIdlingResource() {
- ComposeIdlingResource.removeCompositionAwaiter()
+ composeIdlingResource.removeCompositionAwaiter()
}
/**
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
deleted file mode 100644
index 4a81e55..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
+++ /dev/null
@@ -1,110 +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.ui.test.junit4.android
-
-import android.view.ViewTreeObserver
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.lifecycle.Lifecycle
-import java.util.Collections
-import java.util.WeakHashMap
-
-internal object FirstDrawRegistry {
- private val notYetDrawn = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
- private var onDrawnCallback: (() -> Unit)? = null
-
- private val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
- if (registered) {
- notYetDrawn.add(owner)
- owner.view.viewTreeObserver.addOnDrawListener(FirstDrawListener(owner))
- } else {
- notYetDrawn.remove(owner)
- dispatchOnDrawn()
- }
- }
- }
-
- /**
- * Sets up this registry to listen to the [AndroidOwnerRegistry]. Call this method before
- * [AndroidOwner]s are registered into that registry, which is before the test activity is
- * created, or simply right after the [AndroidOwnerRegistry] is setup.
- */
- internal fun setupRegistry() {
- AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
- }
-
- /**
- * Cleans up the changes made by [setupRegistry]. Call this after your test has run.
- */
- internal fun tearDownRegistry() {
- AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
- }
-
- /**
- * Returns if all registered owners have finished at least one draw call.
- */
- fun haveAllDrawn(): Boolean {
- return notYetDrawn.all {
- val viewTreeOwners = it.viewTreeOwners
- viewTreeOwners == null ||
- viewTreeOwners.lifecycleOwner.lifecycle.currentState != Lifecycle.State.RESUMED
- }
- }
-
- /**
- * Adds a [callback] to be called when all registered [AndroidOwner]s have drawn at least
- * once. The callback will be removed after it is called.
- */
- fun setOnDrawnCallback(callback: (() -> Unit)?) {
- onDrawnCallback = callback
- }
-
- /**
- * Should be called when a registered owner has drawn for the first time. Can be called after
- * subsequent draws as well, but that is not required.
- */
- private fun notifyOwnerDrawn(owner: AndroidOwner) {
- notYetDrawn.remove(owner)
- dispatchOnDrawn()
- }
-
- private fun dispatchOnDrawn() {
- if (haveAllDrawn()) {
- onDrawnCallback?.invoke()
- onDrawnCallback = null
- }
- }
-
- private class FirstDrawListener(private val owner: AndroidOwner) :
- ViewTreeObserver.OnDrawListener {
- private var invoked = false
-
- override fun onDraw() {
- if (!invoked) {
- invoked = true
- owner.view.post {
- // The view was drawn
- notifyOwnerDrawn(owner)
- val viewTreeObserver = owner.view.viewTreeObserver
- if (viewTreeObserver.isAlive) {
- viewTreeObserver.removeOnDrawListener(this)
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
deleted file mode 100644
index 959aa19..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
+++ /dev/null
@@ -1,231 +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.ui.test.junit4.android
-
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.InternalTestingApi
-import androidx.compose.ui.test.junit4.isOnUiThread
-import androidx.test.espresso.AppNotIdleException
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.IdlingResourceTimeoutException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.coroutines.resume
-import kotlin.time.ExperimentalTime
-
-@OptIn(InternalTestingApi::class)
-internal class IdleAwaiter {
-
- fun waitForIdle() {
- check(!isOnUiThread()) {
- "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
- "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
- "from the main thread. Did you nest such a function inside " +
- "runOnIdle {}, runOnUiThread {} or setContent {}?"
- }
-
- // First wait until we have an AndroidOwner (in case an Activity is being started)
- waitForAndroidOwners()
- // Then await composition(s)
- runEspressoOnIdle()
-
- // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
- // Activity that is about to be paused, in cases where a new Activity is being started.
- // That means that AndroidOwnerRegistry.getOwners() may still return an empty list
- // between now and when the new Activity has created its AndroidOwner, even though
- // waitForAndroidOwners() suggests that we are now guaranteed one.
- }
-
- @ExperimentalTesting
- suspend fun awaitIdle() {
- // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
- // runEspressoOnIdle() and replace it with a suspend fun that loops while the
- // snapshot or the recomposer has pending changes, clocks are busy or owners have
- // pending measures or layouts; and do the await on AndroidUiDispatcher.Main
- // We use Espresso to wait for composition, measure, layout and draw,
- // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
- withContext(Dispatchers.IO) {
- // First wait until we have an AndroidOwner (in case an Activity is being started)
- awaitAndroidOwners()
- // Then await composition(s)
- runEspressoOnIdle()
- }
- }
-
- // TODO(168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
- // and blocking await Espresso.onIdle().
- private fun runEspressoOnIdle() {
- fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
- var diagnosticInfo = ""
- val listOfIdlingResources = mutableListOf<String>()
- IdlingRegistry.getInstance().resources.forEach { resource ->
- if (resource is IdlingResourceWithDiagnostics) {
- val message = resource.getDiagnosticMessageIfBusy()
- if (message != null) {
- diagnosticInfo += "$message \n"
- }
- }
- listOfIdlingResources.add(resource.name)
- }
- if (diagnosticInfo.isNotEmpty()) {
- val prefix = if (wasGlobalTimeout) {
- "Global time out"
- } else {
- "Idling resource timed out"
- }
- throw ComposeNotIdleException(
- "$prefix: possibly due to compose being busy.\n" +
- diagnosticInfo +
- "All registered idling resources: " +
- listOfIdlingResources.joinToString(", "),
- e
- )
- }
- // No extra info, re-throw the original exception
- throw e
- }
-
- val compositionAwaiter = CompositionAwaiter()
- try {
- compositionAwaiter.start()
- Espresso.onIdle()
- } catch (e: Throwable) {
- compositionAwaiter.cancel()
-
- // Happens on the global time out, usually when global idling time out is less
- // or equal to dynamic idling time out or when the timeout is not due to individual
- // idling resource. This does not necessary mean that it can't be due to idling
- // resource being busy. So we try to check if it failed due to compose being busy and
- // add some extra information to the developer.
- val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
- if (appNotIdleMaybe != null) {
- rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
- }
-
- // Happens on idling resource taking too long. Espresso gives out which resources caused
- // it but it won't allow us to give any extra information. So we check if it was our
- // resource and give more info if we can.
- val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
- if (resourceNotIdleMaybe != null) {
- rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
- }
-
- // No match, rethrow
- throw e
- }
- }
-
- /**
- * Tries to find if the given exception or any of its cause is of the type of the provided
- * throwable T. Returns null if there is no match. This is required as some exceptions end up
- * wrapped in Runtime or Concurrent exceptions.
- */
- private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
- var causeToCheck: Throwable? = e
- while (causeToCheck != null) {
- if (causeToCheck is T) {
- return causeToCheck
- }
- causeToCheck = causeToCheck.cause
- }
- return null
- }
-
- private fun ensureAndroidOwnerRegistryIsSetUp() {
- check(AndroidOwnerRegistry.isSetUp) {
- "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
- "with composables"
- }
- }
-
- private fun waitForAndroidOwners() {
- ensureAndroidOwnerRegistryIsSetUp()
-
- fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
- if (!hasAndroidOwners()) {
- val latch = CountDownLatch(1)
- val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
- if (hasAndroidOwners()) {
- latch.countDown()
- }
- }
- }
- try {
- AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
- if (!hasAndroidOwners()) {
- latch.await(2, TimeUnit.SECONDS)
- }
- } finally {
- AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
- }
- }
- }
-
- @ExperimentalTesting
- @OptIn(ExperimentalTime::class)
- private suspend fun awaitAndroidOwners() {
- ensureAndroidOwnerRegistryIsSetUp()
-
- fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
- if (!hasAndroidOwners()) {
- suspendCancellableCoroutine<Unit> { continuation ->
- // Make sure we only resume once
- val didResume = AtomicBoolean(false)
- fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
- if (didResume.compareAndSet(false, true)) {
- AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
- continuation.resume(Unit)
- }
- }
-
- // Usually we resume if an AndroidOwner is registered while the listener is added
- val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
- if (hasAndroidOwners()) {
- resume(this)
- }
- }
- }
-
- AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
- continuation.invokeOnCancellation {
- AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
- }
-
- // Sometimes the AndroidOwner was registered before we added
- // the listener, in which case we missed our signal
- if (hasAndroidOwners()) {
- resume(listener)
- }
- }
- }
- }
-}
-
-/**
- * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
- */
-class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
index bacfaf2..20c2fb6 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
@@ -89,6 +89,13 @@
}
}
+@Deprecated(
+ message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+ "it from your ComposeTestRule instead",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
@ExperimentalTesting
+@Suppress("DocumentExceptions")
actual fun createAnimationClockRule(): AnimationClockTestRule =
- DesktopAnimationClockTestRule()
\ No newline at end of file
+ throw UnsupportedOperationException()
\ No newline at end of file
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 37b9c58..282ac47 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
@@ -32,7 +32,6 @@
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.TestOwner
import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.initCompose
import androidx.compose.ui.text.input.EditOperation
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.Density
@@ -53,10 +52,6 @@
class DesktopComposeTestRule : ComposeTestRule {
companion object {
- init {
- initCompose()
- }
-
var current: DesktopComposeTestRule? = null
}
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
index be5d66c..5a02b9c 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
@@ -15,7 +15,6 @@
*/
package androidx.compose.ui.test.junit4
-import androidx.compose.ui.test.initCompose
import org.jetbrains.skija.Surface
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -168,12 +167,6 @@
private lateinit var testIdentifier: String
private lateinit var album: SkijaTestAlbum
- companion object {
- init {
- initCompose()
- }
- }
-
val executionQueue = LinkedList<() -> Unit>()
override fun apply(base: Statement, description: Description?): Statement {
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
index 08c2c7c..3fbde5c 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
@@ -48,5 +48,11 @@
fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
}
+@Deprecated(
+ message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+ "it from your ComposeTestRule instead",
+ level = DeprecationLevel.ERROR,
+ replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
@ExperimentalTesting
-expect fun createAnimationClockRule(): AnimationClockTestRule
\ No newline at end of file
+expect fun createAnimationClockRule(): AnimationClockTestRule
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 a708660..f94df4a 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
@@ -34,7 +34,7 @@
import kotlinx.coroutines.withContext
import kotlin.math.max
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
require(owner is AndroidOwner) {
"InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
owner::class.java.simpleName
@@ -47,7 +47,7 @@
testContext: TestContext,
owner: AndroidOwner?,
private val sendEvent: (MotionEvent) -> Unit
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
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 9f7e5e4..e3ab0ad 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
@@ -112,7 +112,7 @@
// TODO(b/133217292): Better error: explain which gesture couldn't be performed
private var _inputDispatcher: InputDispatcher? =
- InputDispatcher(testContext, checkNotNull(owner))
+ createInputDispatcher(testContext, checkNotNull(owner))
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 7cbf710..01baa72 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
@@ -24,7 +24,7 @@
import kotlin.math.max
import kotlin.math.roundToInt
-internal expect fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
+internal expect fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
/**
* Dispatcher to inject full and partial gestures. An [InputDispatcher] is created at the
@@ -37,9 +37,6 @@
* Clients of [InputDispatcher] should only call methods for the first stage listed below, the
* second stage is handled by [performGesture].
*
- * Implementations of [InputDispatcher] must derive from [PersistingInputDispatcher], which
- * handles state restoration.
- *
* Full gestures:
* * [enqueueClick]
* * [enqueueSwipe]
@@ -56,7 +53,10 @@
* Chaining methods:
* * [enqueueDelay]
*/
-internal abstract class InputDispatcher {
+internal abstract class InputDispatcher(
+ private val testContext: TestContext,
+ private val owner: Owner?
+) {
companion object {
/**
* Whether or not injection of events should be suspended in between events until [now]
@@ -124,6 +124,26 @@
*/
protected abstract val now: Long
+ init {
+ val state = testContext.states.remove(owner)
+ if (state?.partialGesture != null) {
+ nextDownTime = state.nextDownTime
+ gestureLateness = state.gestureLateness
+ partialGesture = state.partialGesture
+ }
+ }
+
+ protected open fun saveState(owner: Owner?) {
+ if (owner != null) {
+ testContext.states[owner] =
+ InputDispatcherState(
+ nextDownTime,
+ gestureLateness,
+ partialGesture
+ )
+ }
+ }
+
/**
* Generates the downTime of the next gesture with the given [duration]. The gesture's
* [duration] is necessary to facilitate chaining of gestures: if another gesture is made
@@ -549,7 +569,12 @@
/**
* Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
*/
- abstract fun dispose()
+ fun dispose() {
+ saveState(owner)
+ onDispose()
+ }
+
+ protected open fun onDispose() {}
}
/**
@@ -565,3 +590,22 @@
val lastPositions = mutableMapOf(Pair(pointerId, startPosition))
var hasPointerUpdates: Boolean = false
}
+
+/**
+ * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
+ * when the [GestureScope] is recreated.
+ *
+ * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
+ * This property will only be restored if an incomplete gesture was in progress when the
+ * state of the [InputDispatcher] was saved.
+ * @param gestureLateness The time difference in milliseconds between enqueuing the first
+ * event of the gesture and dispatching it. Depending on the implementation of
+ * [InputDispatcher], this may or may not be used.
+ * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
+ * when the state of the [InputDispatcher] was saved, this will be `null`.
+ */
+internal data class InputDispatcherState(
+ val nextDownTime: Long,
+ var gestureLateness: Long?,
+ val partialGesture: PartialGesture?
+)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
deleted file mode 100644
index 0ef9f06..0000000
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
+++ /dev/null
@@ -1,67 +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.ui.test
-
-import androidx.compose.ui.node.Owner
-
-internal abstract class PersistingInputDispatcher(
- private val testContext: TestContext,
- private val owner: Owner?
-) : InputDispatcher() {
-
- init {
- val state = testContext.states.remove(owner)
- if (state?.partialGesture != null) {
- nextDownTime = state.nextDownTime
- gestureLateness = state.gestureLateness
- partialGesture = state.partialGesture
- }
- }
-
- protected open fun saveState(owner: Owner?) {
- if (owner != null) {
- testContext.states[owner] =
- InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
- }
- }
-
- final override fun dispose() {
- saveState(owner)
- onDispose()
- }
-
- open fun onDispose() {}
-
- /**
- * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
- * when the [GestureScope] is recreated.
- *
- * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
- * This property will only be restored if an incomplete gesture was in progress when the
- * state of the [InputDispatcher] was saved.
- * @param gestureLateness The time difference in milliseconds between enqueuing the first
- * event of the gesture and dispatching it. Depending on the implementation of
- * [InputDispatcher], this may or may not be used.
- * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
- * when the state of the [InputDispatcher] was saved, this will be `null`.
- */
- internal data class InputDispatcherState(
- val nextDownTime: Long,
- var gestureLateness: Long?,
- val partialGesture: PartialGesture?
- )
-}
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 c4084d0..9624c69 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
@@ -19,7 +19,6 @@
import androidx.compose.ui.node.Owner
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.getAllSemanticsNodes
-import androidx.compose.ui.test.PersistingInputDispatcher.InputDispatcherState
import androidx.compose.ui.text.input.EditOperation
import androidx.compose.ui.text.input.ImeAction
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 94671fd..3d9f038 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
@@ -24,14 +24,14 @@
import androidx.compose.ui.platform.DesktopOwner
import androidx.compose.ui.unit.Uptime
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
return DesktopInputDispatcher(testContext, owner as DesktopOwner)
}
internal class DesktopInputDispatcher(
testContext: TestContext,
val owner: DesktopOwner
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
companion object {
var gesturePointerId = 0L
}
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
index 8720246..3c47455 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
@@ -50,10 +50,4 @@
owner.draw(canvas)
return owners
}
-
- companion object {
- init {
- initCompose()
- }
- }
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -62,8 +62,8 @@
public final class AnnotatedStringKt {
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
- method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
- method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -62,8 +62,8 @@
public final class AnnotatedStringKt {
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
- method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
- method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -62,8 +62,8 @@
public final class AnnotatedStringKt {
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
- method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
- method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+ method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
index 4b443f7..5eb4cee 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
@@ -21,7 +21,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
@@ -124,7 +124,7 @@
@Sampled
fun AnnotatedStringBuilderLambdaSample() {
// create an AnnotatedString using the lambda builder
- annotatedString {
+ buildAnnotatedString {
// append "Hello" with red text color
withStyle(SpanStyle(color = Color.Red)) {
append("Hello")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
index ba55a26..17c7b19 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
@@ -20,7 +20,7 @@
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
@@ -30,7 +30,7 @@
fun BaselineShiftSample() {
Text(
fontSize = 20.sp,
- text = annotatedString {
+ text = buildAnnotatedString {
append(text = "Hello")
withStyle(SpanStyle(baselineShift = BaselineShift.Superscript, fontSize = 16.sp)) {
append("superscript")
@@ -45,7 +45,7 @@
@Sampled
@Composable
fun BaselineShiftAnnotatedStringSample() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("Text ")
withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
append("Demo")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
index 0fc3863..51f383c 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
@@ -21,7 +21,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.sp
@@ -57,7 +57,7 @@
)
Text(
- text = annotatedString {
+ text = buildAnnotatedString {
append(text)
addStyle(paragraphStyle1, 0, text.indexOf('\n') + 1)
addStyle(paragraphStyle2, text.indexOf('\n') + 1, text.length)
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
index e85c75f..451b611 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
@@ -21,8 +21,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.sp
@Sampled
@@ -30,7 +30,7 @@
fun SpanStyleSample() {
Text(
fontSize = 16.sp,
- text = annotatedString {
+ text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("Hello")
}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 11456e3..6f01560 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -1431,7 +1431,7 @@
* Helper function which creates an AnnotatedString where each input string becomes a paragraph.
*/
private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
- return annotatedString {
+ return buildAnnotatedString {
for (paragraph in paragraphs) {
pushStyle(ParagraphStyle())
append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
index 97ec592..c14cc7a 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
@@ -242,7 +242,7 @@
* Helper function which creates an AnnotatedString where each input string becomes a paragraph.
*/
private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
- return annotatedString {
+ return buildAnnotatedString {
for (paragraph in paragraphs) {
pushStyle(ParagraphStyle())
append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index a88a311..2417156 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -33,7 +33,7 @@
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TestFontResourceLoader
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -62,7 +62,7 @@
@Test
fun toAccessibilitySpannableString_with_locale() {
val languageTag = "en-GB"
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(localeList = LocaleList(languageTag))) {
append("world")
@@ -84,7 +84,7 @@
@Test
fun toAccessibilitySpannableString_with_color() {
val color = Color.Black
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(color = color)) {
append("world")
@@ -105,7 +105,7 @@
@Test
fun toAccessibilitySpannableString_with_fontSizeInSp() {
val fontSize = 12.sp
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(fontSize = fontSize)) {
append("world")
@@ -126,7 +126,7 @@
@Test
fun toAccessibilitySpannableString_with_fontSizeInEm() {
val fontSize = 2.em
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(fontSize = fontSize)) {
append("world")
@@ -146,7 +146,7 @@
@Test
fun toAccessibilitySpannableString_with_fontWeightBold() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("world")
@@ -166,7 +166,7 @@
@Test
fun toAccessibilitySpannableString_with_italic() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
append("world")
@@ -187,7 +187,7 @@
@Test
fun toAccessibilitySpannableString_with_fontFamily() {
val fontFamily = FontFamily.Monospace
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(fontFamily = fontFamily)) {
append("world")
@@ -207,7 +207,7 @@
@Test
fun toAccessibilitySpannableString_with_underline() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append("world")
@@ -225,7 +225,7 @@
@Test
fun toAccessibilitySpannableString_with_lineThrough() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
append("world")
@@ -244,7 +244,7 @@
@Test
fun toAccessibilitySpannableString_with_scaleX() {
val scaleX = 1.2f
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(
style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = scaleX))
@@ -267,7 +267,7 @@
@Test
fun toAccessibilitySpannableString_with_background() {
val backgroundColor = Color.Red
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("hello")
withStyle(style = SpanStyle(background = backgroundColor)) {
append("world")
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 189c1ad..732c765 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -743,10 +743,10 @@
* @param builder lambda to modify [AnnotatedString.Builder]
*/
@Deprecated(
- message = "Renamed to annotatedString.",
- replaceWith = ReplaceWith("annotatedString")
+ message = "Renamed to buildAnnotatedString.",
+ replaceWith = ReplaceWith("buildAnnotatedString")
)
-inline fun AnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
Builder().apply(builder).toAnnotatedString()
/**
@@ -757,7 +757,7 @@
*
* @param builder lambda to modify [AnnotatedString.Builder]
*/
-inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun buildAnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
Builder().apply(builder).toAnnotatedString()
/**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
index 0f1e350..42a8fbe 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
@@ -58,7 +58,7 @@
}
/**
- * [ParagraphIntrinsics] for each paragraph included in the [annotatedString]. For empty string
+ * [ParagraphIntrinsics] for each paragraph included in the [buildAnnotatedString]. For empty string
* there will be a single empty paragraph intrinsics info.
*/
internal val infoList: List<ParagraphIntrinsicInfo>
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
index ddf38bd3..efac85d 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
@@ -278,11 +278,11 @@
return null
}
- private fun getBoxBackwardByOffset(offset: Int): TextBox? {
+ private fun getBoxBackwardByOffset(offset: Int, end: Int = offset): TextBox? {
var from = offset - 1
while (from >= 0) {
val box = para.getRectsForRange(
- from, offset,
+ from, end,
RectHeightMode.STRUT, RectWidthMode.TIGHT
).firstOrNull()
when {
@@ -314,8 +314,10 @@
return para.getGlyphPositionAtCoordinate(position.x, position.y).position
}
- override fun getBoundingBox(offset: Int) =
- getBoxForwardByOffset(offset)!!.rect.toComposeRect()
+ override fun getBoundingBox(offset: Int): Rect {
+ val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)!!
+ return box.rect.toComposeRect()
+ }
override fun getWordBoundary(offset: Int) = para.getWordBoundary(offset).let {
TextRange(it.start, it.end)
@@ -554,6 +556,8 @@
when (op) {
is Op.StyleAdd -> {
+ // cached SkTextStyled could was loaded with a different font loader
+ ensureFontsAreRegistered(fontLoader, op.style)
pb.pushStyle(makeSkTextStyle(op.style))
}
is Op.PutPlaceholder -> {
@@ -585,6 +589,12 @@
return pb.build()
}
+ private fun ensureFontsAreRegistered(fontLoader: FontLoader, style: ComputedStyle) {
+ style.fontFamily?.let {
+ fontLoader.ensureRegistered(it)
+ }
+ }
+
private sealed class Op {
abstract val position: Int
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
index f81951e..641b23cd 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
@@ -491,7 +491,7 @@
val paragraphStyle1 = ParagraphStyle(textAlign = TextAlign.Right)
val paragraphStyle2 = ParagraphStyle(textAlign = TextAlign.Right)
- val buildResult = annotatedString {
+ val buildResult = buildAnnotatedString {
withStyle(paragraphStyle1) {
withStyle(spanStyle1) {
append(text1)
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
index 909a52e..5a422e9 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
@@ -324,7 +324,7 @@
@Test
fun subSequence_withAnnotations_noIntersection() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("ab")
pushStringAnnotation("scope1", "annotation1")
append("cd")
@@ -338,7 +338,7 @@
@Test
fun subSequence_withAnnotations_collapsedRange() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("ab")
pushStringAnnotation("scope1", "annotation1")
append("cd")
@@ -370,7 +370,7 @@
@Test
fun subSequence_withAnnotations_hasIntersection() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("ab")
pushStringAnnotation("scope1", "annotation1")
append("cd")
@@ -398,7 +398,7 @@
@Test
fun subSequence_withAnnotations_containsRange() {
- val annotatedString = annotatedString {
+ val annotatedString = buildAnnotatedString {
append("ab")
pushStringAnnotation("scope1", "annotation1")
append("cd")
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 4fc98e6c..db472c3 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -67,8 +67,5 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
useIR = true
- freeCompilerArgs += [
- "-P", "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
- ]
}
}
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 cfcd2a2..73d6452 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
@@ -463,6 +463,7 @@
@SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29: b/171519437
@Test
+ @Ignore("b/174152464")
fun testTextId() {
val slotTableRecord = SlotTableRecord.create()
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
index 591a4fa..6c431b6 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
@@ -53,17 +53,30 @@
* Asserts that the given Composable method executes correct and outputs some [ViewInfo]s.
*/
private fun assertRendersCorrectly(className: String, methodName: String): List<ViewInfo> {
- val committed = CountDownLatch(1)
+ val committedAndDrawn = CountDownLatch(1)
+ val committed = AtomicBoolean(false)
activityTestRule.runOnUiThread {
composeViewAdapter.init(
className, methodName, debugViewInfos = true,
onCommit = {
- committed.countDown()
+ committed.set(true)
+ },
+ onDraw = {
+ if (committed.get()) {
+ committedAndDrawn.countDown()
+ }
}
)
}
- committed.await()
+ // Workaround for a problem described in b/174291742 where onLayout will not be called
+ // after composition for the first test in the test suite.
+ activityTestRule.runOnUiThread {
+ composeViewAdapter.requestLayout()
+ }
+
+ // Wait for the first draw after the Composable has been committed.
+ committedAndDrawn.await()
activityTestRule.runOnUiThread {
assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
}
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
index c74b740..e2ebdbd 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.tooling.test.R
-import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -53,10 +52,6 @@
debugViewInfos = true
)
}
-
- activityTestRule.runOnUiThread {
- Assert.assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
- }
}
private class MyListProvider : CollectionPreviewParameterProvider<Int>(listOf(1, 2, 3))
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
index c72068b..7e6d2b6 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
@@ -255,6 +255,7 @@
throw exception
}
+ processViewInfos()
if (composableName.isNotEmpty()) {
// TODO(b/160126628): support other APIs, e.g. animate
findAndSubscribeTransitions()
@@ -423,7 +424,6 @@
previewComposition = @Composable {
onCommit {
- processViewInfos()
onCommit()
}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5043c6d..a0f762a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -119,8 +119,7 @@
method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
}
- public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
- method public void drawContent();
+ public final class ContentDrawScopeKt {
}
public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
public final class DrawModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class FocusModifierKt {
@@ -279,7 +278,7 @@
method public float getFontScale();
method public long getSize-NH-jbRc();
method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
- method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+ method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
property public float density;
property public float fontScale;
property public final long size;
@@ -295,13 +294,13 @@
}
public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
- method public void draw(androidx.compose.ui.ContentDrawScope);
+ method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
}
public final class DrawModifierKt {
method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class DrawResult {
@@ -2402,6 +2401,10 @@
ctor public LoadedResource(T? resource);
}
+ public final class PainterResourcesKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+ }
+
public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
ctor public PendingResource(T? resource);
}
@@ -2571,6 +2574,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2582,6 +2586,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2710,6 +2715,7 @@
method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 5043c6d..a0f762a 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -119,8 +119,7 @@
method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
}
- public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
- method public void drawContent();
+ public final class ContentDrawScopeKt {
}
public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
public final class DrawModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class FocusModifierKt {
@@ -279,7 +278,7 @@
method public float getFontScale();
method public long getSize-NH-jbRc();
method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
- method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+ method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
property public float density;
property public float fontScale;
property public final long size;
@@ -295,13 +294,13 @@
}
public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
- method public void draw(androidx.compose.ui.ContentDrawScope);
+ method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
}
public final class DrawModifierKt {
method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class DrawResult {
@@ -2402,6 +2401,10 @@
ctor public LoadedResource(T? resource);
}
+ public final class PainterResourcesKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+ }
+
public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
ctor public PendingResource(T? resource);
}
@@ -2571,6 +2574,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2582,6 +2586,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2710,6 +2715,7 @@
method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 6ab61a8..1dc8c01 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -119,8 +119,7 @@
method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
}
- public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
- method public void drawContent();
+ public final class ContentDrawScopeKt {
}
public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
public final class DrawModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class FocusModifierKt {
@@ -279,7 +278,7 @@
method public float getFontScale();
method public long getSize-NH-jbRc();
method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
- method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+ method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
property public float density;
property public float fontScale;
property public final long size;
@@ -295,13 +294,13 @@
}
public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
- method public void draw(androidx.compose.ui.ContentDrawScope);
+ method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
}
public final class DrawModifierKt {
method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
- method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+ method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
public final class DrawResult {
@@ -2464,6 +2463,10 @@
ctor public LoadedResource(T? resource);
}
+ public final class PainterResourcesKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+ }
+
public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
ctor public PendingResource(T? resource);
}
@@ -2633,6 +2636,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2644,6 +2648,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2772,6 +2777,7 @@
method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
index fd1f866..ba2d319 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
@@ -27,7 +27,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
index 1b6c95a..a7dab04 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
@@ -34,7 +34,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
index cbae372..5500315 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
@@ -22,14 +22,23 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.platform.AmbientContext
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import kotlin.math.roundToInt
@Suppress("SetTextI18n")
@Sampled
@@ -43,3 +52,20 @@
view.layoutParams = ViewGroup.LayoutParams(size, size)
}
}
+
+@Sampled
+@Composable
+fun AndroidDrawableInDrawScopeSample() {
+ val drawable = ContextCompat.getDrawable(AmbientContext.current, R.drawable.sample_drawable)
+ Box(
+ modifier = Modifier.size(100.dp)
+ .drawBehind {
+ drawIntoCanvas { canvas ->
+ drawable?.let {
+ it.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
+ it.draw(canvas.nativeCanvas)
+ }
+ }
+ }
+ )
+}
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 04a0ab1..a53a638 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
@@ -26,7 +26,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.BlendMode
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 ef4205e..dfce282 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
@@ -17,9 +17,11 @@
package androidx.compose.ui.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
@@ -27,6 +29,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Sampled
@@ -53,4 +56,16 @@
.background(color = Color.Yellow)
.paint(CustomPainter())
) { /** intentionally empty **/ }
+}
+
+@Sampled
+@Composable
+fun PainterResourceSample() {
+ // Sample showing how to render a Painter based on a different resource (vector vs png)
+ // Here a Vector asset is used in the portrait orientation, however, a png is used instead
+ // in the landscape orientation based on the res/drawable and res/drawable-land-hdpi folders
+ Image(
+ painterResource(R.drawable.ic_vector_or_png),
+ modifier = Modifier.size(50.dp)
+ )
}
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
new file mode 100755
index 0000000..4696f99
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
Binary files differ
diff --git a/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
new file mode 100644
index 0000000..64452ad
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
@@ -0,0 +1,36 @@
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector android:height="200dp" android:viewportHeight="144"
+ android:viewportWidth="144" android:width="200dp"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#F79D80" android:pathData="M69.26,55.73m-53.39,0a53.39,53.39 0,1 1,106.78 0a53.39,53.39 0,1 1,-106.78 0"/>
+ <path android:fillColor="#37474F" android:pathData="M47.66,76.35h2.26v65.31h-2.26z"/>
+ <path android:fillColor="#FFFFFF" android:pathData="M61.47,22.88l7.59,0.63C69.06,23.51 65.07,22.59 61.47,22.88z"/>
+ <path android:fillColor="#FFFFFF" android:pathData="M78.86,44.71c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.11,-3.17 2.61,-4.26 5.6,-4.5l-11.02,-0.92c-3.05,9.16 1.58,18.08 14.69,24.18c10.96,5.1 6.86,12.67 3.64,16.47c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.43 86.05,50.53 78.86,44.71z"/>
+ <path android:fillColor="#434343" android:pathData="M68.8,62.41c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 1.34,-0.96 3.61,-2.58c-2.04,-56.41 29.85,-60.98 35.73,-61.34c0.75,-0.1 1.51,-0.16 2.29,-0.16C65.16,60.29 66.58,61.23 68.8,62.41"/>
+ <path android:fillColor="#FFD54F" android:pathData="M83.23,22.46l20.76,1.43c0.4,0.03 0.52,-0.52 0.16,-0.67l-19.1,-7.78c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.35C82.86,22.23 83.01,22.44 83.23,22.46z"/>
+ <path android:fillColor="#F9BF2C" android:pathData="M86.66,16.09l-1.62,-0.66c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.36c-0.06,0.21 0.1,0.42 0.31,0.44l10.52,0.72L86.66,16.09z"/>
+ <path android:fillColor="#1B2428" android:pathData="M47.66,103.79l0,3.86l2.24,2.24l0,-7.7z"/>
+ <path android:fillColor="#FFFFFF" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.52,-0.06c2.52,1.27 5.61,2.55 9.09,2.79c8.62,0.59 15.88,-2.89 16.22,-7.77C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+ <path android:fillColor="#881A51" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.67,-0.38c2.52,1.27 7.02,1.03 10.5,1.27c8.62,0.59 14.33,-1.05 14.66,-5.93C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+ <path android:fillColor="#FFFFFF" android:pathData="M71.83,11.32c-5.16,-0.29 -9.83,0.61 -12.82,2.21l0,0c-7.5,3.39 -9.16,10.46 -9.25,11.23c-0.15,1.18 3.05,3.13 3.05,3.13l4.2,-4.48l4.47,-0.54c2.52,1.03 5.59,2.28 9.07,2.47c8.62,0.48 15.08,-2.54 15.41,-6.49C86.29,14.92 80.45,11.8 71.83,11.32z"/>
+ <path android:fillColor="#BDBDBD" android:pathData="M73.53,24.78c-3.48,-0.19 -6.55,-1.44 -9.07,-2.47c0,0 0,0 0,0l-1.08,0.13c-0.68,0.11 -1.33,0.28 -1.91,0.52l0.11,-0.01c0,0 0,0 0,0c2.52,1.03 5.59,2.28 9.07,2.47c3.37,0.19 6.4,-0.16 8.87,-0.89C77.7,24.8 75.68,24.9 73.53,24.78z"/>
+ <path android:fillColor="#EEEEEE" android:pathData="M82.13,48.13c1.75,4.57 2.05,9.63 0.33,14.43c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.33,0.24 -41.01,29.29 -53.34,38.1c0.03,2.98 0.16,6.12 0.41,9.41c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.13,62.01 86.99,53.98 82.13,48.13z"/>
+ <path android:fillColor="#BDBDBD" android:pathData="M78.86,44.91c-0.16,-0.13 -0.33,-0.26 -0.5,-0.39c5.68,5.93 8.39,14.63 5.53,22.58c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -43.58,31.16 -54.59,39.02c0.06,1.4 0.15,2.8 0.26,4.26c0,0 57.03,-40.74 57.11,-40.79c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.64 86.05,50.73 78.86,44.91z"/>
+ <path android:fillColor="#BDBDBD" android:pathData="M78.94,44.76c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.04,-1.21 0.33,-2.11 0.97,-2.78c-1.81,0.6 -3.12,1.8 -3.04,4.02C54.2,38.52 70.07,40.5 76.86,46c7.19,5.82 11.02,15.73 7.81,24.68c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -44.69,31.92 -54.89,39.21c0.01,0.08 0.01,0.16 0.02,0.24c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.96,60.49 86.13,50.59 78.94,44.76z"/>
+ <path android:fillColor="#BDBDBD" android:pathData="M54.5,28.8c0,0 -0.52,0.61 -0.52,0.61c0.01,-0.01 -0.02,-0.11 -0.02,-0.12c-0.02,-0.11 -0.03,-0.23 -0.05,-0.34c-0.04,-0.4 -0.06,-0.79 -0.04,-1.19c0.02,-0.55 0.1,-1.1 0.24,-1.64c0.17,-0.63 0.43,-1.24 0.78,-1.79c0.42,-0.65 0.96,-1.21 1.58,-1.66c0.32,-0.23 0.66,-0.43 1.01,-0.61c0.47,-0.24 0.93,-0.43 1.46,-0.49c0.27,-0.03 0.55,-0.05 0.82,-0.05c0.89,-0.02 1.78,0.07 2.65,0.24c0.37,0.07 0.73,0.16 1.09,0.26c0.19,0.05 0.39,0.11 0.58,0.17c0.1,0.03 0.2,0.07 0.3,0.1c0.04,0.01 0.27,0.13 0.3,0.11c0,0 -1.79,1.07 -1.79,1.07s-5.78,-1.34 -6.91,3.25L54.5,28.8z"/>
+</vector>
diff --git a/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
new file mode 100644
index 0000000..68053bc
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <gradient
+ android:startColor="@android:color/holo_red_dark"
+ android:centerColor="@android:color/holo_orange_dark"
+ android:endColor="@android:color/holo_blue_dark" />
+</shape>
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index dfad721..0b258d2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -37,6 +37,7 @@
import androidx.compose.ui.semantics.SemanticsWrapper
import androidx.compose.ui.semantics.accessibilityValue
import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.onClick
@@ -108,15 +109,17 @@
}
@Test
- fun testPopulateAccessibilityNodeInfoProperties() {
- var info = AccessibilityNodeInfoCompat.obtain()
+ fun testPopulateAccessibilityNodeInfoProperties_general() {
+ val info = AccessibilityNodeInfoCompat.obtain()
val clickActionLabel = "click"
+ val dismissActionLabel = "dismiss"
val accessibilityValue = "checked"
- var semanticsModifier = SemanticsModifierCore(1, true) {
+ val semanticsModifier = SemanticsModifierCore(1, true) {
this.accessibilityValue = accessibilityValue
onClick(clickActionLabel) { true }
+ dismiss(dismissActionLabel) { true }
}
- var semanticsNode = SemanticsNode(
+ val semanticsNode = SemanticsNode(
SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
true
)
@@ -131,6 +134,15 @@
)
)
)
+ assertTrue(
+ containsAction(
+ info,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_DISMISS,
+ dismissActionLabel
+ )
+ )
+ )
val stateDescription = when {
BuildCompat.isAtLeastR() -> {
info.unwrap().stateDescription
@@ -147,14 +159,17 @@
assertEquals(accessibilityValue, stateDescription)
assertTrue(info.isClickable)
assertTrue(info.isVisibleToUser)
+ }
- info = AccessibilityNodeInfoCompat.obtain()
+ @Test
+ fun testPopulateAccessibilityNodeInfoProperties_SeekBar() {
+ val info = AccessibilityNodeInfoCompat.obtain()
val setProgressActionLabel = "setProgress"
- semanticsModifier = SemanticsModifierCore(1, true) {
+ val semanticsModifier = SemanticsModifierCore(1, true) {
accessibilityValueRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
setProgress(setProgressActionLabel) { true }
}
- semanticsNode = SemanticsNode(
+ val semanticsNode = SemanticsNode(
SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
true
)
@@ -178,12 +193,15 @@
)
)
}
+ }
- info = AccessibilityNodeInfoCompat.obtain()
+ @Test
+ fun testPopulateAccessibilityNodeInfoProperties_EditText() {
+ val info = AccessibilityNodeInfoCompat.obtain()
val setSelectionActionLabel = "setSelection"
val setTextActionLabel = "setText"
val text = "hello"
- semanticsModifier = SemanticsModifierCore(1, true) {
+ val semanticsModifier = SemanticsModifierCore(1, true) {
this.text = AnnotatedString(text)
this.textSelectionRange = TextRange(1)
this.focused = true
@@ -191,7 +209,7 @@
setText(setTextActionLabel) { true }
setSelection(setSelectionActionLabel) { _, _, _ -> true }
}
- semanticsNode = SemanticsNode(
+ val semanticsNode = SemanticsNode(
SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
true
)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index bffcb8c8..46ca6ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -59,6 +59,7 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index 84b5920..0257d38 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -585,7 +585,7 @@
emit<LayoutNode, Applier<Any>>(
ctor = LayoutEmitHelper.constructor,
update = {
- node = this.node
+ set(Unit) { node = this }
set(noOpMeasureBlocks, LayoutEmitHelper.setMeasureBlocks)
}
)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
index 6a601b5..4dba953cc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
@@ -460,6 +460,15 @@
}
}
+ semanticsNode.config.getOrNull(SemanticsActions.Dismiss)?.let {
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_DISMISS,
+ it.label
+ )
+ )
+ }
+
if (semanticsNode.config.contains(CustomActions)) {
val customActions = semanticsNode.config[CustomActions]
if (customActions.size >= AccessibilityActionsResourceIds.size) {
@@ -891,6 +900,11 @@
it.action()
} ?: false
}
+ AccessibilityNodeInfoCompat.ACTION_DISMISS -> {
+ return node.config.getOrNull(SemanticsActions.Dismiss)?.let {
+ it.action()
+ } ?: false
+ }
// TODO: handling for other system actions
else -> {
val label = actionIdToLabel[virtualViewId]?.get(action) ?: return false
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
index c377937..b8ad9f3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
@@ -30,6 +30,8 @@
*
* Note: This API is transient and will be likely removed for encouraging async resource loading.
*
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
* @param id the resource identifier
* @return the decoded image data associated with the resource
*/
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
new file mode 100644
index 0000000..d8af23f
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.ui.res
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.imageFromResource
+import androidx.compose.ui.graphics.painter.ImagePainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.compat.seekToStartTag
+import androidx.compose.ui.platform.AmbientContext
+
+/**
+ * Create a [Painter] from an Android resource id. This can load either an instance of
+ * [ImagePainter] or [VectorPainter] for [ImageBitmap] based assets or vector based assets
+ * respectively. The resources with the given id must point to either fully rasterized
+ * images (ex. PNG or JPG files) or VectorDrawable xml assets. API based xml Drawables
+ * are not supported here.
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.PainterResourceSample
+ *
+ * Alternative Drawable implementations can be used with compose by calling
+ * [drawIntoCanvas] and drawing with the Android framework canvas provided through [nativeCanvas]
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.AndroidDrawableInDrawScopeSample
+ *
+ * @param id Resources object to query the image file from
+ *
+ * @return [Painter] used for drawing the loaded resource
+ */
+@Composable
+fun painterResource(@DrawableRes id: Int): Painter {
+ val context = AmbientContext.current
+ val res = context.resources
+ val value = remember { TypedValue() }
+ res.getValue(id, value, true)
+ val path = value.string
+ // Assume .xml suffix implies loading a VectorDrawable resource
+ return if (path?.endsWith(".xml") == true) {
+ val imageVector = remember(path, id) {
+ loadVectorResource(context.theme, res, id)
+ }
+ rememberVectorPainter(imageVector)
+ } else {
+ // Otherwise load the bitmap resource
+ val imageBitmap = remember(path, id) {
+ loadImageBitmapResource(res, id)
+ }
+ ImagePainter(imageBitmap)
+ }
+}
+
+/**
+ * Helper method to validate that the xml resource is a vector drawable then load
+ * the ImageVector. Because this throws exceptions we cannot have this implementation as part of
+ * the composable implementation it is invoked in.
+ */
+private fun loadVectorResource(theme: Resources.Theme, res: Resources, id: Int): ImageVector {
+ @Suppress("ResourceType") val parser = res.getXml(id)
+ if (parser.seekToStartTag().name != "vector") {
+ throw IllegalArgumentException(errorMessage)
+ }
+ return loadVectorResourceInner(theme, res, parser)
+}
+
+/**
+ * Helper method to validate the asset resource is a supported resource type and returns
+ * an ImageBitmap resource. Because this throws exceptions we cannot have this implementation
+ * as part of the composable implementation it is invoked in.
+ */
+private fun loadImageBitmapResource(res: Resources, id: Int): ImageBitmap {
+ try {
+ return imageFromResource(res, id)
+ } catch (throwable: Throwable) {
+ throw IllegalArgumentException(errorMessage)
+ }
+}
+
+private const val errorMessage =
+ "Only VectorDrawables and rasterized asset types are supported ex. PNG, JPG"
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
index 8f59986..3fd252b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
@@ -16,8 +16,8 @@
package androidx.compose.ui.res
-import android.annotation.SuppressLint
import android.content.res.Resources
+import android.content.res.XmlResourceParser
import android.util.TypedValue
import android.util.Xml
import androidx.annotation.DrawableRes
@@ -39,6 +39,8 @@
* based off of it's dimensions appropriately
*
* Note: This API is transient and will be likely removed for encouraging async resource loading.
+ *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
*/
@Composable
fun vectorResource(@DrawableRes id: Int): ImageVector {
@@ -57,6 +59,8 @@
* [PendingResource]. Once the loading finishes, recompose is scheduled and this function will
* return deferred vector drawable resource with [LoadedResource] or [FailedResource].
*
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
* @param id the resource identifier
* @param pendingResource an optional resource to be used during loading instead.
* @param failedResource an optional resource to be used if resource loading failed.
@@ -90,11 +94,23 @@
internal fun loadVectorResource(
theme: Resources.Theme? = null,
res: Resources,
- resId: Int
+ resId: Int,
+): ImageVector =
+ loadVectorResourceInner(theme, res, res.getXml(resId).apply { seekToStartTag() })
+
+/**
+ * Helper method that parses a vector asset from the given [XmlResourceParser] position.
+ * This method assumes the parser is already been positioned to the start tag
+ */
+@Throws(XmlPullParserException::class)
+@SuppressWarnings("RestrictedApi")
+internal fun loadVectorResourceInner(
+ theme: Resources.Theme? = null,
+ res: Resources,
+ parser: XmlResourceParser
): ImageVector {
- @SuppressLint("ResourceType") val parser = res.getXml(resId)
val attrs = Xml.asAttributeSet(parser)
- val builder = parser.seekToStartTag().createVectorImageBuilder(res, theme, attrs)
+ val builder = parser.createVectorImageBuilder(res, theme, attrs)
var nestedGroups = 0
while (!parser.isAtEnd()) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
index 30a3c4d..715a43f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
@@ -13,20 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.compose.ui
-import androidx.compose.ui.graphics.drawscope.DrawScope
+package androidx.compose.ui
/**
* Receiver scope for drawing content into a layout, where the content can
* be drawn between other canvas operations. If [drawContent] is not called,
* the contents of the layout will not be drawn.
- *
- * @see DrawModifier
*/
-interface ContentDrawScope : DrawScope {
- /**
- * Causes child drawing operations to run during the `onPaint` lambda.
- */
- fun drawContent()
-}
+@Deprecated(
+ "Use ContentDrawScope in the graphics package instead",
+ ReplaceWith(
+ "ContentDrawScope",
+ "androidx.compose.ui.graphics.drawscope.ContentDrawScope"
+ )
+)
+typealias ContentDrawScope = androidx.compose.ui.graphics.drawscope.ContentDrawScope
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
index 398b8db..8a2591f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
@@ -37,7 +37,7 @@
"Use DrawModifier from the androidx.compose.ui.draw package instead",
ReplaceWith(
"DrawModifier",
- "androidx.compose.ui.draw"
+ "androidx.compose.ui.draw.DrawModifier"
)
)
typealias DrawModifier = androidx.compose.ui.draw.DrawModifier
@@ -50,7 +50,7 @@
"Use DrawCacheModifier from the androidx.compose.ui.draw package instead",
ReplaceWith(
"DrawCacheModifier",
- "androidx.compose.ui.draw"
+ "androidx.compose.ui.draw.DrawCacheModifier"
)
)
typealias DrawCacheModifier = androidx.compose.ui.draw.DrawCacheModifier
@@ -62,7 +62,7 @@
"Use drawBehind from the androidx.compose.ui.draw package instead",
ReplaceWith(
"drawBehind(onDraw)",
- "androidx.compose.ui.draw"
+ "androidx.compose.ui.draw.drawBehind"
)
)
fun Modifier.drawBehind(onDraw: DrawScope.() -> Unit) = drawBehind(onDraw)
@@ -87,7 +87,7 @@
"Use the drawWithCache from the androidx.compose.ui.draw package instead",
ReplaceWith(
"drawWithCache(onBuildCache)",
- "androidx.compose.ui.draw"
+ "androidx.compose.ui.draw.drawWithCache"
)
)
fun Modifier.drawWithCache(onBuildDrawCache: CacheDrawScope.() -> DrawResult) =
@@ -103,7 +103,7 @@
*/
@Deprecated(
"Use CacheDrawScope from the androidx.compose.ui.draw package instead",
- ReplaceWith("CacheDrawScope", "androidx.compose.ui.draw")
+ ReplaceWith("CacheDrawScope", "androidx.compose.ui.draw.CacheDrawScope")
)
typealias CacheDrawScope = androidx.compose.ui.draw.CacheDrawScope
@@ -113,12 +113,16 @@
*/
@Deprecated(
"Use DrawResult from the androidx.compose.ui.draw package instead",
- ReplaceWith("DrawResult", "androidx.compose.ui.draw")
+ ReplaceWith("DrawResult", "androidx.compose.ui.draw.DrawResult")
)
typealias DrawResult = androidx.compose.ui.draw.DrawResult
+@Suppress("DEPRECATION")
@Deprecated(
"Use drawWithContent from the androidx.compose.ui.draw package instead",
- ReplaceWith("drawWithContent(onDraw)", "androidx.compose.ui.draw")
+ ReplaceWith(
+ "drawWithContent(onDraw)",
+ "androidx.compose.ui.draw.drawWithContent"
+ )
)
fun Modifier.drawWithContent(onDraw: ContentDrawScope.() -> Unit) = drawWithContent(onDraw)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index a2f6821..43ba66b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -17,11 +17,11 @@
package androidx.compose.ui.draw
import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 378184a..f50f987 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -17,7 +17,7 @@
package androidx.compose.ui.draw
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 6f534f2..de8dcf9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -177,7 +177,7 @@
* a LayoutNode.
*
* [SuspendingPointerInputFilter] implements the [PointerInputScope] used to offer the
- * [Modifier.pointerInput] DSL and carries the [Density] from [DensityAmbient] at the point of
+ * [Modifier.pointerInput] DSL and carries the [Density] from [AmbientDensity] at the point of
* the modifier's materialization. Even if this value were returned to the [PointerInputFilter]
* callbacks, we would still need the value at composition time in order for [Modifier.pointerInput]
* to begin its internal [LaunchedEffect] for the provided code block.
@@ -236,6 +236,13 @@
private var lastPointerEvent: PointerEvent? = null
/**
+ * The size of the bounds of this input filter. Normally [PointerInputFilter.size] can
+ * be used, but for tests, it is better to not rely on something set to an `internal`
+ * method.
+ */
+ private var boundsSize: IntSize = IntSize.Zero
+
+ /**
* Snapshot the current [pointerHandlers] and run [block] on each one.
* May not be called reentrant or concurrent with itself.
*
@@ -255,13 +262,9 @@
try {
when (pass) {
PointerEventPass.Initial, PointerEventPass.Final ->
- dispatchingPointerHandlers.forEach {
- block(it)
- }
+ dispatchingPointerHandlers.forEach(block)
PointerEventPass.Main ->
- dispatchingPointerHandlers.forEachReversed {
- block(it)
- }
+ dispatchingPointerHandlers.forEachReversed(block)
}
} finally {
dispatchingPointerHandlers.clear()
@@ -286,6 +289,7 @@
pass: PointerEventPass,
bounds: IntSize
) {
+ boundsSize = bounds
if (pass == PointerEventPass.Initial) {
currentPointers.clear()
pointerEvent.changes.fastMapTo(currentPointers) { it.current }
@@ -336,7 +340,7 @@
val handlerCoroutine = PointerEventHandlerCoroutine(
continuation,
currentPointers,
- size,
+ boundsSize,
viewConfiguration,
this
)
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 0470a3f..f4e5345 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
@@ -17,7 +17,7 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.FocusModifier
import androidx.compose.ui.FocusObserverModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 34db148..e6d3278 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -20,7 +20,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.util.annotation.IntRange
import kotlin.reflect.KProperty
@@ -155,7 +155,7 @@
if (parentValue == null) {
childValue
} else {
- annotatedString {
+ buildAnnotatedString {
append(parentValue)
append(", ")
append(childValue)
@@ -289,6 +289,13 @@
val PasteText = SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("PasteText")
/**
+ * Action to dismiss a dismissible node.
+ *
+ * @see SemanticsPropertyReceiver.dismiss
+ */
+ val Dismiss = SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("Dismiss")
+
+ /**
* Custom actions which are defined by app developers.
*
* @see SemanticsPropertyReceiver.customActions
@@ -665,4 +672,17 @@
action: () -> Boolean
) {
this[SemanticsActions.PasteText] = AccessibilityAction(label, action)
+}
+
+/**
+ * This function adds the [SemanticsActions.Dismiss] to the [SemanticsPropertyReceiver].
+ *
+ * @param label Optional label for this action.
+ * @param action Action to be performed when the [SemanticsActions.Dismiss] is called.
+ */
+fun SemanticsPropertyReceiver.dismiss(
+ label: String? = null,
+ action: () -> Boolean
+) {
+ this[SemanticsActions.Dismiss] = AccessibilityAction(label, action)
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
index 323a091..9a82047 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
@@ -21,36 +21,72 @@
import androidx.compose.ui.window.MenuBar
import java.awt.image.BufferedImage
+/**
+ * AppFrame is an abstract class that represents a window.
+ * <p>
+ * Known subclasses: AppWindow
+ */
abstract class AppFrame {
+ /**
+ * Gets ComposeWindow object.
+ */
abstract val window: ComposeWindow
internal var menuBar: MenuBar? = null
+ /**
+ * Gets the parent window. If the value is not null, the current window is a dialog.
+ */
var invoker: AppFrame? = null
protected set
+ /**
+ * Gets the title of the window. The title is displayed in the windows's native border.
+ */
val title: String
get() = window.title
+ /**
+ * Gets the width of the window.
+ */
val width: Int
get() = window.width
+ /**
+ * Gets the height of the window.
+ */
val height: Int
get() = window.height
+ /**
+ * Gets the current x coordinate of the window.
+ */
val x: Int
get() = window.x
+ /**
+ * Gets the current y coordinate of the window.
+ */
val y: Int
get() = window.y
+ /**
+ * Returns true if the winodw is closed, false otherwise.
+ */
var isClosed: Boolean = false
internal set
+ /**
+ * Returns the icon image of the window. If the icon is not set, null is returned.
+ */
var icon: BufferedImage? = null
internal set
+ /**
+ * Returns events of the window. Each event is described as a lambda that is invoked when
+ * needed.
+ */
var events: WindowEvents = WindowEvents()
internal set
@@ -58,22 +94,64 @@
internal var onDismiss: (() -> Unit)? = null
+ /**
+ * Sets the title of the window.
+ *
+ * @param title Window title text.
+ */
abstract fun setTitle(title: String)
+ /**
+ * Sets the image icon of the window.
+ *
+ * @param image Image of the icon.
+ */
abstract fun setIcon(image: BufferedImage?)
+ /**
+ * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+ * Linux) or at the top of the screen (Mac OS).
+ *
+ * @param manuBar Window menu bar.
+ */
abstract fun setMenuBar(menuBar: MenuBar)
+ /**
+ * Removes the menu bar of the window.
+ */
abstract fun removeMenuBar()
+ /**
+ * Sets the new position of the window on the screen.
+ *
+ * @param x the new x-coordinate of the window.
+ * @param y the new y-coordinate of the window.
+ */
abstract fun setLocation(x: Int, y: Int)
+ /**
+ * Sets the window to the center of the screen.
+ */
abstract fun setWindowCentered()
+ /**
+ * Sets the new size of the window.
+ *
+ * @param width the new width of the window.
+ * @param height the new height of the window.
+ */
abstract fun setSize(width: Int, height: Int)
+ /**
+ * Shows a window with the given Compose content.
+ *
+ * @param content Composable content of the window.
+ */
abstract fun show(content: @Composable () -> Unit)
+ /**
+ * Closes the window.
+ */
abstract fun close()
internal abstract fun dispose()
@@ -87,6 +165,20 @@
internal abstract fun unlockWindow()
}
+/**
+ * WindowEvents is the class that contains all the events supported by window.
+ *
+ * @param onOpen The event that is invoked after the window appears.
+ * @param onClose The event that is invoked after the window is closed.
+ * @param onMinimize The event that is invoked after the window is minimized.
+ * @param onMaximize The event that is invoked after the window is maximized.
+ * @param onRestore The event that is invoked when the window is restored after the window has been
+ * maximized or minimized.
+ * @param onFocusGet The event that is invoked when the window gets focus.
+ * @param onFocusLost The event that is invoked when the window lost focus.
+ * @param onResize The event that is invoked when the window is resized.
+ * @param onRelocate The event that is invoked when the window is relocated.
+ */
class WindowEvents(
var onOpen: (() -> Unit)? = null,
var onClose: (() -> Unit)? = null,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index d154ee8..f4131a1 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -89,6 +89,9 @@
*/
class AppWindow : AppFrame {
+ /**
+ * Gets ComposeWindow object.
+ */
override val window: ComposeWindow
init {
@@ -232,10 +235,20 @@
pair = null
}
+ /**
+ * Sets the title of the window.
+ *
+ * @param title Window title text.
+ */
override fun setTitle(title: String) {
window.setTitle(title)
}
+ /**
+ * Sets the image icon of the window.
+ *
+ * @param image Image of the icon.
+ */
override fun setIcon(image: BufferedImage?) {
this.icon = image
if (icon != null) {
@@ -249,16 +262,31 @@
}
}
+ /**
+ * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+ * Linux) or at the top of the screen (Mac OS).
+ *
+ * @param manuBar Window menu bar.
+ */
override fun setMenuBar(menuBar: MenuBar) {
this.menuBar = menuBar
window.setJMenuBar(menuBar.menuBar)
}
+ /**
+ * Removes the menu bar of the window.
+ */
override fun removeMenuBar() {
this.menuBar = null
window.setJMenuBar(JMenuBar())
}
+ /**
+ * Sets the new size of the window.
+ *
+ * @param width the new width of the window.
+ * @param height the new height of the window.
+ */
override fun setSize(width: Int, height: Int) {
// better check min/max values of current window size
var w = width
@@ -273,10 +301,19 @@
window.setSize(w, h)
}
+ /**
+ * Sets the new position of the window on the screen.
+ *
+ * @param x the new x-coordinate of the window.
+ * @param y the new y-coordinate of the window.
+ */
override fun setLocation(x: Int, y: Int) {
window.setLocation(x, y)
}
+ /**
+ * Sets the window to the center of the screen.
+ */
override fun setWindowCentered() {
val dim: Dimension = Toolkit.getDefaultToolkit().getScreenSize()
val x = dim.width / 2 - width / 2
@@ -293,6 +330,11 @@
}
}
+ /**
+ * Shows a window with the given Compose content.
+ *
+ * @param content Composable content of the window.
+ */
@OptIn(ExperimentalKeyInput::class)
override fun show(content: @Composable () -> Unit) {
if (invoker != null) {
@@ -309,15 +351,18 @@
events.invokeOnOpen()
}
+ /**
+ * Closes the window.
+ */
override fun close() {
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
}
- override fun dispose() {
+ internal override fun dispose() {
invoker?.unlockWindow()
}
- override fun lockWindow() {
+ internal override fun lockWindow() {
window.apply {
defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE
setFocusableWindowState(false)
@@ -327,7 +372,7 @@
invoker?.connectPair(this)
}
- override fun unlockWindow() {
+ internal override fun unlockWindow() {
window.apply {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
setFocusableWindowState(true)
@@ -339,6 +384,9 @@
disconnectPair()
}
+ /**
+ * Gets the Keyboard object of the window.
+ */
@ExperimentalKeyInput
val keyboard: Keyboard = Keyboard()
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
index fbf5304..7d74767 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
@@ -18,8 +18,6 @@
package androidx.compose.desktop
-import org.jetbrains.skiko.Library
-
/**
* Can be called multiple times.
*
@@ -35,16 +33,6 @@
* }
* }
*/
-fun initCompose() {
- // call object initializer only once
- ComposeInit
-}
-
-private object ComposeInit {
- init {
- Library.load("/", "skiko")
- // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved
- // we have to set this property manually for coroutines to work.
- System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
- }
-}
\ No newline at end of file
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("We don't need to init Compose explicitly now")
+fun initCompose() = Unit
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
index 3524a49..c189e14 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
@@ -16,12 +16,16 @@
package androidx.compose.desktop
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.platform.DesktopComponent
+import androidx.compose.ui.platform.DesktopOwner
import androidx.compose.ui.platform.DesktopOwners
import androidx.compose.ui.platform.FrameDispatcher
+import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.Density
import org.jetbrains.skija.Canvas
import org.jetbrains.skiko.HardwareLayer
@@ -137,12 +141,14 @@
private fun initCanvas() {
wrapped.addInputMethodListener(object : InputMethodListener {
- override fun caretPositionChanged(p0: InputMethodEvent?) {
- TODO("Implement input method caret change")
+ override fun caretPositionChanged(event: InputMethodEvent?) {
+ if (event != null) {
+ owners?.onInputMethodEvent(event)
+ }
}
override fun inputMethodTextChanged(event: InputMethodEvent) = events.post {
- owners?.onInputMethodTextChanged(event)
+ owners?.onInputMethodEvent(event)
}
})
@@ -277,7 +283,7 @@
isDisposed = true
}
- fun needRedrawLayer() {
+ internal fun needRedrawLayer() {
check(!isDisposed)
frameDispatcher.scheduleFrame()
}
@@ -285,4 +291,28 @@
interface Renderer {
suspend fun onFrame(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
}
+
+ internal fun setContent(
+ parent: Any? = null,
+ invalidate: () -> Unit = this::needRedrawLayer,
+ content: @Composable () -> Unit
+ ): Composition {
+ check(owners == null) {
+ "Cannot setContent twice."
+ }
+ val desktopOwners = DesktopOwners(wrapped, invalidate)
+ val desktopOwner = DesktopOwner(desktopOwners, density)
+
+ owners = desktopOwners
+ val composition = desktopOwner.setContent(content)
+
+ onDensityChanged(desktopOwner::density::set)
+
+ when (parent) {
+ is AppFrame -> parent.onDispose = desktopOwner::dispose
+ is ComposePanel -> parent.onDispose = desktopOwner::dispose
+ }
+
+ return composition
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
index fa2ca73..6915ec41 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
@@ -27,12 +27,6 @@
* ComposePanel is panel for building UI using Compose for Desktop.
*/
class ComposePanel : JPanel {
- companion object {
- init {
- initCompose()
- }
- }
-
constructor() : super() {
setLayout(GridLayout(1, 1))
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
index 84257ff..b980f90 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
@@ -15,17 +15,13 @@
*/
package androidx.compose.desktop
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
class ComposeWindow : JFrame {
- companion object {
- init {
- initCompose()
- }
- }
-
val parent: AppFrame
internal val layer = ComposeLayer()
@@ -43,6 +39,21 @@
})
}
+ /**
+ * Sets Compose content of the ComposeWindow.
+ *
+ * @param content Composable content of the ComposeWindow.
+ *
+ * @return Composition of the content.
+ */
+ fun setContent(content: @Composable () -> Unit): Composition {
+ return layer.setContent(
+ parent = parent,
+ invalidate = this::needRedrawLayer,
+ content = content
+ )
+ }
+
private fun updateLayer() {
if (!isVisible) {
return
@@ -50,7 +61,7 @@
layer.updateLayer()
}
- fun needRedrawLayer() {
+ internal fun needRedrawLayer() {
if (!isVisible) {
return
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
deleted file mode 100644
index 58b5578..0000000
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
+++ /dev/null
@@ -1,60 +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.desktop
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Composition
-import androidx.compose.ui.platform.DesktopOwner
-import androidx.compose.ui.platform.DesktopOwners
-import androidx.compose.ui.platform.setContent
-
-/**
- * Sets Compose content of the ComposeWindow.
- *
- * @param content Composable content of the ComposeWindow.
- *
- * @return Composition of the content.
- */
-fun ComposeWindow.setContent(content: @Composable () -> Unit): Composition {
- return this.layer.setContent(
- parent = parent,
- invalidate = this::needRedrawLayer,
- content = content
- )
-}
-
-internal fun ComposeLayer.setContent(
- parent: Any? = null,
- invalidate: () -> Unit = this::needRedrawLayer,
- content: @Composable () -> Unit
-): Composition {
- check(owners == null) {
- "Cannot setContent twice."
- }
- val owners = DesktopOwners(this.wrapped, invalidate)
- val owner = DesktopOwner(owners, density)
- this.owners = owners
- val composition = owner.setContent(content)
-
- onDensityChanged(owner::density::set)
-
- when (parent) {
- is AppFrame -> parent.onDispose = owner::dispose
- is ComposePanel -> parent.onDispose = owner::dispose
- }
-
- return composition
-}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
index fb56f9a..d615e5e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
@@ -156,8 +156,19 @@
platformInputService.onKeyTyped(event.keyChar)
}
- fun onInputMethodTextChanged(event: InputMethodEvent) {
- platformInputService.onInputMethodTextChanged(event)
+ fun onInputMethodEvent(event: InputMethodEvent) {
+ if (!event.isConsumed()) {
+ when (event.id) {
+ InputMethodEvent.INPUT_METHOD_TEXT_CHANGED -> {
+ platformInputService.replaceInputMethodText(event)
+ event.consume()
+ }
+ InputMethodEvent.CARET_POSITION_CHANGED -> {
+ platformInputService.inputMethodCaretPositionChanged(event)
+ event.consume()
+ }
+ }
+ }
}
private fun pointerInputEvent(x: Int, y: Int, down: Boolean): PointerInputEvent {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
index a455fb6..30a941e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
@@ -155,9 +155,17 @@
}
}
- fun onInputMethodTextChanged(event: InputMethodEvent) {
+ internal fun inputMethodCaretPositionChanged(
+ @Suppress("UNUSED_PARAMETER") event: InputMethodEvent
+ ) {
+ // Which OSes and which input method could produce such events? We need to have some
+ // specific cases in mind before implementing this
+ println("DesktopInputComponent.inputMethodCaretPositionChanged")
+ }
+
+ internal fun replaceInputMethodText(event: InputMethodEvent) {
currentInput?.let { input ->
- if (event.caret == null) {
+ if (event.text == null) {
return
}
val committed = event.text.toStringUntil(event.committedCharacterCount)
@@ -173,12 +181,15 @@
ops.add(DeleteSurroundingTextInCodePointsEditOp(1, 0))
}
+ // newCursorPosition == 1 leads to effectively ignoring of this parameter in EditOps
+ // processing. the cursor will be set after the inserted text.
if (committed.isNotEmpty()) {
- ops.add(CommitTextEditOp(committed, committed.length))
+ ops.add(CommitTextEditOp(committed, 1))
}
if (composing.isNotEmpty()) {
- ops.add(SetComposingTextEditOp(composing, composing.length))
+ ops.add(SetComposingTextEditOp(composing, 1))
}
+
input.onEditCommand.invoke(ops)
}
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
index 2df9345..545b562 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
@@ -17,17 +17,20 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.DesktopOwner
import androidx.compose.ui.platform.DesktopOwnersAmbient
import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntBounds
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.round
@Composable
internal actual fun ActualPopup(
@@ -49,6 +52,24 @@
) {
val owners = DesktopOwnersAmbient.current
val density = AmbientDensity.current
+
+ var parentBounds = remember { mutableStateOf(IntBounds(0, 0, 0, 0)) }
+
+ // getting parent bounds
+ Layout(
+ content = {},
+ modifier = Modifier.onGloballyPositioned { childCoordinates ->
+ val coordinates = childCoordinates.parentCoordinates!!
+ parentBounds.value = IntBounds(
+ coordinates.localToGlobal(Offset.Zero).round(),
+ coordinates.size
+ )
+ },
+ measureBlock = { _, _ ->
+ layout(0, 0) {}
+ }
+ )
+
val owner = remember {
val owner = DesktopOwner(owners, density)
owner.setContent {
@@ -60,16 +81,24 @@
}
},
measureBlock = { measurables, constraints ->
- val width = owner.size.width
- val height = owner.size.height
- layout(width, height) {
+ val width = constraints.maxWidth
+ val height = constraints.maxHeight
+
+ val windowBounds = IntBounds(
+ left = 0,
+ top = 0,
+ right = width,
+ bottom = height
+ )
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
measurables.forEach {
val placeable = it.measure(constraints)
val offset = popupPositionProvider.calculatePosition(
- IntBounds(0, 0, width, height),
- IntBounds(0, 0, width, height),
- LayoutDirection.Ltr,
- IntSize(placeable.width, placeable.height)
+ parentGlobalBounds = parentBounds.value,
+ windowGlobalBounds = windowBounds,
+ layoutDirection = layoutDirection,
+ popupContentSize = IntSize(placeable.width, placeable.height)
)
placeable.place(offset.x, offset.y)
}
@@ -83,4 +112,4 @@
onDispose {
owner.dispose()
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
index 6a7f0d2..3672743 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
@@ -15,23 +15,39 @@
*/
package androidx.compose.ui.window
+import org.jetbrains.skiko.Library
import java.awt.event.ActionListener
import java.awt.event.ActionEvent
import javax.swing.JMenu
import javax.swing.JMenuBar
import javax.swing.JMenuItem
+/**
+ * MenuBar is a class that represents a menu bar that can be attached to a window.
+ * The menu bar can be displayed inside a window (Windows, Linux) or at the top of
+ * the screen (Mac OS).
+ */
class MenuBar {
internal var menuBar: JMenuBar
init {
menuBar = JMenuBar()
+ // For the MenuBar to work correctly, we need to set the skiko system properties
+ Library.load()
}
+ /**
+ * Constructs a MenuBar with the given menus.
+ *
+ * @param menu MenuBar menus.
+ */
constructor(vararg menu: Menu) {
menu(menu)
}
+ /**
+ * Adds additional menus to the MenuBar.
+ */
fun add(vararg menu: Menu) {
menu(menu)
}
@@ -55,10 +71,26 @@
}
}
+/**
+ * Menu is a class that represents a menu on a menu bar.
+ */
class Menu {
+ /**
+ * Gets the menu name.
+ */
val name: String
+
+ /**
+ * Gets the menu items.
+ */
val list: List<MenuItem>
+ /**
+ * Constructs a Menu with the given name and menu items.
+ *
+ * @param name Menu name.
+ * @param item Menu items.
+ */
constructor(name: String, vararg item: MenuItem) {
this.name = name
this.list = item.asList()
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
index cfd4ae3..d83546f 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
@@ -19,11 +19,34 @@
import java.awt.Toolkit
import javax.swing.KeyStroke
+/**
+ * MenuItem is a class that represents an implementation of an item in a menu.
+ * Can be used with Menu or Tray.
+ */
class MenuItem {
+
+ /**
+ * Gets the MenuItem name.
+ */
val name: String
+
+ /**
+ * Gets the MenuItem action when it is clicked or a keyboard shortcut is pressed (if specified).
+ */
val action: (() -> Unit)?
+
+ /**
+ * Gets the MenuItem shortcut.
+ */
val shortcut: KeyStroke
+ /**
+ * Constructs a MenuItem with the given name, action, and keyboard shortcut.
+ *
+ * @param name MenuItem name.
+ * @param onClick MenuItem action.
+ * @param shortcut MenuItem keyboard shortcut.
+ */
constructor(
name: String,
onClick: (() -> Unit)? = null,
@@ -35,6 +58,14 @@
}
}
+/**
+ * Returns KeyStroke for the given key. KeyStroke contains a OS-specific modifier
+ * (Command on Mac OS and Control on Windows and Linux) and the given key.
+ *
+ * @param key Keyboard key.
+ *
+ * @return KeyStroke for the given key.
+ */
fun KeyStroke(key: Key): KeyStroke {
return KeyStroke.getKeyStroke(
key.keyCode,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
index d6e05cb..084d72b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
@@ -21,15 +21,37 @@
import java.awt.TrayIcon
import java.awt.TrayIcon.MessageType
+/**
+ * Notifier is a class that can send system notifications.
+ */
class Notifier {
+
+ /**
+ * Sends a regular notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun notify(title: String, message: String) {
send(title, message, MessageType.INFO)
}
+ /**
+ * Sends a warning notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun warn(title: String, message: String) {
send(title, message, MessageType.WARNING)
}
+ /**
+ * Sends a error notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun error(title: String, message: String) {
send(title, message, MessageType.ERROR)
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
index 3bb7faa..5e51ec8 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
@@ -23,6 +23,9 @@
import java.awt.TrayIcon
import java.awt.TrayIcon.MessageType
+/**
+ * Tray is class for working with the system tray.
+ */
class Tray {
private lateinit var trayIcon: TrayIcon
private var init: Boolean = false
@@ -31,8 +34,16 @@
init = SystemTray.isSupported()
}
+ /**
+ * Constructs a Tray with the empty icon image.
+ */
constructor() {}
+ /**
+ * Constructs a Tray with the given icon image.
+ *
+ * @param image Tray icon image.
+ */
constructor(image: Image) {
if (!init) {
return
@@ -42,6 +53,12 @@
trayIcon.setImageAutoSize(true)
}
+ /**
+ * Constructs a Tray with the given icon image and tooltip.
+ *
+ * @param image Tray icon image.
+ * @param tooltip Tray icon tooltip.
+ */
constructor(image: Image, tooltip: String) {
if (!init) {
return
@@ -51,6 +68,11 @@
trayIcon.setImageAutoSize(true)
}
+ /**
+ * Sets the Tray icon image.
+ *
+ * @param image Tray icon image.
+ */
fun icon(image: Image) {
if (!init) {
return
@@ -63,6 +85,11 @@
}
}
+ /**
+ * Sets the Tray menu.
+ *
+ * @param item Menu items.
+ */
fun menu(vararg item: MenuItem) {
if (!init) {
return
@@ -85,6 +112,12 @@
}
}
+ /**
+ * Sends a regular notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun notify(title: String, message: String) {
if (!init) {
return
@@ -92,6 +125,12 @@
trayIcon.displayMessage(title, message, MessageType.INFO)
}
+ /**
+ * Sends a warning notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun warn(title: String, message: String) {
if (!init) {
return
@@ -99,6 +138,12 @@
trayIcon.displayMessage(title, message, MessageType.WARNING)
}
+ /**
+ * Sends a error notification with the given title and message.
+ *
+ * @param title Notification title.
+ * @param message Notification message.
+ */
fun error(title: String, message: String) {
if (!init) {
return
@@ -106,6 +151,9 @@
trayIcon.displayMessage(title, message, MessageType.ERROR)
}
+ /**
+ * Removes the tray icon from the system tray.
+ */
fun remove() {
try {
SystemTray.getSystemTray().remove(trayIcon)
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
similarity index 97%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
index 27a16c4..078976f 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.desktop
-import androidx.compose.ui.test.TestThread
import org.junit.Assert.assertTrue
import org.junit.Test
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
similarity index 96%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
index f7f4eb3..f98d967 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.ui.test
+package androidx.compose.desktop
internal class TestThread(private val _run: () -> Unit) : Thread() {
private var exception: Exception? = null
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
new file mode 100644
index 0000000..e7e98ab
--- /dev/null
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.ui.platform
+
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.EditProcessor
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TextInputService
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.awt.Component
+import java.awt.event.InputMethodEvent
+import java.text.AttributedString
+
+private object DummyComponent : Component()
+
+@RunWith(JUnit4::class)
+class DesktopInputComponentTest {
+ @OptIn(InternalTextApi::class)
+ @Test
+ fun replaceInputMethodText_basic() {
+ val processor = EditProcessor()
+
+ val input = DesktopPlatformInput(DummyDesktopComponent)
+ val inputService = TextInputService(input)
+
+ val token = inputService.startInput(
+ TextFieldValue(),
+ ImeOptions.Default,
+ processor::onEditCommands,
+ {}
+ )
+
+ processor.onNewState(TextFieldValue("h"), inputService, token)
+
+ val familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66"
+
+ input.replaceInputMethodText(
+ InputMethodEvent(
+ DummyComponent,
+ InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
+ AttributedString(familyEmoji).iterator,
+ 11,
+ null,
+ null
+ )
+ )
+
+ val buffer = processor.mBufferState
+
+ Assert.assertEquals("${familyEmoji}h", buffer.text)
+ Assert.assertEquals(TextRange(11), buffer.selection)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index 1e5c29a..10adbae 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -39,8 +39,8 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
-import androidx.compose.ui.graphicsLayer
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.layout.Layout
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
index e30a9da..b637b80 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
@@ -22,12 +22,12 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
-import androidx.compose.ui.TransformOrigin
-import androidx.compose.ui.graphicsLayer
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.test.TestComposeWindow
+import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.unit.dp
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index c17175e..6c3c4b2 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -20,7 +20,6 @@
import androidx.compose.runtime.Providers
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.test.initCompose
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -34,7 +33,6 @@
platform: DesktopPlatform = DesktopPlatform.Linux,
block: suspend RenderingTestScope.() -> Unit
) = runBlocking(Dispatchers.Main) {
- initCompose()
val scope = RenderingTestScope(width, height, platform)
try {
scope.block()
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
index fb44ef2..e697605 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
@@ -16,15 +16,15 @@
package androidx.compose.ui.platform
-import androidx.compose.ui.TransformOrigin
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
-import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
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 73bcf4c..b954d68 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
@@ -15,7 +15,7 @@
*/
package androidx.compose.ui.node
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 3c70810..729b498 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2227,6 +2227,7 @@
method public static int checkArgumentInRange(int, int, int, String);
method @IntRange(from=0) public static int checkArgumentNonnegative(int, String?);
method @IntRange(from=0) public static int checkArgumentNonnegative(int);
+ method public static int checkFlagsArgument(int, int);
method public static <T> T checkNotNull(T?);
method public static <T> T checkNotNull(T?, Object);
method public static void checkState(boolean, String?);
diff --git a/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
new file mode 100644
index 0000000..3d9b90e
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.core.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PreconditionsTest {
+
+ @Test
+ public void testCheckFlagsArgument() throws Exception {
+ final int allowedFlags = (1 << 0) | (1 << 1) | (1 << 2);
+
+ int flags = 0;
+ assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+ flags = (1 << 1) | (1 << 2);
+ assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+ flags = (1 << 3);
+ try {
+ Preconditions.checkFlagsArgument(flags, allowedFlags);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) { }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index 9f8833a..9a759f8 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -164,6 +164,20 @@
}
/**
+ * Check the requested flags, throwing if any requested flags are outside the allowed set.
+ *
+ * @return the validated requested flags.
+ */
+ public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
+ if ((requestedFlags & allowedFlags) != requestedFlags) {
+ throw new IllegalArgumentException("Requested flags 0x"
+ + Integer.toHexString(requestedFlags) + ", but only 0x"
+ + Integer.toHexString(allowedFlags) + " are allowed");
+ }
+ return requestedFlags;
+ }
+
+ /**
* Ensures that that the argument numeric value is non-negative.
*
* @param value a numeric int value
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
index 84d597a..37548c3 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
@@ -28,8 +28,9 @@
*/
public object DataStoreFactory {
/**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one DataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton.
*
* T is the type DataStore acts on. The type T must be immutable. Mutating a type used in
* DataStore invalidates any guarantees that DataStore provides and will result in
@@ -47,6 +48,8 @@
* @param produceFile Function which returns the file that the new DataStore will act on. The
* function must return the same path every time. No two instances of DataStore should act on
* the same file at the same time.
+ *
+ * @return a new DataStore instance with the provided configuration
*/
@JvmOverloads // Generate constructors for default params for java users.
public fun <T> create(
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
index fcdddd7..1dbcac4 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
@@ -300,11 +300,16 @@
}
if (!scratchFile.renameTo(file)) {
- throw IOException("$scratchFile could not be renamed to $file")
+ throw IOException(
+ "Unable to rename $scratchFile." +
+ "This likely means that there are multiple instances of DataStore " +
+ "for this file. Ensure that you are only creating a single instance of " +
+ "datastore for this file."
+ )
}
} catch (ex: IOException) {
if (scratchFile.exists()) {
- scratchFile.delete()
+ scratchFile.delete() // Swallow failure to delete
}
throw ex
}
diff --git a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
index a3fedbc..ed8110d 100644
--- a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
+++ b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
@@ -31,8 +31,9 @@
*/
public object PreferenceDataStoreFactory {
/**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton.
*
* @param produceFile Function which returns the file that the new DataStore will act on.
* The function must return the same path every time. No two instances of PreferenceDataStore
@@ -49,6 +50,8 @@
* The function must return the same path every time. No two instances of PreferenceDataStore
* should act on the same file at the same time. The file must have the extension
* preferences_pb.
+ *
+ * @return a new DataStore instance with the provided configuration
*/
@JvmOverloads
public fun create(
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
index 245df17..b25b66b 100644
--- a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
+++ b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
@@ -31,8 +31,11 @@
import java.io.File
/**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton. Note: this function is *not*
+ * comparable to [Context.getSharedPreferences]; this function creates and returns a
+ * new instance of DataStore each time it is called.
*
* @param name The name of the preferences. The preferences will be stored in a file obtained
* by calling: File(context.filesDir, "datastore/" + name + ".preferences_pb")
@@ -43,6 +46,8 @@
* may be run more than once whether or not it already succeeded (potentially because another
* migration failed or a write to disk failed.)
* @param scope The scope in which IO operations and transform functions will execute.
+ *
+ * @return a new DataStore instance with the provided configuration
*/
public fun Context.createDataStore(
name: String,
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt b/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
index b3274d2..b37786a 100644
--- a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
+++ b/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
@@ -28,8 +28,11 @@
import java.io.File
/**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton. Note: this function is *not*
+ * comparable to [Context.getSharedPreferences]; this function creates and returns a
+ * new instance of DataStore each time it is called.
*
* @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
* obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
@@ -42,6 +45,8 @@
* may be run more than once whether or not it already succeeded (potentially because another
* migration failed or a write to disk failed.)
* @param scope The scope in which IO operations and transform functions will execute.
+ *
+ * @return a new DataStore instance with the provided configuration
*/
public fun <T> Context.createDataStore(
fileName: String,
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index cf48ed2..537f909 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -653,4 +653,19 @@
Attempt to define local of type int as it\$iv:java\.lang\.Object
java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, kotlin\.coroutines\.Continuation\)
# > Task :hilt:integration-tests:hilt-testapp-viewmodel:kaptDebugKotlin
-warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
\ No newline at end of file
+warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
+# > Task :preference:preference:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
+# > Task :viewpager2:viewpager2:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/HostFragmentBackStackTest\.kt\: \([0-9]+\, [0-9]+\)\: \'enableDebugLogging\(Boolean\)\: Unit\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for systemWindowInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for stableInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemWindowInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setStableInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTappableElementInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setMandatorySystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeSystemWindowInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: Unnecessary non\-null assertion \(\!\!\) on a non\-null receiver of type WindowInsetsCompat
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeStableInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeDisplayCutout\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 5a145f5..3a97ddb 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,8 +4,8 @@
}
dependencies {
- docs("androidx.activity:activity:1.2.0-beta01")
- docs("androidx.activity:activity-ktx:1.2.0-beta01")
+ docs("androidx.activity:activity:1.2.0-beta02")
+ docs("androidx.activity:activity-ktx:1.2.0-beta02")
docs("androidx.ads:ads-identifier:1.0.0-alpha04")
docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
docs("androidx.annotation:annotation:1.2.0-alpha01")
@@ -19,8 +19,9 @@
docs("androidx.autofill:autofill:1.1.0-rc01")
docs("androidx.benchmark:benchmark-common:1.1.0-alpha01")
docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha01")
- docs("androidx.biometric:biometric:1.1.0-rc01")
- docs("androidx.browser:browser:1.3.0-rc01")
+ docs("androidx.biometric:biometric:1.2.0-alpha01")
+ docs("androidx.biometric:biometric-ktx:1.2.0-alpha01")
+ docs("androidx.browser:browser:1.3.0")
docs("androidx.camera:camera-camera2:1.0.0-beta12")
docs("androidx.camera:camera-core:1.0.0-beta12")
docs("androidx.camera:camera-extensions:1.0.0-alpha19")
@@ -30,41 +31,44 @@
docs("androidx.cardview:cardview:1.0.0")
docs("androidx.collection:collection:1.1.0")
docs("androidx.collection:collection-ktx:1.1.0")
- docs("androidx.compose.animation:animation:1.0.0-alpha07")
- docs("androidx.compose.animation:animation-core:1.0.0-alpha07")
- samples("androidx.compose.animation:animation-samples:1.0.0-alpha07")
- samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha07")
- docs("androidx.compose.foundation:foundation:1.0.0-alpha07")
- docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha07")
- samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha07")
- samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha07")
- docs("androidx.compose.material:material:1.0.0-alpha07")
- docs("androidx.compose.material:material-icons-core:1.0.0-alpha07")
- samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha07")
- docs("androidx.compose.material:material-icons-extended:1.0.0-alpha07")
- samples("androidx.compose.material:material-samples:1.0.0-alpha07")
- docs("androidx.compose.runtime:runtime:1.0.0-alpha07")
- docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha07")
- docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha07")
- samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha07")
- docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha07")
- samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha07")
- docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha07")
- samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha07")
- samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha07")
- docs("androidx.compose.ui:ui:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-geometry:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-graphics:1.0.0-alpha07")
- samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-text:1.0.0-alpha07")
+ docs("androidx.compose.animation:animation:1.0.0-alpha08")
+ docs("androidx.compose.animation:animation-core:1.0.0-alpha08")
+ samples("androidx.compose.animation:animation-samples:1.0.0-alpha08")
+ samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha08")
+ docs("androidx.compose.foundation:foundation:1.0.0-alpha08")
+ docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha08")
+ samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha08")
+ samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha08")
+ docs("androidx.compose.material:material:1.0.0-alpha08")
+ docs("androidx.compose.material:material-icons-core:1.0.0-alpha08")
+ samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha08")
+ docs("androidx.compose.material:material-icons-extended:1.0.0-alpha08")
+ samples("androidx.compose.material:material-samples:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha08")
+ samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha08")
+ samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime-rxjava3:1.0.0-alpha08")
+ docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha08")
+ samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha08")
+ samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-geometry:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-graphics:1.0.0-alpha08")
+ samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-test:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-test-junit4:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-text:1.0.0-alpha08")
docs("androidx.compose.ui:ui-text-android:1.0.0-alpha06")
- samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-unit:1.0.0-alpha07")
- samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-util:1.0.0-alpha07")
- docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha07")
- samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha07")
- samples("androidx.compose.ui:ui-samples:1.0.0-alpha07")
+ samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-unit:1.0.0-alpha08")
+ samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-util:1.0.0-alpha08")
+ docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha08")
+ samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha08")
+ samples("androidx.compose.ui:ui-samples:1.0.0-alpha08")
docs("androidx.concurrent:concurrent-futures:1.1.0")
docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
docs("androidx.contentpager:contentpager:1.0.0")
@@ -76,10 +80,10 @@
docs("androidx.core:core-ktx:1.5.0-alpha05")
docs("androidx.cursoradapter:cursoradapter:1.0.0")
docs("androidx.customview:customview:1.1.0")
- docs("androidx.datastore:datastore:1.0.0-alpha04")
- docs("androidx.datastore:datastore-core:1.0.0-alpha04")
- docs("androidx.datastore:datastore-preferences:1.0.0-alpha04")
- docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha04")
+ docs("androidx.datastore:datastore:1.0.0-alpha05")
+ docs("androidx.datastore:datastore-core:1.0.0-alpha05")
+ docs("androidx.datastore:datastore-preferences:1.0.0-alpha05")
+ docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha05")
docs("androidx.documentfile:documentfile:1.0.0")
docs("androidx.drawerlayout:drawerlayout:1.1.1")
docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
@@ -87,9 +91,9 @@
docs("androidx.emoji:emoji:1.2.0-alpha01")
docs("androidx.emoji:emoji-appcompat:1.2.0-alpha01")
docs("androidx.emoji:emoji-bundled:1.2.0-alpha01")
- docs("androidx.enterprise:enterprise-feedback:1.1.0-beta01")
- docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-beta01")
- docs("androidx.exifinterface:exifinterface:1.3.1")
+ docs("androidx.enterprise:enterprise-feedback:1.1.0-rc01")
+ docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-rc01")
+ docs("androidx.exifinterface:exifinterface:1.3.2")
docs("androidx.fragment:fragment:1.3.0-beta01")
docs("androidx.fragment:fragment-ktx:1.3.0-beta01")
docs("androidx.fragment:fragment-testing:1.3.0-beta01")
@@ -99,10 +103,10 @@
docs("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02")
docs("androidx.hilt:hilt-work:1.0.0-alpha02")
docs("androidx.interpolator:interpolator:1.0.0")
- docs("androidx.leanback:leanback:1.1.0-alpha05")
- docs("androidx.leanback:leanback-paging:1.1.0-alpha05")
- docs("androidx.leanback:leanback-preference:1.1.0-alpha05")
- docs("androidx.leanback:leanback-tab:1.1.0-alpha05")
+ docs("androidx.leanback:leanback:1.1.0-beta01")
+ docs("androidx.leanback:leanback-paging:1.1.0-alpha06")
+ docs("androidx.leanback:leanback-preference:1.1.0-beta01")
+ docs("androidx.leanback:leanback-tab:1.1.0-beta01")
docs("androidx.lifecycle:lifecycle-common:2.3.0-beta01")
docs("androidx.lifecycle:lifecycle-common-java8:2.3.0-beta01")
docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -121,36 +125,36 @@
docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
docs("androidx.loader:loader:1.1.0")
docs("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01")
- docs("androidx.media2:media2-common:1.1.0-rc01")
- docs("androidx.media2:media2-player:1.1.0-rc01")
- docs("androidx.media2:media2-session:1.1.0-rc01")
- docs("androidx.media2:media2-widget:1.1.0-rc01")
- docs("androidx.media:media:1.2.0")
+ docs("androidx.media2:media2-common:1.1.0")
+ docs("androidx.media2:media2-player:1.1.0")
+ docs("androidx.media2:media2-session:1.1.0")
+ docs("androidx.media2:media2-widget:1.1.0")
+ docs("androidx.media:media:1.2.1")
docs("androidx.mediarouter:mediarouter:1.2.0")
- docs("androidx.navigation:navigation-common:2.3.1")
- docs("androidx.navigation:navigation-common-ktx:2.3.1")
- docs("androidx.navigation:navigation-compose:1.0.0-alpha02")
+ docs("androidx.navigation:navigation-common:2.3.2")
+ docs("androidx.navigation:navigation-common-ktx:2.3.2")
+ docs("androidx.navigation:navigation-compose:1.0.0-alpha03")
samples("androidx.navigation:navigation-compose-samples:1.0.0-alpha01")
- docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.1")
- docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.1")
- docs("androidx.navigation:navigation-fragment:2.3.1")
- docs("androidx.navigation:navigation-fragment-ktx:2.3.1")
- docs("androidx.navigation:navigation-runtime:2.3.1")
- docs("androidx.navigation:navigation-runtime-ktx:2.3.1")
- docs("androidx.navigation:navigation-testing:2.3.1")
- docs("androidx.navigation:navigation-ui:2.3.1")
- docs("androidx.navigation:navigation-ui-ktx:2.3.1")
- docs("androidx.paging:paging-common:3.0.0-alpha09")
- docs("androidx.paging:paging-common-ktx:3.0.0-alpha09")
- docs("androidx.paging:paging-compose:1.0.0-alpha02")
+ docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.2")
+ docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.2")
+ docs("androidx.navigation:navigation-fragment:2.3.2")
+ docs("androidx.navigation:navigation-fragment-ktx:2.3.2")
+ docs("androidx.navigation:navigation-runtime:2.3.2")
+ docs("androidx.navigation:navigation-runtime-ktx:2.3.2")
+ docs("androidx.navigation:navigation-testing:2.3.2")
+ docs("androidx.navigation:navigation-ui:2.3.2")
+ docs("androidx.navigation:navigation-ui-ktx:2.3.2")
+ docs("androidx.paging:paging-common:3.0.0-alpha10")
+ docs("androidx.paging:paging-common-ktx:3.0.0-alpha10")
+ docs("androidx.paging:paging-compose:1.0.0-alpha03")
samples("androidx.paging:paging-compose-samples:3.0.0-alpha08")
- docs("androidx.paging:paging-guava:3.0.0-alpha09")
- docs("androidx.paging:paging-runtime:3.0.0-alpha09")
- docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha09")
- docs("androidx.paging:paging-rxjava2:3.0.0-alpha09")
- docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha09")
- docs("androidx.paging:paging-rxjava3:3.0.0-alpha09")
- samples("androidx.paging:paging-samples:3.0.0-alpha09")
+ docs("androidx.paging:paging-guava:3.0.0-alpha10")
+ docs("androidx.paging:paging-runtime:3.0.0-alpha10")
+ docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha10")
+ docs("androidx.paging:paging-rxjava2:3.0.0-alpha10")
+ docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha10")
+ docs("androidx.paging:paging-rxjava3:3.0.0-alpha10")
+ samples("androidx.paging:paging-samples:3.0.0-alpha10")
docs("androidx.palette:palette:1.0.0")
docs("androidx.palette:palette-ktx:1.0.0")
docs("androidx.percentlayout:percentlayout:1.0.1")
@@ -158,7 +162,7 @@
docs("androidx.preference:preference-ktx:1.1.1")
docs("androidx.print:print:1.1.0-beta01")
docs("androidx.recommendation:recommendation:1.0.0")
- docs("androidx.recyclerview:recyclerview:1.2.0-alpha06")
+ docs("androidx.recyclerview:recyclerview:1.2.0-beta01")
docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
docs("androidx.room:room-common:2.3.0-alpha03")
@@ -170,10 +174,10 @@
docs("androidx.room:room-testing:2.3.0-alpha03")
docs("androidx.savedstate:savedstate:1.1.0-beta01")
docs("androidx.savedstate:savedstate-ktx:1.1.0-beta01")
- docs("androidx.security:security-crypto:1.1.0-alpha02")
+ docs("androidx.security:security-crypto:1.1.0-alpha03")
docs("androidx.security:security-crypto-ktx:1.1.0-alpha02")
docs("androidx.security:security-identity-credential:1.0.0-alpha01")
- docs("androidx.sharetarget:sharetarget:1.1.0-beta01")
+ docs("androidx.sharetarget:sharetarget:1.1.0-rc01")
docs("androidx.slice:slice-builders:1.1.0-alpha01")
docs("androidx.slice:slice-builders-ktx:1.0.0-alpha07")
docs("androidx.slice:slice-core:1.1.0-alpha01")
@@ -188,7 +192,8 @@
docs("androidx.textclassifier:textclassifier:1.0.0-alpha03")
docs("androidx.tracing:tracing:1.0.0")
docs("androidx.tracing:tracing-ktx:1.0.0")
- docs("androidx.transition:transition:1.4.0-beta01")
+ docs("androidx.transition:transition:1.4.0-rc01")
+ docs("androidx.transition:transition-ktx:1.4.0-rc01")
docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
docs("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -196,27 +201,27 @@
docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
docs("androidx.viewpager2:viewpager2:1.1.0-alpha01")
docs("androidx.viewpager:viewpager:1.0.0")
- docs("androidx.wear:wear:1.2.0-alpha02")
+ docs("androidx.wear:wear:1.2.0-alpha03")
stubs(fileTree(dir: '../wear/wear_stubs/', include: ['com.google.android.wearable-stubs.jar']))
- docs("androidx.wear:wear-complications-data:1.0.0-alpha02")
- docs("androidx.wear:wear-complications-provider:1.0.0-alpha02")
- docs("androidx.wear:wear-watchface:1.0.0-alpha02")
- docs("androidx.wear:wear-watchface-client:1.0.0-alpha02")
- docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha02")
- docs("androidx.wear:wear-watchface-data:1.0.0-alpha02")
+ docs("androidx.wear:wear-complications-data:1.0.0-alpha03")
+ docs("androidx.wear:wear-complications-provider:1.0.0-alpha03")
+ docs("androidx.wear:wear-watchface:1.0.0-alpha03")
+ docs("androidx.wear:wear-watchface-client:1.0.0-alpha03")
+ docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha03")
+ docs("androidx.wear:wear-watchface-data:1.0.0-alpha03")
samples("androidx.wear:wear-watchface-samples:1.0.0-alpha02")
- docs("androidx.wear:wear-watchface-style:1.0.0-alpha02")
- docs("androidx.wear:wear-input:1.0.0-rc01")
- docs("androidx.wear:wear-input-testing:1.0.0-rc01")
- docs("androidx.webkit:webkit:1.4.0-rc01")
+ docs("androidx.wear:wear-watchface-style:1.0.0-alpha03")
+ docs("androidx.wear:wear-input:1.0.0")
+ docs("androidx.wear:wear-input-testing:1.0.0")
+ docs("androidx.webkit:webkit:1.4.0-rc02")
docs("androidx.window:window:1.0.0-alpha01")
stubs(fileTree(dir: '../window/stubs/', include: ['window-sidecar-release-0.1.0-alpha01.aar']))
stubs(project(":window:window-extensions"))
- docs("androidx.work:work-gcm:2.5.0-beta01")
- docs("androidx.work:work-multiprocess:2.5.0-beta01")
- docs("androidx.work:work-runtime:2.5.0-beta01")
- docs("androidx.work:work-runtime-ktx:2.5.0-beta01")
- docs("androidx.work:work-rxjava2:2.5.0-beta01")
- docs("androidx.work:work-rxjava3:2.5.0-beta01")
- docs("androidx.work:work-testing:2.5.0-beta01")
+ docs("androidx.work:work-gcm:2.5.0-beta02")
+ docs("androidx.work:work-multiprocess:2.5.0-beta02")
+ docs("androidx.work:work-runtime:2.5.0-beta02")
+ docs("androidx.work:work-runtime-ktx:2.5.0-beta02")
+ docs("androidx.work:work-rxjava2:2.5.0-beta02")
+ docs("androidx.work:work-rxjava3:2.5.0-beta02")
+ docs("androidx.work:work-testing:2.5.0-beta02")
}
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 5899803..77b8b66 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -70,6 +70,8 @@
/**
* Test {@link ExifInterface}.
*/
+// TODO: Add NEF test file from CTS after reducing file size in order to test uncompressed thumbnail
+// image.
@RunWith(AndroidJUnit4.class)
public class ExifInterfaceTest {
private static final String TAG = ExifInterface.class.getSimpleName();
@@ -97,19 +99,20 @@
"jpeg_with_datetime_tag_primary_format.jpg";
private static final String JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT =
"jpeg_with_datetime_tag_secondary_format.jpg";
+ private static final String HEIC_WITH_EXIF = "heic_with_exif.heic";
private static final int[] IMAGE_RESOURCES = new int[] {
R.raw.jpeg_with_exif_byte_order_ii, R.raw.jpeg_with_exif_byte_order_mm,
R.raw.dng_with_exif_with_xmp, R.raw.jpeg_with_exif_with_xmp,
R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif, R.raw.webp_with_exif,
R.raw.webp_with_anim_without_exif, R.raw.webp_without_exif,
R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag_primary_format,
- R.raw.jpeg_with_datetime_tag_secondary_format};
+ R.raw.jpeg_with_datetime_tag_secondary_format, R.raw.heic_with_exif};
private static final String[] IMAGE_FILENAMES = new String[] {
JPEG_WITH_EXIF_BYTE_ORDER_II, JPEG_WITH_EXIF_BYTE_ORDER_MM, DNG_WITH_EXIF_WITH_XMP,
JPEG_WITH_EXIF_WITH_XMP, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
WEBP_WITH_EXIF, WEBP_WITHOUT_EXIF_WITH_ANIM_DATA, WEBP_WITHOUT_EXIF,
WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT,
- JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT};
+ JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT, HEIC_WITH_EXIF};
private static final int USER_READ_WRITE = 0600;
private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -455,6 +458,19 @@
writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING);
}
+ /**
+ * .heic file is a container for HEIF format images, which ExifInterface supports.
+ */
+ @Test
+ @LargeTest
+ public void testHeicFile() throws Throwable {
+ // TODO: Reading HEIC file for SDK < 28 throws an exception. Revisit once issue is solved.
+ // (b/172025296)
+ if (Build.VERSION.SDK_INT > 27) {
+ readFromFilesWithExif(HEIC_WITH_EXIF, R.array.heic_with_exif);
+ }
+ }
+
@Test
@LargeTest
public void testDoNotFailOnCorruptedImage() throws Throwable {
@@ -628,22 +644,32 @@
}
}
+ /**
+ * JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT contains the following tags:
+ * TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+ * TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+ * TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
+ */
@Test
@SmallTest
public void testGetSetDateTime() throws IOException {
- final long expectedDatetimeValue = 1454059947000L;
- final long expectedDatetimeOffsetLongValue = 32400000L; /* +09:00 converted to msec */
+ final long expectedGetDatetimeValue =
+ 1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+ + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+ + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+ // GPS datetime does not support subsec precision
+ final long expectedGetGpsDatetimeValue =
+ 1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+ + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
final String expectedDatetimeOffsetStringValue = "+09:00";
- final String dateTimeValue = "2017:02:02 22:22:22";
- final String dateTimeOriginalValue = "2017:01:01 11:11:11";
File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
- assertEquals(expectedDatetimeValue, (long) exif.getDateTime());
- assertEquals(expectedDatetimeValue, (long) exif.getDateTimeOriginal());
- assertEquals(expectedDatetimeValue, (long) exif.getDateTimeDigitized());
- assertEquals(expectedDatetimeValue, (long) exif.getGpsDateTime());
- // getDateTime() = TAG_DATETIME + TAG_OFFSET_TIME
+ // Test getting datetime values
+ assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
+ assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeOriginal());
+ assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeDigitized());
+ assertEquals(expectedGetGpsDatetimeValue, (long) exif.getGpsDateTime());
assertEquals(expectedDatetimeOffsetStringValue,
exif.getAttribute(ExifInterface.TAG_OFFSET_TIME));
assertEquals(expectedDatetimeOffsetStringValue,
@@ -651,24 +677,9 @@
assertEquals(expectedDatetimeOffsetStringValue,
exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED));
- exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
- exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
- exif.saveAttributes();
-
- // Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value.
- exif = new ExifInterface(imageFile.getAbsolutePath());
- assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
- assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
-
- // Now remove the DATETIME value.
- exif.setAttribute(ExifInterface.TAG_DATETIME, null);
- exif.saveAttributes();
-
- // When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
- exif = new ExifInterface(imageFile.getAbsolutePath());
- assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
-
- long currentTimeStamp = System.currentTimeMillis();
+ // Test setting datetime values
+ final long currentTimeStamp = System.currentTimeMillis();
+ final long expectedDatetimeOffsetLongValue = 32400000L;
exif.setDateTime(currentTimeStamp);
exif.saveAttributes();
exif = new ExifInterface(imageFile.getAbsolutePath());
@@ -685,30 +696,90 @@
* Setting a datetime tag value with the secondary format with
* {@link ExifInterface#setAttribute(String, String)} should automatically convert it to the
* primary format.
+ *
+ * JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT contains the following tags:
+ * TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+ * TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+ * TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
*/
@Test
@SmallTest
public void testGetSetDateTimeForSecondaryFormat() throws Exception {
- final long dateTimePrimaryFormatLongValue = 1604075491000L;
- final String dateTimePrimaryFormatStringValue = "2020-10-30 16:31:31";
+ // Test getting datetime values
+ final long expectedGetDatetimeValue =
+ 1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+ + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+ + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+ final String expectedDateTimeStringValue = "2016-01-29 18:32:27";
+
File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT);
-
- // Check that secondary format value is read correctly.
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
- assertEquals(dateTimePrimaryFormatStringValue,
+ assertEquals(expectedDateTimeStringValue,
exif.getAttribute(ExifInterface.TAG_DATETIME));
- assertEquals(dateTimePrimaryFormatLongValue, (long) exif.getDateTime());
+ assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
- final long dateTimeSecondaryFormatLongValue = 1577836800000L;
- final String dateTimeSecondaryFormatStringValue = "2020-01-01 00:00:00";
- final String modifiedDateTimeSecondaryFormatStringValue = "2020:01:01 00:00:00";
+ // Test setting datetime value: check that secondary format value is modified correctly
+ // when it is saved.
+ final long newDateTimeLongValue =
+ 1577772000000L /* TAG_DATETIME value ("2020-01-01 00:00:00") converted to msec */
+ + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+ + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+ final String newDateTimeStringValue = "2020-01-01 00:00:00";
+ final String modifiedNewDateTimeStringValue = "2020:01:01 00:00:00";
- // Check that secondary format value is written correctly.
- exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeSecondaryFormatStringValue);
+ exif.setAttribute(ExifInterface.TAG_DATETIME, newDateTimeStringValue);
exif.saveAttributes();
- assertEquals(modifiedDateTimeSecondaryFormatStringValue,
- exif.getAttribute(ExifInterface.TAG_DATETIME));
- assertEquals(dateTimeSecondaryFormatLongValue, (long) exif.getDateTime());
+ assertEquals(modifiedNewDateTimeStringValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+ assertEquals(newDateTimeLongValue, (long) exif.getDateTime());
+ }
+
+ @Test
+ @LargeTest
+ public void testAddDefaultValuesForCompatibility() throws Exception {
+ File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+ ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+
+ // 1. Check that the TAG_DATETIME value is not overwritten by TAG_DATETIME_ORIGINAL's value
+ // when TAG_DATETIME value exists.
+ final String dateTimeValue = "2017:02:02 22:22:22";
+ final String dateTimeOriginalValue = "2017:01:01 11:11:11";
+ exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+ assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+
+ // 2. Check that when TAG_DATETIME has no value, it is set to TAG_DATETIME_ORIGINAL's value.
+ exif.setAttribute(ExifInterface.TAG_DATETIME, null);
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+ }
+
+ // TODO: Add tests for other variations (e.g. single/double digit number strings)
+ @Test
+ @LargeTest
+ public void testParsingSubsec() throws IOException {
+ File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+ ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000000");
+ exif.saveAttributes();
+ long currentDateTimeValue = exif.getDateTime();
+
+ // Check that TAG_SUBSEC_TIME values starting with zero are supported.
+ // Note: getDateTime() supports only up to 1/1000th of a second.
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 1ms */ "001000");
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
+
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 10ms */ "010000");
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 100ms */ "100000");
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
}
@Test
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
new file mode 100644
index 0000000..ec03bfd
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
index f50e845..5930e2c 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
index 53ad4e9..9899556 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
index e6f342e..b3438c9 100644
--- a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
+++ b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
@@ -367,4 +367,48 @@
<item>0</item>
<item>0</item>
</array>
+ <array name="heic_with_exif">
+ <!--Whether thumbnail exists-->
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>0</item>
+ <item>0</item>
+ <item>false</item>
+ <!--Whether GPS LatLong information exists-->
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <!--Whether Make information exists-->
+ <item>true</item>
+ <item>3519</item>
+ <item>4</item>
+ <item>LGE</item>
+ <item>Nexus 5</item>
+ <item>0.0</item>
+ <item />
+ <item>0.0</item>
+ <item>0</item>
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item>1080</item>
+ <item>1920</item>
+ <item />
+ <item>1</item>
+ <item>0</item>
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ </array>
</resources>
\ No newline at end of file
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index c0347c4..76d6f81 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -16,6 +16,13 @@
package androidx.exifinterface.media;
+import static androidx.exifinterface.media.ExifInterfaceUtils.byteArrayToHexString;
+import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
+import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
+import static androidx.exifinterface.media.ExifInterfaceUtils.isSupportedFormatForSavingAttributes;
+import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
+import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
+
import android.annotation.SuppressLint;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
@@ -3835,21 +3842,21 @@
static final byte MARKER_EOI = (byte) 0xd9;
// Supported Image File Types
- private static final int IMAGE_TYPE_UNKNOWN = 0;
- private static final int IMAGE_TYPE_ARW = 1;
- private static final int IMAGE_TYPE_CR2 = 2;
- private static final int IMAGE_TYPE_DNG = 3;
- private static final int IMAGE_TYPE_JPEG = 4;
- private static final int IMAGE_TYPE_NEF = 5;
- private static final int IMAGE_TYPE_NRW = 6;
- private static final int IMAGE_TYPE_ORF = 7;
- private static final int IMAGE_TYPE_PEF = 8;
- private static final int IMAGE_TYPE_RAF = 9;
- private static final int IMAGE_TYPE_RW2 = 10;
- private static final int IMAGE_TYPE_SRW = 11;
- private static final int IMAGE_TYPE_HEIF = 12;
- private static final int IMAGE_TYPE_PNG = 13;
- private static final int IMAGE_TYPE_WEBP = 14;
+ static final int IMAGE_TYPE_UNKNOWN = 0;
+ static final int IMAGE_TYPE_ARW = 1;
+ static final int IMAGE_TYPE_CR2 = 2;
+ static final int IMAGE_TYPE_DNG = 3;
+ static final int IMAGE_TYPE_JPEG = 4;
+ static final int IMAGE_TYPE_NEF = 5;
+ static final int IMAGE_TYPE_NRW = 6;
+ static final int IMAGE_TYPE_ORF = 7;
+ static final int IMAGE_TYPE_PEF = 8;
+ static final int IMAGE_TYPE_RAF = 9;
+ static final int IMAGE_TYPE_RW2 = 10;
+ static final int IMAGE_TYPE_SRW = 11;
+ static final int IMAGE_TYPE_HEIF = 12;
+ static final int IMAGE_TYPE_PNG = 13;
+ static final int IMAGE_TYPE_WEBP = 14;
static {
sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
@@ -4694,7 +4701,7 @@
* </p>
*/
public void saveAttributes() throws IOException {
- if (!isSupportedFormatForSavingAttributes()) {
+ if (!isSupportedFormatForSavingAttributes(mMimeType)) {
throw new IOException("ExifInterface only supports saving attributes on JPEG, PNG, "
+ "or WebP formats.");
}
@@ -5152,6 +5159,10 @@
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
* Jan. 1, 1970, midnight local time.
+ *
+ * <p>Note: The return value includes the first three digits (or less depending on the length
+ * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
+ *
* @return null if date time information is unavailable or invalid.
*
* @hide
@@ -5167,6 +5178,10 @@
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
+ *
+ * <p>Note: The return value includes the first three digits (or less depending on the length
+ * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
+ *
* @return null if digitized date time information is unavailable or invalid.
*
* @hide
@@ -5182,6 +5197,10 @@
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
+ *
+ * <p>Note: The return value includes the first three digits (or less depending on the length
+ * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
+ *
* @return null if original date time information is unavailable or invalid.
*
* @hide
@@ -5224,15 +5243,7 @@
}
if (subSecs != null) {
- try {
- long sub = Long.parseLong(subSecs);
- while (sub > 1000) {
- sub /= 10;
- }
- msecs += sub;
- } catch (NumberFormatException e) {
- // Ignored
- }
+ msecs += parseSubSeconds(subSecs);
}
return msecs;
} catch (IllegalArgumentException e) {
@@ -8073,87 +8084,4 @@
Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
}
}
-
- /**
- * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
- * Returns the total number of bytes transferred.
- */
- private static int copy(InputStream in, OutputStream out) throws IOException {
- int total = 0;
- byte[] buffer = new byte[8192];
- int c;
- while ((c = in.read(buffer)) != -1) {
- total += c;
- out.write(buffer, 0, c);
- }
- return total;
- }
-
- /**
- * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
- * closed.
- */
- private static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
- int remainder = numBytes;
- byte[] buffer = new byte[8192];
- while (remainder > 0) {
- int bytesToRead = Math.min(remainder, 8192);
- int bytesRead = in.read(buffer, 0, bytesToRead);
- if (bytesRead != bytesToRead) {
- throw new IOException("Failed to copy the given amount of bytes from the input"
- + "stream to the output stream.");
- }
- remainder -= bytesRead;
- out.write(buffer, 0, bytesRead);
- }
- }
-
- /**
- * Convert given int[] to long[]. If long[] is given, just return it.
- * Return null for other types of input.
- */
- private static long[] convertToLongArray(Object inputObj) {
- if (inputObj instanceof int[]) {
- int[] input = (int[]) inputObj;
- long[] result = new long[input.length];
- for (int i = 0; i < input.length; i++) {
- result[i] = input[i];
- }
- return result;
- } else if (inputObj instanceof long[]) {
- return (long[]) inputObj;
- }
- return null;
- }
-
- private static boolean startsWith(byte[] cur, byte[] val) {
- if (cur == null || val == null) {
- return false;
- }
- if (cur.length < val.length) {
- return false;
- }
- for (int i = 0; i < val.length; i++) {
- if (cur[i] != val[i]) {
- return false;
- }
- }
- return true;
- }
-
- private static String byteArrayToHexString(byte[] bytes) {
- StringBuilder sb = new StringBuilder(bytes.length * 2);
- for (int i = 0; i < bytes.length; i++) {
- sb.append(String.format("%02x", bytes[i]));
- }
- return sb.toString();
- }
-
- private boolean isSupportedFormatForSavingAttributes() {
- if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_PNG
- || mMimeType == IMAGE_TYPE_WEBP) {
- return true;
- }
- return false;
- }
}
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
new file mode 100644
index 0000000..8c3390d
--- /dev/null
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
@@ -0,0 +1,129 @@
+/*
+ * 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.exifinterface.media;
+
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_JPEG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_PNG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_WEBP;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class ExifInterfaceUtils {
+ private static final String TAG = "ExifInterfaceUtils";
+
+ private ExifInterfaceUtils() {
+ // Prevent instantiation
+ }
+ /**
+ * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+ * Returns the total number of bytes transferred.
+ */
+ static int copy(InputStream in, OutputStream out) throws IOException {
+ int total = 0;
+ byte[] buffer = new byte[8192];
+ int c;
+ while ((c = in.read(buffer)) != -1) {
+ total += c;
+ out.write(buffer, 0, c);
+ }
+ return total;
+ }
+
+ /**
+ * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
+ * closed.
+ */
+ static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
+ int remainder = numBytes;
+ byte[] buffer = new byte[8192];
+ while (remainder > 0) {
+ int bytesToRead = Math.min(remainder, 8192);
+ int bytesRead = in.read(buffer, 0, bytesToRead);
+ if (bytesRead != bytesToRead) {
+ throw new IOException("Failed to copy the given amount of bytes from the input"
+ + "stream to the output stream.");
+ }
+ remainder -= bytesRead;
+ out.write(buffer, 0, bytesRead);
+ }
+ }
+
+ /**
+ * Convert given int[] to long[]. If long[] is given, just return it.
+ * Return null for other types of input.
+ */
+ static long[] convertToLongArray(Object inputObj) {
+ if (inputObj instanceof int[]) {
+ int[] input = (int[]) inputObj;
+ long[] result = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ result[i] = input[i];
+ }
+ return result;
+ } else if (inputObj instanceof long[]) {
+ return (long[]) inputObj;
+ }
+ return null;
+ }
+
+ static boolean startsWith(byte[] cur, byte[] val) {
+ if (cur == null || val == null) {
+ return false;
+ }
+ if (cur.length < val.length) {
+ return false;
+ }
+ for (int i = 0; i < val.length; i++) {
+ if (cur[i] != val[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static boolean isSupportedFormatForSavingAttributes(int mimeType) {
+ if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG
+ || mimeType == IMAGE_TYPE_WEBP) {
+ return true;
+ }
+ return false;
+ }
+
+ static String byteArrayToHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (int i = 0; i < bytes.length; i++) {
+ sb.append(String.format("%02x", bytes[i]));
+ }
+ return sb.toString();
+ }
+
+ static long parseSubSeconds(String subSec) {
+ try {
+ final int len = Math.min(subSec.length(), 3);
+ long sub = Long.parseLong(subSec.substring(0, len));
+ for (int i = len; i < 3; i++) {
+ sub *= 10;
+ }
+ return sub;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ return 0L;
+ }
+}
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 6cfa8e7..88435eb 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -36,11 +36,11 @@
api("androidx.collection:collection-ktx:1.1.0") {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0-beta01") {
+ api(projectOrArtifact(":lifecycle:lifecycle-livedata-core-ktx")) {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
- api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+ api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
api(KOTLIN_STDLIB)
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 961ddb2..a6c2996 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -38,10 +38,10 @@
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
api(projectOrArtifact(":activity:activity"))
- api("androidx.lifecycle:lifecycle-livedata-core:2.3.0-beta01")
- api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
- api("androidx.savedstate:savedstate:1.1.0-beta01")
+ api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ api(projectOrArtifact(":savedstate:savedstate"))
api("androidx.annotation:annotation-experimental:1.0.0")
androidTestImplementation("androidx.appcompat:appcompat:1.1.0", {
diff --git a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
index 167669e..ab97f41 100644
--- a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
+++ b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
@@ -42,7 +42,7 @@
isRequired = false
)
- private fun createOption(
+ internal fun createOption(
argName: String,
desc: String,
isRequired: Boolean = true,
@@ -94,4 +94,4 @@
fun main(args: Array<String>) {
Main().run(args)
-}
\ No newline at end of file
+}
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
index 587c1ce..9e7ec3d 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
@@ -102,7 +102,7 @@
isRequired = false
)
- private fun createOption(
+ internal fun createOption(
argName: String,
argNameLong: String,
desc: String,
diff --git a/leanback/leanback/src/androidTest/AndroidManifest.xml b/leanback/leanback/src/androidTest/AndroidManifest.xml
index 2534e63..41194c1 100644
--- a/leanback/leanback/src/androidTest/AndroidManifest.xml
+++ b/leanback/leanback/src/androidTest/AndroidManifest.xml
@@ -66,6 +66,11 @@
<activity android:name="androidx.leanback.app.TestActivity"
android:exported="true"
android:theme="@style/Theme.Leanback"/>
+
+ <activity android:name="androidx.leanback.app.TestActivity$TestActivity2"
+ android:exported="true"
+ android:theme="@style/Theme.Leanback"/>
+
</application>
</manifest>
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
index 7ae71d7..00c7aca 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
@@ -46,8 +46,11 @@
public TestName mUnitTestName = new TestName();
@Rule
- public final TestActivity.TestActivityTestRule mRule = new TestActivity.TestActivityTestRule(
- generateProviderName("activity1"));
+ public final TestActivity.TestActivityTestRule mRule = new TestActivity.TestActivityTestRule();
+
+ @Rule
+ public final TestActivity.TestActivityTestRule2 mRule2 =
+ new TestActivity.TestActivityTestRule2();
String generateProviderName(String name) {
return mUnitTestName.getMethodName() + "_" + name;
@@ -159,7 +162,7 @@
assertIsColorDrawable(manager, color);
}
- void assertIsColorDrawable(BackgroundManager manager, int color) {
+ static void assertIsColorDrawable(BackgroundManager manager, int color) {
assertNull(manager.mLayerDrawable.mWrapper[0]);
assertTrue(manager.mLayerDrawable.getDrawable(0)
instanceof BackgroundManager.EmptyDrawable);
@@ -168,7 +171,7 @@
assertNull(manager.mBackgroundDrawable);
}
- void assertIsBitmapDrawable(BackgroundManager manager, Bitmap bitmap) {
+ static void assertIsBitmapDrawable(BackgroundManager manager, Bitmap bitmap) {
assertNull(manager.mLayerDrawable.mWrapper[0]);
assertTrue(manager.mLayerDrawable.getDrawable(0)
instanceof BackgroundManager.EmptyDrawable);
@@ -179,7 +182,7 @@
.mState.mBitmap, bitmap);
}
- void assertIsDrawable(BackgroundManager manager, Drawable drawable) {
+ static void assertIsDrawable(BackgroundManager manager, Drawable drawable) {
assertNull(manager.mLayerDrawable.mWrapper[0]);
assertTrue(manager.mLayerDrawable.getDrawable(0)
instanceof BackgroundManager.EmptyDrawable);
@@ -188,7 +191,7 @@
assertSame(manager.mBackgroundDrawable, drawable);
}
- Bitmap createBitmap(int width, int height, int color) {
+ static Bitmap createBitmap(int width, int height, int color) {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
@@ -227,20 +230,21 @@
setBitmapAndVerify(manager, createBitmap(200, 100, Color.MAGENTA));
}
+ public static class EstablishInOnAttachToWindow extends TestActivity.Provider {
+ @Override
+ public void onAttachedToWindow(TestActivity activity) {
+ BackgroundManager.getInstance(activity).attach(activity.getWindow());
+ }
+
+ @Override
+ public void onStart(TestActivity activity) {
+ BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+ }
+ }
+
@Test
public void establishInOnAttachToWindow() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onAttachedToWindow(TestActivity activity) {
- BackgroundManager.getInstance(activity).attach(activity.getWindow());
- }
-
- @Override
- public void onStart(TestActivity activity) {
- BackgroundManager.getInstance(activity).setColor(Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(EstablishInOnAttachToWindow.class);
BackgroundManager manager = BackgroundManager.getInstance(activity1);
waitForBackgroundAnimationFinish(manager);
@@ -249,20 +253,21 @@
testSwitchBackgrounds(manager);
}
+ public static class MultipleSetBitmaps extends TestActivity.Provider {
+ @Override
+ public void onAttachedToWindow(TestActivity activity) {
+ BackgroundManager.getInstance(activity).attach(activity.getWindow());
+ }
+
+ @Override
+ public void onStart(TestActivity activity) {
+ BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+ }
+ }
+
@Test
public void multipleSetBitmaps() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onAttachedToWindow(TestActivity activity) {
- BackgroundManager.getInstance(activity).attach(activity.getWindow());
- }
-
- @Override
- public void onStart(TestActivity activity) {
- BackgroundManager.getInstance(activity).setColor(Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(MultipleSetBitmaps.class);
final BackgroundManager manager = BackgroundManager.getInstance(activity1);
waitForBackgroundAnimationFinish(manager);
@@ -285,20 +290,21 @@
assertIsBitmapDrawable(manager, bitmap4);
}
+ public static class MultipleSetBitmapsAndColor extends TestActivity.Provider {
+ @Override
+ public void onAttachedToWindow(TestActivity activity) {
+ BackgroundManager.getInstance(activity).attach(activity.getWindow());
+ }
+
+ @Override
+ public void onStart(TestActivity activity) {
+ BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+ }
+ }
+
@Test
public void multipleSetBitmapsAndColor() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onAttachedToWindow(TestActivity activity) {
- BackgroundManager.getInstance(activity).attach(activity.getWindow());
- }
-
- @Override
- public void onStart(TestActivity activity) {
- BackgroundManager.getInstance(activity).setColor(Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(MultipleSetBitmapsAndColor.class);
final BackgroundManager manager = BackgroundManager.getInstance(activity1);
waitForBackgroundAnimationFinish(manager);
@@ -326,21 +332,22 @@
assertIsColorDrawable(manager, color);
}
+ public static class EstablishInOnCreate extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager.getInstance(activity).attach(activity.getWindow());
+ }
+
+ @Override
+ public void onStart(TestActivity activity) {
+ BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+ }
+ }
+
@Test
public void establishInOnCreate() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager.getInstance(activity).attach(activity.getWindow());
- }
-
- @Override
- public void onStart(TestActivity activity) {
- BackgroundManager.getInstance(activity).setColor(Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(EstablishInOnCreate.class);
BackgroundManager manager = BackgroundManager.getInstance(activity1);
waitForBackgroundAnimationFinish(manager);
@@ -349,20 +356,21 @@
testSwitchBackgrounds(manager);
}
+ public static class EstablishInOnStart extends TestActivity.Provider {
+ @Override
+ public void onStart(TestActivity activity) {
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ if (!m.isAttached()) {
+ // onStart will be called multiple times, attach() can only be called once.
+ m.attach(activity.getWindow());
+ }
+ m.setColor(Color.BLUE);
+ }
+ }
+
@Test
public void establishInOnStart() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onStart(TestActivity activity) {
- BackgroundManager m = BackgroundManager.getInstance(activity);
- if (!m.isAttached()) {
- // onStart will be called mutliple times, attach() can only be called once.
- m.attach(activity.getWindow());
- }
- m.setColor(Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(EstablishInOnStart.class);
BackgroundManager manager = BackgroundManager.getInstance(activity1);
waitForBackgroundAnimationFinish(manager);
@@ -371,150 +379,155 @@
testSwitchBackgrounds(manager);
}
+ public static class AssignColorImmediately extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ // if we set color before attach, it will be assigned immediately
+ m.setColor(Color.BLUE);
+ m.attach(activity.getWindow());
+ assertIsColorDrawable(m, Color.BLUE);
+ }
+ }
+
@Test
public void assignColorImmediately() throws Throwable {
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- // if we set color before attach, it will be assigned immediately
- m.setColor(Color.BLUE);
- m.attach(activity.getWindow());
- assertIsColorDrawable(m, Color.BLUE);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(AssignColorImmediately.class);
BackgroundManager manager = BackgroundManager.getInstance(activity1);
testSwitchBackgrounds(manager);
}
+ public static class AssignBitmapImmediately extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ // if we set bitmap before attach, it will be assigned immediately
+ m.setBitmap(mBitmap);
+ m.attach(activity.getWindow());
+ assertIsBitmapDrawable(m, mBitmap);
+ }
+ }
+
@Test
public void assignBitmapImmediately() throws Throwable {
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- // if we set bitmap before attach, it will be assigned immediately
- m.setBitmap(bitmap);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(AssignBitmapImmediately.class);
BackgroundManager manager = BackgroundManager.getInstance(activity1);
testSwitchBackgrounds(manager);
}
+ public static class InheritBitmapByNewActivity extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ // if we set bitmap before attach, it will be assigned immediately
+ m.setBitmap(mBitmap);
+ m.attach(activity.getWindow());
+ assertIsBitmapDrawable(m, mBitmap);
+ }
+ }
+ public static class InheritBitmapByNewActivity_2 extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.attach(activity.getWindow());
+ }
+ };
@Test
public void inheritBitmapByNewActivity() throws Throwable {
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- // if we set bitmap before attach, it will be assigned immediately
- m.setBitmap(bitmap);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(InheritBitmapByNewActivity.class);
- TestActivity activity2 = mRule.launchSecondActivity(
- generateProviderName("activity2"),
- new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
+ InheritBitmapByNewActivity provider = (InheritBitmapByNewActivity) activity1.getProvider();
+ TestActivity activity2 = mRule2.launchActivity(InheritBitmapByNewActivity_2.class);
waitForActivityStop(activity1);
+ assertIsBitmapDrawable(BackgroundManager.getInstance(activity2), provider.mBitmap);
BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
- assertIsBitmapDrawable(manager2, bitmap);
+ assertIsBitmapDrawable(manager2, provider.mBitmap);
activity2.finish();
}
+ public static class InheritColorByNewActivity extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ // if we set color before attach, it will be assigned immediately
+ m.setColor(Color.BLUE);
+ m.attach(activity.getWindow());
+ assertIsColorDrawable(m, Color.BLUE);
+ }
+ }
+
+ public static class InheritColorByNewActivity2 extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.attach(activity.getWindow());
+ assertIsColorDrawable(m, Color.BLUE);
+ }
+ };
+
@Test
public void inheritColorByNewActivity() throws Throwable {
- final int color = Color.BLUE;
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- // if we set color before attach, it will be assigned immediately
- m.setColor(color);
- m.attach(activity.getWindow());
- assertIsColorDrawable(m, color);
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(InheritColorByNewActivity.class);
- TestActivity activity2 = mRule.launchSecondActivity(
- generateProviderName("activity2"),
- new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.attach(activity.getWindow());
- assertIsColorDrawable(m, color);
- }
- });
+ TestActivity activity2 = mRule2.launchActivity(InheritColorByNewActivity2.class);
waitForActivityStop(activity1);
BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
- assertIsColorDrawable(manager2, color);
+ assertIsColorDrawable(manager2, Color.BLUE);
activity2.finish();
}
+ public static class ReturnFromNewActivity extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ // if we set bitmap before attach, it will be assigned immediately
+ m.setColor(Color.RED);
+ m.setBitmap(mBitmap);
+ m.attach(activity.getWindow());
+ assertIsBitmapDrawable(m, mBitmap);
+ }
+ }
+
+ public static class ReturnFromNewActivity2 extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.attach(activity.getWindow());
+ }
+ }
+
@Test
public void returnFromNewActivity() throws Throwable {
- final int color = Color.RED;
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- // if we set bitmap before attach, it will be assigned immediately
- m.setColor(color);
- m.setBitmap(bitmap);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
-
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(ReturnFromNewActivity.class);
final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
- TestActivity activity2 = mRule.launchSecondActivity(
- generateProviderName("activity2"),
- new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
+ ReturnFromNewActivity provider = (ReturnFromNewActivity) activity1.getProvider();
+ TestActivity activity2 = mRule2.launchActivity(ReturnFromNewActivity2.class);
waitForActivityStop(activity1);
+
final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+ assertIsBitmapDrawable(manager2, provider.mBitmap);
final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
setBitmapAndVerify(manager2, bitmap2);
@@ -527,47 +540,48 @@
// when return from the other app, last drawable is cleared.
waitForBackgroundAnimationFinish(manager1);
- assertIsColorDrawable(manager1, color);
+ assertIsColorDrawable(manager1, Color.RED);
+ }
+
+ public static class ManuallyReleaseInOnStop extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.setAutoReleaseOnStop(false);
+ // if we set bitmap before attach, it will be assigned immediately
+ m.setColor(Color.RED);
+ m.setBitmap(mBitmap);
+ m.attach(activity.getWindow());
+ assertIsBitmapDrawable(m, mBitmap);
+ }
+
+ @Override
+ public void onStop(TestActivity activity) {
+ BackgroundManager.getInstance(activity).release();
+ }
+ }
+
+ public static class ManuallyReleaseInOnStop2 extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.attach(activity.getWindow());
+ }
}
@Test
public void manuallyReleaseInOnStop() throws Throwable {
- final int color = Color.RED;
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.setAutoReleaseOnStop(false);
- // if we set bitmap before attach, it will be assigned immediately
- m.setColor(color);
- m.setBitmap(bitmap);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
-
- @Override
- public void onStop(TestActivity activity) {
- BackgroundManager.getInstance(activity).release();
- }
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(ManuallyReleaseInOnStop.class);
final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
- TestActivity activity2 = mRule.launchSecondActivity(
- generateProviderName("activity2"),
- new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
+ ManuallyReleaseInOnStop provider = (ManuallyReleaseInOnStop) activity1.getProvider();
+ TestActivity activity2 = mRule2.launchActivity(ManuallyReleaseInOnStop2.class);
waitForActivityStop(activity1);
final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+ assertIsBitmapDrawable(manager2, provider.mBitmap);
final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
setBitmapAndVerify(manager2, bitmap2);
@@ -580,76 +594,81 @@
// when return from the other app, last drawable is cleared.
waitForBackgroundAnimationFinish(manager1);
- assertIsColorDrawable(manager1, color);
+ assertIsColorDrawable(manager1, Color.RED);
+ }
+
+ public static class DisableAutoRelease extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.setAutoReleaseOnStop(false);
+ // if we set bitmap before attach, it will be assigned immediately
+ m.setColor(Color.RED);
+ m.setBitmap(mBitmap);
+ m.attach(activity.getWindow());
+ assertIsBitmapDrawable(m, mBitmap);
+ }
+
+ }
+
+ public static class DisableAutoRelease2 extends TestActivity.Provider {
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.attach(activity.getWindow());
+ }
}
@Test
public void disableAutoRelease() throws Throwable {
- final int color = Color.RED;
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.setAutoReleaseOnStop(false);
- // if we set bitmap before attach, it will be assigned immediately
- m.setColor(color);
- m.setBitmap(bitmap);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
-
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 = mRule.launchActivity(DisableAutoRelease.class);
final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
- TestActivity activity2 = mRule.launchSecondActivity(
- generateProviderName("activity2"),
- new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.attach(activity.getWindow());
- assertIsBitmapDrawable(m, bitmap);
- }
- });
+ DisableAutoRelease provider = (DisableAutoRelease) activity1.getProvider();
+ TestActivity activity2 = mRule2.launchActivity(DisableAutoRelease2.class);
waitForActivityStop(activity1);
final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+ assertIsBitmapDrawable(manager2, provider.mBitmap);
final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
setBitmapAndVerify(manager2, bitmap2);
// after activity2 is launched, activity will keep its drawable because
// setAutoReleaseOnStop(false)
- assertIsBitmapDrawable(manager1, bitmap);
+ assertIsBitmapDrawable(manager1, provider.mBitmap);
activity2.finish();
// when return from the activity, it's still the same bitmap
waitForBackgroundAnimationFinish(manager1);
- assertIsBitmapDrawable(manager1, bitmap);
+ assertIsBitmapDrawable(manager1, provider.mBitmap);
+ }
+
+ public static class DelayDrawableChangeUntilFullAlpha extends TestActivity.Provider {
+ final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+ @Override
+ public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+ super.onCreate(activity, savedInstanceState);
+ BackgroundManager m = BackgroundManager.getInstance(activity);
+ m.setAutoReleaseOnStop(false);
+ m.attach(activity.getWindow());
+ m.setColor(Color.RED);
+ }
+
}
@Test
public void delayDrawableChangeUntilFullAlpha() throws Throwable {
- final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
- TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
- @Override
- public void onCreate(TestActivity activity, Bundle savedInstanceState) {
- super.onCreate(activity, savedInstanceState);
- BackgroundManager m = BackgroundManager.getInstance(activity);
- m.setAutoReleaseOnStop(false);
- m.attach(activity.getWindow());
- m.setColor(Color.RED);
- }
-
- });
- final TestActivity activity1 = mRule.launchActivity();
+ final TestActivity activity1 =
+ mRule.launchActivity(DelayDrawableChangeUntilFullAlpha.class);
final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
assertIsColorDrawable(manager1, Color.RED);
+ DelayDrawableChangeUntilFullAlpha provider =
+ (DelayDrawableChangeUntilFullAlpha) activity1.getProvider();
// when set drawable, the change will be pending because alpha is 128
mRule.runOnUiThread(new Runnable() {
@Override
@@ -657,7 +676,7 @@
assertSame(manager1.mLayerDrawable,
activity1.getWindow().getDecorView().getBackground());
activity1.getWindow().getDecorView().getBackground().setAlpha(128);
- manager1.setBitmap(bitmap);
+ manager1.setBitmap(provider.mBitmap);
}
});
assertEquals(Color.RED,
@@ -672,6 +691,6 @@
}
});
waitForBackgroundAnimationFinish(manager1);
- assertIsBitmapDrawable(manager1, bitmap);
+ assertIsBitmapDrawable(manager1, provider.mBitmap);
}
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
index 9c6e789..058c193 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
+import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
@@ -33,11 +34,15 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ItemBridgeAdapter;
@@ -45,9 +50,11 @@
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
@@ -341,6 +348,66 @@
});
}
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_HEADERS_STATE,
+ BrowseFragment.HEADERS_DISABLED);
+ mActivity = activityTestRule.launchActivity(intent);
+ waitForEntranceTransitionFinished();
+
+ VerticalGridView gridView = mActivity.getBrowseTestFragment().getGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ mActivity.getFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
+
static void tapView(View v) {
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
int[] xy = new int[2];
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
index 72dc227..1a36149 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
+import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
@@ -30,11 +31,15 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ItemBridgeAdapter;
@@ -42,9 +47,11 @@
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
@@ -338,6 +345,66 @@
});
}
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE,
+ BrowseSupportFragment.HEADERS_DISABLED);
+ mActivity = activityTestRule.launchActivity(intent);
+ waitForEntranceTransitionFinished();
+
+ VerticalGridView gridView = mActivity.getBrowseTestSupportFragment().getGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ mActivity.getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
+
static void tapView(View v) {
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
int[] xy = new int[2];
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
index 504f501..cc83090 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
@@ -20,17 +20,17 @@
import android.os.Bundle;
import androidx.annotation.CallSuper;
-import androidx.leanback.testutils.PollingCheck;
import androidx.testutils.AnimationActivityTestRule;
-import java.util.HashMap;
-
/**
* A general Activity that allows test set a Provider to custom activity's behavior in life
* cycle events.
*/
public class TestActivity extends Activity {
+ public static class TestActivity2 extends TestActivity {
+ }
+
public static class Provider {
TestActivity mActivity;
@@ -70,14 +70,14 @@
final String mProviderName;
- public TestActivityTestRule(TestActivity.Provider provider, String providerName) {
- this(providerName);
- TestActivity.setProvider(mProviderName, provider);
+ public TestActivityTestRule(Class<? extends TestActivity.Provider> providerClass) {
+ super(TestActivity.class, false, false);
+ mProviderName = providerClass.getName();
}
- public TestActivityTestRule(String providerName) {
+ public TestActivityTestRule() {
super(TestActivity.class, false, false);
- mProviderName = providerName;
+ mProviderName = null;
}
public String getProviderName() {
@@ -90,42 +90,28 @@
return launchActivity(intent);
}
- /**
- * Launch secondary TestActivity without using TestActivityRule.
- */
- TestActivity launchSecondActivity(String providerName2, TestActivity.Provider provider2)
- throws Throwable {
- TestActivity.setProvider(providerName2, provider2);
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Intent intent = new Intent(getActivity(), TestActivity.class);
- intent.putExtra(TestActivity.EXTRA_PROVIDER, providerName2);
- getActivity().startActivity(intent);
- }
- });
+ public TestActivity launchActivity(Class<? extends TestActivity.Provider> providerClass) {
+ Intent intent = new Intent();
+ intent.putExtra(TestActivity.EXTRA_PROVIDER, providerClass.getName());
+ return launchActivity(intent);
+ }
+ }
- PollingCheck.waitFor(5000/*timeout*/, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canPreProceed() {
- return false;
- }
+ public static class TestActivityTestRule2 extends AnimationActivityTestRule<TestActivity2> {
- @Override
- public boolean canProceed() {
- return provider2.getActivity() != null && provider2.getActivity().isStarted();
- }
- });
- afterActivityLaunched();
- return provider2.getActivity();
+ public TestActivityTestRule2() {
+ super(TestActivity2.class, false, false);
}
+ public TestActivity launchActivity(Class<? extends TestActivity.Provider> providerClass) {
+ Intent intent = new Intent();
+ intent.putExtra(TestActivity.EXTRA_PROVIDER, providerClass.getName());
+ return launchActivity(intent);
+ }
}
public static final String EXTRA_PROVIDER = "testActivityProvider";
- static HashMap<String, Provider> sProviders = new HashMap<>();
-
String mProviderName;
Provider mProvider;
boolean mStarted;
@@ -133,15 +119,19 @@
public TestActivity() {
}
- public static void setProvider(String name, Provider provider) {
- sProviders.put(name, provider);
+ public Provider getProvider() {
+ return mProvider;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mProviderName = getIntent().getStringExtra(EXTRA_PROVIDER);
- mProvider = sProviders.get(mProviderName);
+ try {
+ mProvider = (Provider) Class.forName(mProviderName).newInstance();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
if (mProvider != null) {
mProvider.onCreate(this, savedInstanceState);
}
@@ -197,7 +187,6 @@
protected void onDestroy() {
if (mProvider != null) {
mProvider.onDestroy(this);
- setProvider(mProviderName, null);
}
super.onDestroy();
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
index ecd2773..2f0f90e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
@@ -19,13 +19,25 @@
package androidx.leanback.app;
+import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.VerticalGridPresenter;
+import androidx.leanback.widget.VerticalGridView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
@@ -35,6 +47,19 @@
@RunWith(AndroidJUnit4.class)
public class VerticalGridFragmentTest extends SingleFragmentTestBase {
+ static void loadData(ArrayObjectAdapter adapter, int items) {
+ int index = 0;
+ for (int j = 0; j < items; ++j) {
+ adapter.add("Hello world-" + (index++));
+ adapter.add("This is a test-" + (index++));
+ adapter.add("Android TV-" + (index++));
+ adapter.add("Leanback-" + (index++));
+ adapter.add("Hello world-" + (index++));
+ adapter.add("Android TV-" + (index++));
+ adapter.add("Leanback-" + (index++));
+ }
+ }
+
public static class GridFragment extends VerticalGridFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -45,7 +70,9 @@
VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
gridPresenter.setNumberOfColumns(3);
setGridPresenter(gridPresenter);
- setAdapter(new ArrayObjectAdapter());
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(new StringPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
}
}
@@ -69,4 +96,62 @@
activity.finish();
}
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ GridFragment.class,
+ 1000);
+
+ VerticalGridView gridView = ((GridFragment) activity.getTestFragment())
+ .mGridViewHolder.getGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ activity.getFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
index 4594ec8..3b28421 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
@@ -16,13 +16,25 @@
package androidx.leanback.app;
+import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.VerticalGridPresenter;
+import androidx.leanback.widget.VerticalGridView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
@@ -32,6 +44,19 @@
@RunWith(AndroidJUnit4.class)
public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
+ static void loadData(ArrayObjectAdapter adapter, int items) {
+ int index = 0;
+ for (int j = 0; j < items; ++j) {
+ adapter.add("Hello world-" + (index++));
+ adapter.add("This is a test-" + (index++));
+ adapter.add("Android TV-" + (index++));
+ adapter.add("Leanback-" + (index++));
+ adapter.add("Hello world-" + (index++));
+ adapter.add("Android TV-" + (index++));
+ adapter.add("Leanback-" + (index++));
+ }
+ }
+
public static class GridFragment extends VerticalGridSupportFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -42,7 +67,9 @@
VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
gridPresenter.setNumberOfColumns(3);
setGridPresenter(gridPresenter);
- setAdapter(new ArrayObjectAdapter());
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(new StringPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
}
}
@@ -66,4 +93,62 @@
activity.finish();
}
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ GridFragment.class,
+ 1000);
+
+ VerticalGridView gridView = ((GridFragment) activity.getTestFragment())
+ .mGridViewHolder.getGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ activity.getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java b/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
index af86a35..1564cbd 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
@@ -29,10 +29,17 @@
mWeakReferences.add(new WeakReference<>(object));
}
- public void assertNoLeak() {
+ public void assertNoLeak() throws Exception {
System.gc();
System.runFinalization();
for (WeakReference<?> weakReference : mWeakReferences) {
+ int count = 0;
+ while (weakReference.get() != null && count < 5) {
+ System.gc();
+ System.runFinalization();
+ Thread.sleep(1000);
+ count++;
+ }
/**
* Debugging leak: Sleep and run adb command:
* adb shell am dumpheap PID_OF_TEST /data/local/tmp/test_leak.hprof
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
index ef4e3c3..37b2bac 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
@@ -63,7 +63,16 @@
// The following provider will create an Activity which can inflate the ImageCardView
// And the ImageCardView can be fetched through ID for future testing.
- private final TestActivity.Provider mImageCardViewProvider = new TestActivity.Provider() {
+ public static final class ImageCardViewProvider extends TestActivity.Provider {
+ // Sample Drawable which will be used as the parameter for some methods.
+ private Drawable mSampleDrawable;
+
+ // Another Sample Drawable.
+ private Drawable mSampleDrawable2;
+
+ // Generated Image View Id.
+ private int mImageCardViewId;
+
@Override
public void onCreate(TestActivity activity, Bundle savedInstanceState) {
super.onCreate(activity, savedInstanceState);
@@ -90,8 +99,7 @@
// Enable lifecycle based testing
@Rule
public final TestActivity.TestActivityTestRule mRule =
- new TestActivity.TestActivityTestRule(
- mImageCardViewProvider, generateProviderName(IMAGE_CARD_VIEW_ACTIVITY));
+ new TestActivity.TestActivityTestRule(ImageCardViewProvider.class);
// Only support alpha animation
private static final String ALPHA = "alpha";
@@ -166,6 +174,11 @@
// Initialize testing rule and testing activity
final TestActivity imageCardViewTestActivity = mRule.launchActivity();
+ ImageCardViewProvider provider = (ImageCardViewProvider)
+ imageCardViewTestActivity.getProvider();
+ mImageCardViewId = provider.mImageCardViewId;
+ mSampleDrawable = provider.mSampleDrawable;
+ mSampleDrawable2 = provider.mSampleDrawable2;
// Create card view and image view
mImageCardView = (ImageCardView) imageCardViewTestActivity.findViewById(mImageCardViewId);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
index b29c59c..1dccd30 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
@@ -190,6 +190,13 @@
mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
}
+ @Override
+ public void onDestroyView() {
+ mProgressBarManager.setRootView(null);
+ mProgressBarManager.setProgressBarView(null);
+ super.onDestroyView();
+ }
+
/**
* Enables entrance transition.<p>
* Entrance transition is the standard slide-in transition that shows rows of data in
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
index c84d119..783a8ea 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
@@ -185,6 +185,13 @@
mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
}
+ @Override
+ public void onDestroyView() {
+ mProgressBarManager.setRootView(null);
+ mProgressBarManager.setProgressBarView(null);
+ super.onDestroyView();
+ }
+
/**
* Enables entrance transition.<p>
* Entrance transition is the standard slide-in transition that shows rows of data in
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
index c8012e5..de4327a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
@@ -1239,6 +1239,11 @@
mMainFragmentAdapter = null;
mMainFragment = null;
mHeadersFragment = null;
+ mBrowseFrame = null;
+ mScaleFrameLayout = null;
+ mSceneAfterEntranceTransition = null;
+ mSceneWithHeaders = null;
+ mSceneWithoutHeaders = null;
super.onDestroyView();
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
index 24b3b64..ef17147 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
@@ -1216,6 +1216,11 @@
mMainFragmentAdapter = null;
mMainFragment = null;
mHeadersSupportFragment = null;
+ mBrowseFrame = null;
+ mScaleFrameLayout = null;
+ mSceneAfterEntranceTransition = null;
+ mSceneWithHeaders = null;
+ mSceneWithoutHeaders = null;
super.onDestroyView();
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java b/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
index 276b899..9b5f3e4 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
@@ -91,13 +91,15 @@
* @param progressBarView custom view that will be shown to indicate progress.
*/
public void setProgressBarView(View progressBarView) {
- if (progressBarView.getParent() == null) {
+ if (progressBarView != null && progressBarView.getParent() == null) {
throw new IllegalArgumentException("Must have a parent");
}
this.mProgressBarView = progressBarView;
- this.mProgressBarView.setVisibility(View.INVISIBLE);
- mUserProvidedProgressBar = true;
+ if (this.mProgressBarView != null) {
+ this.mProgressBarView.setVisibility(View.INVISIBLE);
+ mUserProvidedProgressBar = true;
+ }
}
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
index 13ab244..73047073 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
@@ -222,7 +222,9 @@
@Override
public void onDestroyView() {
super.onDestroyView();
+ mGridViewHolder.getGridView().swapAdapter(null, true);
mGridViewHolder = null;
+ mSceneAfterEntranceTransition = null;
}
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
index 7966043..26b9a648 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
@@ -217,7 +217,9 @@
@Override
public void onDestroyView() {
super.onDestroyView();
+ mGridViewHolder.getGridView().swapAdapter(null, true);
mGridViewHolder = null;
+ mSceneAfterEntranceTransition = null;
}
/**
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index 13b478b..c581d23 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -35,8 +35,7 @@
* finished.
* <p>
* In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
- * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
- * existing ViewModel.
+ * configuration change (e.g. rotation). The new owner instance just re-connects to the existing model.
* <p>
* The purpose of the ViewModel is to acquire and keep the information that is necessary for an
* Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index a65e7a2..2b98060 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -40,6 +40,7 @@
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -53,10 +54,9 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
/**
* Provides non-system routes (and related RouteControllers) by using MediaRouter2.
@@ -154,9 +154,18 @@
protected void refreshRoutes() {
// Syetem routes should not be published by this provider.
- List<MediaRoute2Info> newRoutes = mMediaRouter2.getRoutes().stream().distinct()
- .filter(r -> !r.isSystemRoute())
- .collect(Collectors.toList());
+ List<MediaRoute2Info> newRoutes = new ArrayList<>();
+ Set<MediaRoute2Info> route2InfoSet = new ArraySet<>();
+ for (MediaRoute2Info route : mMediaRouter2.getRoutes()) {
+ // A route should be unique
+ if (route == null || route2InfoSet.contains(route) || route.isSystemRoute()) {
+ continue;
+ }
+ route2InfoSet.add(route);
+
+ // Not using new ArrayList(route2InfoSet) here for preserving the order.
+ newRoutes.add(route);
+ }
if (newRoutes.equals(mRoutes)) {
return;
@@ -175,10 +184,13 @@
extras.getString(MediaRouter2Utils.KEY_ORIGINAL_ROUTE_ID));
}
- List<MediaRouteDescriptor> routeDescriptors = mRoutes.stream()
- .map(MediaRouter2Utils::toMediaRouteDescriptor)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
+ List<MediaRouteDescriptor> routeDescriptors = new ArrayList<>();
+ for (MediaRoute2Info route : mRoutes) {
+ MediaRouteDescriptor descriptor = MediaRouter2Utils.toMediaRouteDescriptor(route);
+ if (route != null) {
+ routeDescriptors.add(descriptor);
+ }
+ }
MediaRouteProviderDescriptor descriptor = new MediaRouteProviderDescriptor.Builder()
.setSupportsDynamicGroupRoute(true)
.addRoutes(routeDescriptors)
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
index a279629..cfde237 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
@@ -29,6 +29,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
+import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderService;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
@@ -61,9 +62,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.UUID;
-import java.util.stream.Collectors;
@RequiresApi(api = Build.VERSION_CODES.R)
class MediaRoute2ProviderServiceAdapter extends MediaRoute2ProviderService {
@@ -279,12 +278,8 @@
@Override
public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {
- MediaRouteSelector selector = new MediaRouteSelector.Builder()
- .addControlCategories(preference.getPreferredFeatures().stream()
- .map(MediaRouter2Utils::toControlCategory)
- .collect(Collectors.toList())).build();
- mServiceImpl.setBaseDiscoveryRequest(new MediaRouteDiscoveryRequest(selector,
- preference.shouldPerformActiveScan()));
+ mServiceImpl.setBaseDiscoveryRequest(
+ MediaRouter2Utils.toMediaRouteDiscoveryRequest(preference));
}
public void setProviderDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
@@ -292,17 +287,26 @@
List<MediaRouteDescriptor> routeDescriptors =
(descriptor == null) ? Collections.emptyList() : descriptor.getRoutes();
- Map<String, MediaRouteDescriptor> descriptorMap =
- routeDescriptors.stream().filter(Objects::nonNull)
- // Ignores duplicated route IDs.
- .collect(Collectors.toMap(r -> r.getId(), r -> r, (fst, snd) -> fst));
+ Map<String, MediaRouteDescriptor> descriptorMap = new ArrayMap<>();
+ for (MediaRouteDescriptor desc : routeDescriptors) {
+ // Ignores duplicated route IDs.
+ if (desc == null || descriptorMap.containsKey(desc.getId())) {
+ continue;
+ }
+ descriptorMap.put(desc.getId(), desc);
+ }
updateStaticSessions(descriptorMap);
// Handle duplicated IDs
- notifyRoutes(descriptorMap.values().stream()
- .map(MediaRouter2Utils::toFwkMediaRoute2Info)
- .filter(Objects::nonNull)
- .collect(Collectors.toList()));
+
+ List<MediaRoute2Info> routes = new ArrayList<>();
+ for (MediaRouteDescriptor desc : descriptorMap.values()) {
+ MediaRoute2Info fwkMediaRouteInfo = MediaRouter2Utils.toFwkMediaRoute2Info(desc);
+ if (fwkMediaRouteInfo != null) {
+ routes.add(fwkMediaRouteInfo);
+ }
+ }
+ notifyRoutes(routes);
}
private DynamicGroupRouteController findControllerBySessionId(String sessionId) {
@@ -354,11 +358,13 @@
}
void updateStaticSessions(Map<String, MediaRouteDescriptor> routeDescriptors) {
- List<SessionRecord> staticSessions;
+ List<SessionRecord> staticSessions = new ArrayList<>();
synchronized (mLock) {
- staticSessions = mSessionRecords.values().stream()
- .filter(r -> (r.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0)
- .collect(Collectors.toList());
+ for (SessionRecord sessionRecord : mSessionRecords.values()) {
+ if ((sessionRecord.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0) {
+ staticSessions.add(sessionRecord);
+ }
+ }
}
for (SessionRecord sessionRecord : staticSessions) {
DynamicGroupRouteControllerProxy controller =
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
index ea442e4..91db8ba 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
@@ -33,6 +33,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.util.ArraySet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -42,9 +43,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
-import java.util.Objects;
import java.util.Set;
-import java.util.stream.Collectors;
//TODO: Remove SuppressLInt
@SuppressLint("NewApi")
@@ -185,14 +184,26 @@
if (features == null) {
return new ArrayList<>();
}
- return features.stream().distinct().map(f -> {
+
+ List<IntentFilter> controlFilters = new ArrayList<>();
+ Set<String> featuresSet = new ArraySet<>();
+ for (String feature : features) {
+ // A feature should be unique.
+ if (featuresSet.contains(feature)) {
+ continue;
+ }
+ featuresSet.add(feature);
+
IntentFilter filter = new IntentFilter();
- filter.addCategory(toControlCategory(f));
+ filter.addCategory(toControlCategory(feature));
// TODO: Add actions by using extras. (see RemotePlaybackClient#detectFeatures())
// filter.addAction(MediaControlIntent.ACTION_PLAY);
// filter.addAction(MediaControlIntent.ACTION_SEEK);
- return filter;
- }).collect(Collectors.toList());
+
+ controlFilters.add(filter);
+ }
+
+ return controlFilters;
}
@NonNull
@@ -200,8 +211,29 @@
if (routes == null) {
return new ArrayList<>();
}
- return routes.stream().filter(Objects::nonNull)
- .map(MediaRoute2Info::getId).collect(Collectors.toList());
+
+ List<String> routeIds = new ArrayList<>();
+ for (MediaRoute2Info route : routes) {
+ if (route == null) {
+ continue;
+ }
+ routeIds.add(route.getId());
+ }
+ return routeIds;
+ }
+
+ @NonNull
+ static MediaRouteDiscoveryRequest toMediaRouteDiscoveryRequest(
+ @NonNull RouteDiscoveryPreference preference) {
+ List<String> controlCategories = new ArrayList<>();
+ for (String feature : preference.getPreferredFeatures()) {
+ controlCategories.add(MediaRouter2Utils.toControlCategory(feature));
+ }
+ MediaRouteSelector selector = new MediaRouteSelector.Builder()
+ .addControlCategories(controlCategories)
+ .build();
+
+ return new MediaRouteDiscoveryRequest(selector, preference.shouldPerformActiveScan());
}
@NonNull
@@ -211,9 +243,11 @@
return new RouteDiscoveryPreference.Builder(new ArrayList<>(), false).build();
}
boolean activeScan = discoveryRequest.isActiveScan();
- List<String> routeFeatures = discoveryRequest.getSelector().getControlCategories()
- .stream().map(MediaRouter2Utils::toRouteFeature)
- .collect(Collectors.toList());
+
+ List<String> routeFeatures = new ArrayList<>();
+ for (String controlCategory : discoveryRequest.getSelector().getControlCategories()) {
+ routeFeatures.add(MediaRouter2Utils.toRouteFeature(controlCategory));
+ }
return new RouteDiscoveryPreference.Builder(routeFeatures, activeScan).build();
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
index 3126485..8141dc1 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -34,7 +34,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Watches for media route provider services to be installed.
@@ -163,8 +162,12 @@
@NonNull
List<ServiceInfo> getMediaRoute2ProviderServices() {
Intent intent = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
- return mPackageManager.queryIntentServices(intent, 0).stream()
- .map(resolveInfo -> resolveInfo.serviceInfo).collect(Collectors.toList());
+
+ List<ServiceInfo> serviceInfoList = new ArrayList<>();
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServices(intent, 0)) {
+ serviceInfoList.add(resolveInfo.serviceInfo);
+ }
+ return serviceInfoList;
}
private int findProvider(String packageName, String className) {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index 268a5f7..41f6df0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -17,6 +17,8 @@
import androidx.room.compiler.processing.javac.JavacElement
import androidx.room.compiler.processing.javac.JavacExecutableElement
+import androidx.room.compiler.processing.ksp.KspMethodElement
+import androidx.room.compiler.processing.ksp.KspMethodType
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
@@ -100,10 +102,18 @@
elm: XMethodElement,
owner: XDeclaredType
): MethodSpec.Builder {
- return overridingWithFinalParams(
- elm,
- elm.asMemberOf(owner)
- )
+ val asMember = elm.asMemberOf(owner)
+ return if (elm is KspMethodElement && asMember is KspMethodType) {
+ overridingWithFinalParams(
+ executableElement = elm,
+ resolvedType = asMember.inheritVarianceForOverride()
+ )
+ } else {
+ overridingWithFinalParams(
+ executableElement = elm,
+ resolvedType = asMember
+ )
+ }
}
private fun overridingWithFinalParams(
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index db41d40..03fc077 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -166,6 +166,18 @@
* If this is a wildcard with an extends bound, returns that bounded typed.
*/
fun extendsBound(): XType?
+
+ /**
+ * Creates a type with nullability [XNullability.NULLABLE] or returns this if the nullability is
+ * already [XNullability.NULLABLE].
+ */
+ fun makeNullable(): XType
+
+ /**
+ * Creates a type with nullability [XNullability.NONNULL] or returns this if the nullability is
+ * already [XNullability.NONNULL].
+ */
+ fun makeNonNullable(): XType
}
/**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
index 3a79c3a..7e4e1f0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
@@ -56,4 +56,13 @@
override val equalityItems by lazy {
arrayOf(typeMirror)
}
+
+ override fun copyWithNullability(nullability: XNullability): JavacType {
+ return DefaultJavacType(
+ env = env,
+ typeMirror = typeMirror,
+ kotlinType = kotlinType,
+ nullability = nullability
+ )
+ }
}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
index 87489e2..189f9dc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
@@ -75,4 +75,14 @@
elementNullability = componentTypeNullability
)
}
+
+ override fun copyWithNullability(nullability: XNullability): JavacType {
+ return JavacArrayType(
+ env = env,
+ typeMirror = typeMirror,
+ nullability = nullability,
+ knownComponentNullability = knownComponentNullability,
+ kotlinType = kotlinType
+ )
+ }
}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
index bed2e88..348ca07 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
@@ -65,4 +65,13 @@
)
}
}
+
+ override fun copyWithNullability(nullability: XNullability): JavacDeclaredType {
+ return JavacDeclaredType(
+ env = env,
+ typeMirror = typeMirror,
+ kotlinType = kotlinType,
+ nullability = nullability
+ )
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index dc9a6be..173ac32 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -77,16 +77,26 @@
}
}
- override fun boxed(): XType {
- return if (typeMirror.kind.isPrimitive) {
- env.wrap(
- typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
- .asType(),
- kotlinType = kotlinType,
- elementNullability = XNullability.NULLABLE
- )
- } else {
- this
+ override fun boxed(): JavacType {
+ return when {
+ typeMirror.kind.isPrimitive -> {
+ env.wrap(
+ typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
+ .asType(),
+ kotlinType = kotlinType,
+ elementNullability = XNullability.NULLABLE
+ )
+ }
+ typeMirror.kind == TypeKind.VOID -> {
+ env.wrap(
+ typeMirror = env.elementUtils.getTypeElement("java.lang.Void").asType(),
+ kotlinType = kotlinType,
+ elementNullability = XNullability.NULLABLE
+ )
+ }
+ else -> {
+ this
+ }
}
}
@@ -134,6 +144,32 @@
return MoreTypes.isType(typeMirror)
}
+ /**
+ * Create a copy of this type with the given nullability.
+ * This method is not called if the nullability of the type is already equal to the given
+ * nullability.
+ */
+ protected abstract fun copyWithNullability(nullability: XNullability): JavacType
+
+ final override fun makeNullable(): JavacType {
+ if (nullability == XNullability.NULLABLE) {
+ return this
+ }
+ if (typeMirror.kind.isPrimitive || typeMirror.kind == TypeKind.VOID) {
+ return boxed().makeNullable()
+ }
+ return copyWithNullability(XNullability.NULLABLE)
+ }
+
+ final override fun makeNonNullable(): JavacType {
+ if (nullability == XNullability.NONNULL) {
+ return this
+ }
+ // unlike makeNullable, we don't try to degrade to primitives here because it is valid for
+ // a boxed primitive to be marked as non-null.
+ return copyWithNullability(XNullability.NONNULL)
+ }
+
companion object {
private val BOXED_INT = TypeName.INT.box()
private val BOXED_LONG = TypeName.LONG.box()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
index f4a48bd..b7ec701 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
@@ -18,14 +18,17 @@
import com.google.devtools.ksp.symbol.KSAnnotated
-internal fun KSAnnotated.hasJvmStaticAnnotation() = annotations.any {
- it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmStatic"
+private fun KSAnnotated.hasAnnotationWithQName(qName: String) = annotations.any {
+ try {
+ it.annotationType.resolve().declaration.qualifiedName?.asString() == qName
+ } catch (illegal: IllegalStateException) {
+ // see: https://github.com/google/ksp/issues/173
+ false
+ }
}
-internal fun KSAnnotated.hasJvmFieldAnnotation() = annotations.any {
- it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmField"
-}
+internal fun KSAnnotated.hasJvmStaticAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmStatic")
-internal fun KSAnnotated.hasJvmDefaultAnnotation() = annotations.any {
- it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmDefault"
-}
+internal fun KSAnnotated.hasJvmFieldAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmField")
+
+internal fun KSAnnotated.hasJvmDefaultAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmDefault")
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
index ba27191..6c230d8 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
@@ -26,6 +26,11 @@
* Returns the type of a property as if it is member of the given [ksType].
*/
internal fun KSPropertyDeclaration.typeAsMemberOf(resolver: Resolver, ksType: KSType): KSType {
+ if (isStatic()) {
+ // calling as member with a static would throw as it might be a member of the companion
+ // object
+ return type.resolve()
+ }
return resolver.asMemberOf(
property = this,
containing = ksType
@@ -37,6 +42,11 @@
functionDeclaration: KSFunctionDeclaration,
ksType: KSType
): KSType {
+ if (functionDeclaration.isStatic()) {
+ // calling as member with a static would throw as it might be a member of the companion
+ // object
+ return type.resolve()
+ }
val asMember = resolver.asMemberOf(
function = functionDeclaration,
containing = ksType
@@ -51,8 +61,15 @@
resolver: Resolver,
ksType: KSType
): KSType {
- return resolver.asMemberOf(
- function = this,
- containing = ksType
- ).returnType ?: returnType?.resolve() ?: error("cannot find return type for $this")
+ val returnType = if (isStatic()) {
+ // calling as member with a static would throw as it might be a member of the companion
+ // object
+ returnType?.resolve()
+ } else {
+ resolver.asMemberOf(
+ function = this,
+ containing = ksType
+ ).returnType
+ }
+ return returnType ?: error("cannot find return type for $this")
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
index 60beee8..206f312 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
@@ -18,6 +18,7 @@
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.Modifier
/**
* Finds the class that contains this declaration and throws [IllegalStateException] if it cannot
@@ -49,3 +50,7 @@
}
return parent as? KSClassDeclaration
}
+
+internal fun KSDeclaration.isStatic(): Boolean {
+ return modifiers.contains(Modifier.JAVA_STATIC) || hasJvmStaticAnnotation()
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 31b2ad9..82f56d6 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -16,15 +16,11 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.javac.kotlin.typeNameFromJvmSignature
import androidx.room.compiler.processing.tryBox
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.processing.Resolver
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
-import com.squareup.javapoet.WildcardTypeName
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeArgument
@@ -33,6 +29,11 @@
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Variance
import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
+import com.squareup.javapoet.WildcardTypeName
internal const val ERROR_PACKAGE_NAME = "androidx.room.compiler.processing.kotlin.error"
@@ -99,7 +100,7 @@
TypeVariableName.get(param.name.asString(), type.typeName(resolver).tryBox())
}
}
- else -> type.typeName(resolver)
+ else -> type.typeName(resolver).tryBox()
}
}
@@ -152,4 +153,10 @@
return this.resolve().declaration is KSTypeParameter
}
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
\ No newline at end of file
+fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+
+internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
+ XNullability.NULLABLE -> makeNullable()
+ XNullability.NONNULL -> makeNotNullable()
+ else -> throw IllegalArgumentException("Cannot set KSType nullability to platform")
+}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
index 7831603..6b9912e 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
@@ -56,6 +56,13 @@
allowPrimitives = false
)
}
+
+ override fun copyWithNullability(nullability: XNullability): BoxedArray {
+ return BoxedArray(
+ env = env,
+ ksType = ksType.withNullability(nullability)
+ )
+ }
}
/**
@@ -67,7 +74,15 @@
override val componentType: KspType
) : KspArrayType(
env, ksType
- )
+ ) {
+ override fun copyWithNullability(nullability: XNullability): PrimitiveArray {
+ return PrimitiveArray(
+ env = env,
+ ksType = ksType.withNullability(nullability),
+ componentType = componentType
+ )
+ }
+ }
/**
* Factory class to create instances of [KspArrayType].
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
index e34f3ef..0932efc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
@@ -17,6 +17,7 @@
package androidx.room.compiler.processing.ksp
import androidx.room.compiler.processing.XDeclaredType
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.tryBox
import com.google.devtools.ksp.symbol.KSType
@@ -38,7 +39,14 @@
}
}
- override fun boxed(): XType {
+ override fun boxed(): KspDeclaredType {
return this
}
+
+ override fun copyWithNullability(nullability: XNullability): KspType {
+ return KspDeclaredType(
+ env = env,
+ ksType = ksType.withNullability(nullability)
+ )
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index c1570e1..0ebc228 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -23,6 +23,7 @@
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.Modifier
internal abstract class KspExecutableElement(
env: KspProcessingEnv,
@@ -59,8 +60,11 @@
}
override fun isVarArgs(): Boolean {
- return declaration.parameters.any {
- it.isVararg
- }
+ // in java, only the last argument can be a vararg so for suspend functions, it is never
+ // a vararg function. this would change if room generated kotlin code
+ return !declaration.modifiers.contains(Modifier.SUSPEND) &&
+ declaration.parameters.any {
+ it.isVararg
+ }
}
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 629d421..fcfa5de 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -20,7 +20,6 @@
import androidx.room.compiler.processing.XDeclaredType
import androidx.room.compiler.processing.XEquality
import androidx.room.compiler.processing.XExecutableParameterElement
-import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.METHOD_PARAMETER
import com.google.devtools.ksp.symbol.KSValueParameter
@@ -38,7 +37,7 @@
override val name: String
get() = parameter.name?.asString() ?: "_no_param_name"
- override val type: XType by lazy {
+ override val type: KspType by lazy {
parameter.typeAsMemberOf(
resolver = env.resolver,
functionDeclaration = method.declaration,
@@ -51,7 +50,7 @@
}
}
- override fun asMemberOf(other: XDeclaredType): XType {
+ override fun asMemberOf(other: XDeclaredType): KspType {
if (method.containing.type.isSameType(other)) {
return type
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
index ea0aa93..aa1add9 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
@@ -17,6 +17,7 @@
package androidx.room.compiler.processing.ksp
import androidx.room.compiler.processing.XHasModifiers
+import com.google.devtools.ksp.getVisibility
import com.google.devtools.ksp.isOpen
import com.google.devtools.ksp.isPrivate
import com.google.devtools.ksp.isProtected
@@ -28,6 +29,7 @@
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Origin
+import com.google.devtools.ksp.symbol.Visibility
/**
* Implementation of [XHasModifiers] for ksp declarations.
@@ -36,7 +38,9 @@
protected val declaration: KSDeclaration
) : XHasModifiers {
override fun isPublic(): Boolean {
- return declaration.isPublic()
+ // internals are public from java but KSP's declaration.isPublic excludes them.
+ return declaration.getVisibility() == Visibility.INTERNAL ||
+ declaration.getVisibility() == Visibility.PUBLIC
}
override fun isProtected(): Boolean {
@@ -52,8 +56,7 @@
}
override fun isStatic(): Boolean {
- return declaration.modifiers.contains(Modifier.JAVA_STATIC) ||
- declaration.hasJvmStaticAnnotation()
+ return declaration.isStatic()
}
override fun isTransient(): Boolean {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index 48841e32..359f413 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -42,13 +42,7 @@
@OptIn(KspExperimental::class)
override val name: String by lazy {
- try {
- env.resolver.getJvmName(declaration)
- } catch (ignored: ClassCastException) {
- // TODO remove this catch once that issue is fixed.
- // workaround for https://github.com/google/ksp/issues/164
- declaration.simpleName.asString()
- }
+ env.resolver.safeGetJvmName(declaration)
}
override val executableType: XMethodType by lazy {
@@ -104,9 +98,11 @@
) {
override val returnType: XType by lazy {
env.wrap(
- checkNotNull(declaration.returnType) {
- "return type on a method declaration cannot be null"
- }
+ ksType = declaration.returnTypeAsMemberOf(
+ resolver = env.resolver,
+ ksType = containing.type.ksType
+ ),
+ originatingReference = checkNotNull(declaration.returnType)
)
}
override fun isSuspendFunction() = false
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
index 0ce3d78..4ce33b0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
@@ -44,6 +44,15 @@
}
}
+ /**
+ * Creates a MethodType where variance is inherited for java code generation.
+ *
+ * see [OverrideVarianceResolver] for details.
+ */
+ fun inheritVarianceForOverride(): XMethodType {
+ return OverrideVarianceResolver(env, this).resolve()
+ }
+
private class KspNormalMethodType(
env: KspProcessingEnv,
origin: KspMethodElement,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
index 29fed31..c006611 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
@@ -16,7 +16,7 @@
package androidx.room.compiler.processing.ksp
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.tryUnbox
import com.google.devtools.ksp.symbol.KSType
import com.squareup.javapoet.TypeName
@@ -35,10 +35,27 @@
override val typeName: TypeName
get() = ksType.typeName(env.resolver).tryUnbox()
- override fun boxed(): XType {
+ override fun boxed(): KspType {
return env.wrap(
ksType = ksType,
allowPrimitives = false
)
}
+
+ override fun copyWithNullability(nullability: XNullability): KspType {
+ return when (nullability) {
+ XNullability.NONNULL -> {
+ this
+ }
+ XNullability.NULLABLE -> {
+ // primitive types cannot be nullable hence we box them.
+ boxed().makeNullable()
+ }
+ else -> {
+ // this should actually never happens as the only time this is called is from
+ // make nullable-make nonnull but we have this error here for completeness.
+ error("cannot set nullability to unknown in KSP")
+ }
+ }
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index cb03dab..1292542 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -156,4 +156,27 @@
override fun isEnum(): Boolean {
return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
}
+
+ abstract override fun boxed(): KspType
+
+ /**
+ * Create a copy of this type with the given nullability.
+ * This method is not called if the nullability of the type is already equal to the given
+ * nullability.
+ */
+ protected abstract fun copyWithNullability(nullability: XNullability): KspType
+
+ final override fun makeNullable(): KspType {
+ if (nullability == XNullability.NULLABLE) {
+ return this
+ }
+ return copyWithNullability(XNullability.NULLABLE)
+ }
+
+ final override fun makeNonNullable(): KspType {
+ if (nullability == XNullability.NONNULL) {
+ return this
+ }
+ return copyWithNullability(XNullability.NONNULL)
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index 36a3e12..0690474 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -16,10 +16,11 @@
package androidx.room.compiler.processing.ksp
-import androidx.room.compiler.processing.XType
-import com.squareup.javapoet.TypeName
+import androidx.room.compiler.processing.XNullability
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.squareup.javapoet.TypeName
/**
* The typeName for type arguments requires the type parameter, hence we have a special type
@@ -37,7 +38,23 @@
typeArg.typeName(typeParam, env.resolver)
}
- override fun boxed(): XType {
+ override fun boxed(): KspTypeArgumentType {
return this
}
+
+ override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
+ return KspTypeArgumentType(
+ env = env,
+ typeParam = typeParam,
+ typeArg = DelegatingTypeArg(
+ original = typeArg,
+ type = typeArg.requireType().withNullability(nullability).createTypeReference()
+ )
+ )
+ }
+
+ private class DelegatingTypeArg(
+ val original: KSTypeArgument,
+ override val type: KSTypeReference
+ ) : KSTypeArgument by original
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index eb88523..60339a5 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -106,8 +106,14 @@
}
private val _declaredPropertyFields by lazy {
- declaration
- .getDeclaredProperties()
+ val declaredProperties = declaration.getDeclaredProperties()
+ val companionProperties = declaration
+ .findCompanionObject()
+ ?.getDeclaredProperties()
+ ?.filter {
+ it.isStatic()
+ }.orEmpty()
+ (declaredProperties + companionProperties)
.map {
KspFieldElement(
env = env,
@@ -221,7 +227,15 @@
}
private val _declaredMethods by lazy {
- val myMethods = declaration.getDeclaredFunctions().asSequence()
+ val instanceMethods = declaration.getDeclaredFunctions().asSequence()
+ val companionMethods = declaration.findCompanionObject()
+ ?.getDeclaredFunctions()
+ ?.asSequence()
+ ?.filter {
+ it.isStatic()
+ }
+ ?: emptySequence()
+ val declaredMethods = (instanceMethods + companionMethods)
.filterNot {
// filter out constructors
it.simpleName.asString() == name
@@ -238,15 +252,7 @@
declaration = it
)
}.toList()
- val companionMethods = declaration.findCompanionObject()
- ?.let {
- env.wrapClassDeclaration(it)
- }?.getDeclaredMethods()
- ?.filter {
- it.isStatic()
- } ?: emptyList()
-
- myMethods + syntheticGetterSetterMethods + companionMethods
+ declaredMethods + syntheticGetterSetterMethods
}
override fun getDeclaredMethods(): List<XMethodElement> {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
index 2bf6728..cc5a601 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
@@ -16,7 +16,7 @@
package androidx.room.compiler.processing.ksp
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
import com.google.devtools.ksp.symbol.KSType
import com.squareup.javapoet.TypeName
@@ -30,16 +30,16 @@
internal class KspVoidType(
env: KspProcessingEnv,
ksType: KSType,
- private val boxed: Boolean
+ val boxed: Boolean
) : KspType(env, ksType) {
override val typeName: TypeName
- get() = if (boxed) {
+ get() = if (boxed || nullability == XNullability.NULLABLE) {
TypeName.VOID.box()
} else {
TypeName.VOID
}
- override fun boxed(): XType {
+ override fun boxed(): KspType {
return if (boxed) {
this
} else {
@@ -50,4 +50,12 @@
)
}
}
+
+ override fun copyWithNullability(nullability: XNullability): KspType {
+ return KspVoidType(
+ env = env,
+ ksType = ksType.withNullability(nullability),
+ boxed = boxed || nullability == XNullability.NULLABLE
+ )
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
new file mode 100644
index 0000000..d4f3e0e
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XMethodType
+import androidx.room.compiler.processing.XType
+import com.google.devtools.ksp.closestClassDeclaration
+import com.google.devtools.ksp.isOpen
+import com.google.devtools.ksp.symbol.ClassKind
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeArgument
+import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Variance
+import com.squareup.javapoet.TypeVariableName
+
+/**
+ * When kotlin generates java code, it has some interesting rules on how variance is handled.
+ *
+ * https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics
+ *
+ * This helper class applies that to [KspMethodType].
+ *
+ * Note that, this is only relevant when Room tries to generate overrides. For regular type
+ * operations, we prefer the variance declared in Kotlin source.
+ */
+internal class OverrideVarianceResolver(
+ private val env: KspProcessingEnv,
+ private val methodType: KspMethodType
+) {
+ fun resolve(): XMethodType {
+ val overideeElm = methodType.origin.findOverridee()
+ return ResolvedMethodType(
+ // kotlin does not touch return type
+ returnType = methodType.returnType,
+ parameterTypes = methodType.parameterTypes.mapIndexed { index, xType ->
+ xType.maybeInheritVariance(overideeElm?.parameterTypes?.getOrNull(index))
+ },
+ typeVariableNames = methodType.typeVariableNames
+ )
+ }
+
+ private fun XType.maybeInheritVariance(
+ overridee: XType?
+ ): XType {
+ return if (this is KspType) {
+ this.inheritVariance(overridee as? KspType)
+ } else {
+ this
+ }
+ }
+
+ private fun KspType.inheritVariance(overridee: KspType?): KspType {
+ return env.wrap(
+ ksType = ksType.inheritVariance(overridee?.ksType),
+ allowPrimitives = this is KspPrimitiveType || (this is KspVoidType && !this.boxed)
+ )
+ }
+
+ /**
+ * Finds the method type for the method element that was overridden by this method element.
+ */
+ private fun KspMethodElement.findOverridee(): KspMethodType? {
+ // now find out if this is overriding a method
+ val funDeclaration = declaration
+ val declaredIn = funDeclaration.closestClassDeclaration() ?: return null
+ if (declaredIn == containing.declaration) {
+ // if declared in the same class, skip
+ return null
+ }
+ // it is declared in a super type, get that
+ val overrideeElm = KspMethodElement.create(
+ env = env,
+ containing = env.wrapClassDeclaration(declaredIn),
+ declaration = funDeclaration.findOverridee() ?: funDeclaration
+ )
+ val containing = overrideeElm.enclosingTypeElement.type as? KspDeclaredType ?: return null
+ return KspMethodType.create(
+ env = env,
+ origin = overrideeElm,
+ containing = containing
+ )
+ }
+
+ /**
+ * Update the variance of the arguments of this type based on the types declaration.
+ *
+ * For instance, in List<Foo>, it actually inherits the `out` variance from `List`.
+ */
+ private fun KSType.inheritVariance(
+ overridee: KSType?
+ ): KSType {
+ if (arguments.isEmpty()) return this
+ // need to swap arguments with the variance from declaration
+ val newArguments = arguments.mapIndexed { index, typeArg ->
+ val param = declaration.typeParameters.getOrNull(index)
+ val overrideeArg = overridee?.arguments?.getOrNull(index)
+ typeArg.inheritVariance(overrideeArg, param)
+ }
+ return this.replace(newArguments)
+ }
+
+ private fun KSTypeReference.inheritVariance(
+ overridee: KSTypeReference?
+ ): KSTypeReference {
+ return resolve()
+ .inheritVariance(overridee = overridee?.resolve())
+ .createTypeReference()
+ }
+
+ private fun KSTypeArgument.inheritVariance(
+ overridee: KSTypeArgument?,
+ param: KSTypeParameter?
+ ): KSTypeArgument {
+ if (param == null) {
+ return this
+ }
+ val myTypeRef = type ?: return this
+
+ if (variance != Variance.INVARIANT) {
+ return env.resolver.getTypeArgument(
+ typeRef = myTypeRef.inheritVariance(overridee?.type),
+ variance = variance
+ )
+ }
+ if (overridee != null) {
+ // get it from overridee
+ return env.resolver.getTypeArgument(
+ typeRef = myTypeRef.inheritVariance(overridee.type),
+ variance = if (overridee.variance == Variance.STAR) {
+ Variance.COVARIANT
+ } else {
+ overridee.variance
+ }
+ )
+ }
+ // Now we need to guess from this type. If the type is final, it does not inherit unless
+ // the parameter is CONTRAVARIANT (`in`).
+ val myType = myTypeRef.resolve()
+ val shouldInherit = param.variance == Variance.CONTRAVARIANT ||
+ when (val decl = myType.declaration) {
+ is KSClassDeclaration -> {
+ decl.isOpen() ||
+ decl.classKind == ClassKind.ENUM_CLASS ||
+ decl.classKind == ClassKind.OBJECT
+ }
+ else -> true
+ }
+ return if (shouldInherit) {
+ env.resolver.getTypeArgument(
+ typeRef = myTypeRef.inheritVariance(overridee = null),
+ variance = param.variance
+ )
+ } else {
+ env.resolver.getTypeArgument(
+ typeRef = myTypeRef.inheritVariance(overridee = null),
+ variance = variance
+ )
+ }
+ }
+
+ /**
+ * [XMethodType] implementation where variance of types are resolved.
+ */
+ private class ResolvedMethodType(
+ override val returnType: XType,
+ override val parameterTypes: List<XType>,
+ override val typeVariableNames: List<TypeVariableName>
+ ) : XMethodType
+}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index 4901b29..e8a80d4 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
@@ -17,11 +17,15 @@
package androidx.room.compiler.processing.ksp
import androidx.room.compiler.processing.XExecutableElement
+import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
-import com.google.devtools.ksp.closestClassDeclaration
-import com.google.devtools.ksp.getAllSuperTypes
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSPropertyAccessor
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
+import com.google.devtools.ksp.symbol.Origin
internal fun Resolver.findClass(qName: String) = getClassDeclarationByName(
getKSNameFromString(qName)
@@ -42,8 +46,8 @@
}
internal fun Resolver.overrides(
- overriderElement: XExecutableElement,
- overrideeElement: XExecutableElement
+ overriderElement: XMethodElement,
+ overrideeElement: XMethodElement
): Boolean {
// in addition to functions declared in kotlin, we also synthesize getter/setter functions for
// properties which means we cannot simply send the declaration to KSP for override check
@@ -55,16 +59,82 @@
if (overriderElement.parameters.size != overrideeElement.parameters.size) {
return false
}
- val ksOverrider = overriderElement.getDeclarationForOverride()
- val ksOverridee = overrideeElement.getDeclarationForOverride()
- if (!overrides(ksOverrider, ksOverridee)) {
+ // do a quick check on name before doing the more expensive operations
+ if (overriderElement.name != overrideeElement.name) {
return false
}
- // TODO Workaround for https://github.com/google/ksp/issues/123
- // remove once that bug is fixed
- val subClass = ksOverrider.closestClassDeclaration() ?: return false
- val superClass = ksOverridee.closestClassDeclaration() ?: return false
- return subClass.getAllSuperTypes().any {
- it.declaration.closestClassDeclaration() == superClass
+ val ksOverrider = overriderElement.getDeclarationForOverride()
+ val ksOverridee = overrideeElement.getDeclarationForOverride()
+ if (overrides(ksOverrider, ksOverridee)) {
+ return true
+ }
+ // workaround for: https://github.com/google/ksp/issues/175
+ if (ksOverrider is KSFunctionDeclaration && ksOverridee is KSFunctionDeclaration) {
+ return ksOverrider.overrides(ksOverridee)
+ }
+ if (ksOverrider is KSPropertyDeclaration && ksOverridee is KSPropertyDeclaration) {
+ return ksOverrider.overrides(ksOverridee)
+ }
+ return false
+}
+
+private fun KSFunctionDeclaration.overrides(other: KSFunctionDeclaration): Boolean {
+ val overridee = try {
+ findOverridee()
+ } catch (ignored: ClassCastException) {
+ // workaround for https://github.com/google/ksp/issues/164
+ null
+ }
+ if (overridee == other) {
+ return true
+ }
+ return overridee?.overrides(other) ?: false
+}
+
+private fun KSPropertyDeclaration.overrides(other: KSPropertyDeclaration): Boolean {
+ val overridee = try {
+ findOverridee()
+ } catch (ex: NoSuchElementException) {
+ // workaround for https://github.com/google/ksp/issues/174
+ null
+ }
+ if (overridee == other) {
+ return true
+ }
+ return overridee?.overrides(other) ?: false
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+ declaration: KSFunctionDeclaration
+): String {
+ if (declaration.origin == Origin.JAVA) {
+ // https://github.com/google/ksp/issues/170
+ return declaration.simpleName.asString()
+ }
+ return try {
+ getJvmName(declaration)
+ } catch (ignored: ClassCastException) {
+ // TODO remove this catch once that issue is fixed.
+ // workaround for https://github.com/google/ksp/issues/164
+ return declaration.simpleName.asString()
+ }
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+ accessor: KSPropertyAccessor,
+ fallback: () -> String
+): String {
+ if (accessor.origin == Origin.JAVA) {
+ // https://github.com/google/ksp/issues/170
+ return fallback()
+ }
+ return try {
+ getJvmName(accessor)
+ } catch (ignored: ClassCastException) {
+ // TODO remove this catch once that issue is fixed.
+ // workaround for https://github.com/google/ksp/issues/164
+ return fallback()
}
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 68121e3..f2b09cb 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -46,8 +46,17 @@
filter = NO_USE_SITE
) {
- override val name: String
- get() = "_syntheticContinuation"
+ override val name: String by lazy {
+ // kotlin names this as pN where N is the # of arguments
+ // seems like kapt doesn't handle conflicts with declared arguments but we should
+ val desiredName = "p${containing.declaration.parameters.size}"
+
+ if (containing.declaration.parameters.none { it.name?.asString() == desiredName }) {
+ desiredName
+ } else {
+ "_syntheticContinuation"
+ }
+ }
override val equalityItems: Array<out Any?> by lazy {
arrayOf("continuation", containing)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 868a69c..bf906fd 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -35,6 +35,7 @@
import androidx.room.compiler.processing.ksp.KspProcessingEnv
import androidx.room.compiler.processing.ksp.KspTypeElement
import androidx.room.compiler.processing.ksp.overrides
+import androidx.room.compiler.processing.ksp.safeGetJvmName
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.KSPropertyAccessor
import java.util.Locale
@@ -121,15 +122,11 @@
@OptIn(KspExperimental::class)
override val name: String by lazy {
field.declaration.getter?.let {
- return@lazy env.resolver.getJvmName(it)
+ return@lazy env.resolver.safeGetJvmName(it) {
+ computeGetterName(field.name)
+ }
}
- // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
- val propName = field.name
- if (propName.startsWith("is")) {
- propName
- } else {
- "get${propName.capitalize(Locale.US)}"
- }
+ computeGetterName(field.name)
}
override val returnType: XType by lazy {
@@ -150,6 +147,17 @@
field = field.copyTo(newContainer)
)
}
+
+ companion object {
+ private fun computeGetterName(propName: String): String {
+ // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+ return if (propName.startsWith("is")) {
+ propName
+ } else {
+ "get${propName.capitalize(Locale.US)}"
+ }
+ }
+ }
}
internal class Setter(
@@ -176,15 +184,11 @@
@OptIn(KspExperimental::class)
override val name: String by lazy {
field.declaration.setter?.let {
- return@lazy env.resolver.getJvmName(it)
+ return@lazy env.resolver.safeGetJvmName(it) {
+ computeSetterName(field.name)
+ }
}
- // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
- val propName = field.name
- if (propName.startsWith("is")) {
- "set${propName.substring(2)}"
- } else {
- "set${propName.capitalize(Locale.US)}"
- }
+ computeSetterName(field.name)
}
override val returnType: XType by lazy {
@@ -238,5 +242,16 @@
return "method parameter"
}
}
+
+ companion object {
+ private fun computeSetterName(propName: String): String {
+ // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+ return if (propName.startsWith("is")) {
+ "set${propName.substring(2)}"
+ } else {
+ "set${propName.capitalize(Locale.US)}"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index e0f2844..49ad742 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -16,10 +16,13 @@
package androidx.room.compiler.processing
-import androidx.room.compiler.processing.javac.JavacProcessingEnv
+import androidx.room.compiler.processing.javac.JavacMethodElement
+import androidx.room.compiler.processing.javac.JavacTypeElement
import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.javaTypeUtils
import androidx.room.compiler.processing.util.runKaptTest
-import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import com.google.auto.common.MoreTypes
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.MethodSpec
@@ -27,7 +30,6 @@
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.type.DeclaredType
-import javax.lang.model.util.ElementFilter
import javax.lang.model.util.Types
class MethodSpecHelperTest {
@@ -101,10 +103,35 @@
return 3;
}
+ open fun boxedLongArrayReturn(): Array<Long> {
+ TODO();
+ }
+
+ open fun boxedIntArrayReturn(): Array<Int> {
+ TODO();
+ }
+
+ protected open fun listArg(r:List<String>) {
+ }
+
+ open suspend fun suspendUnitFun() {
+ }
+
+ protected open suspend fun suspendBasic(p0:Int):String {
+ TODO()
+ }
+
+ protected open suspend fun suspendVarArg(p0:Int, vararg p1:String):Long {
+ TODO()
+ }
+
protected open fun <R> typeArgs(r:R): R {
return r;
}
+ internal open fun internalFun() {
+ }
+
@Throws(Exception::class)
protected open fun throwsException() {
}
@@ -114,47 +141,229 @@
overridesCheck(source)
}
- private fun overridesCheck(source: Source) {
+ @Test
+ fun variance() {
+ // check our override impl matches javapoet
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar;
+ interface MyInterface<T> {
+ suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+ }
+ interface I1<in T>
+ interface I2<out T>
+ interface I3<T>
+ enum class Lang {
+ ES,
+ EN;
+ }
+ class Box<out T>(val value: T)
+
+ interface Base
+ class Derived : Base
+
+ interface Baz : MyInterface<String> {
+ fun boxDerived(value: Derived): Box<Derived> = Box(value)
+ fun unboxBase(box: Box<Base>): Base = box.value
+ fun unboxString(box: Box<String>): String = box.value
+ fun findByLanguages(langs: Set<Lang>): List<String>
+ fun f1(args : I1<String>): I1<String>
+ fun f2(args : I2<String>): I2<String>
+ fun f3(args : I3<String>): I3<String>
+ suspend fun s1(args : I1<String>): I1<String>
+ suspend fun s2(args : I2<String>): I2<String>
+ suspend fun s3(args : I3<String>): I3<String>
+ suspend fun s4(args : I1<String>): String
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source)
+ }
+
+ @Test
+ fun inheritedVariance_openType() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar;
+ interface MyInterface<T> {
+ fun receiveList(argsInParent : List<T>):Unit
+ suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+ }
+ open class Book(val id:Int)
+ interface Baz : MyInterface<Book> {
+ fun myList(args: List<Book>):Unit
+ override fun receiveList(argsInParent : List<Book>):Unit
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source)
+ }
+
+ @Test
+ fun inheritedVariance_finalType() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar;
+ interface MyInterface<T> {
+ fun receiveList(argsInParent : List<T>):Unit
+ suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+ }
+ interface Baz : MyInterface<String> {
+ fun myList(args: List<String>):Unit
+ override fun receiveList(argsInParent : List<String>):Unit
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source, ignoreInheritedMethods = true)
+ }
+
+ @Test
+ fun inheritedVariance_enumType() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar;
+ enum class EnumType {
+ FOO,
+ BAR;
+ }
+ interface MyInterface<T> {
+ fun receiveList(argsInParent : List<T>):Unit
+ suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+ }
+ interface Baz : MyInterface<EnumType> {
+ fun myList(args: List<EnumType>):Unit
+ override fun receiveList(argsInParent : List<EnumType>):Unit
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source)
+ }
+
+ @Test
+ fun inheritedVariance_multiLevel() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar;
+ interface GrandParent<T> {
+ fun receiveList(list : List<T>): Unit
+ suspend fun suspendReceiveList(list : List<T>): Unit
+ suspend fun suspendReturnList(): List<T>
+ }
+ interface Parent: GrandParent<Number> {
+ }
+ interface Baz : Parent {
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source)
+ }
+
+ @Test
+ fun primitiveOverrides() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ package foo.bar
+ data class LongFoo(val id: Long, val description: String)
+ /* Interface with generics only */
+ interface MyInterface<Key, Value> {
+ fun getItem(id: Key): Value?
+ //fun delete(id: Key)
+ //fun getFirstItemId(): Key
+ }
+ /* Interface with non-generics and generics */
+ interface Baz : MyInterface<Long, LongFoo> {
+ override fun getItem(id: Long): LongFoo?
+ //override fun delete(id: Long)
+ //fun insert(item: LongFoo)
+ //override fun getFirstItemId(): Long
+ }
+ """.trimIndent()
+ )
+ overridesCheck(source)
+ }
+
+ private fun overridesCheck(source: Source, ignoreInheritedMethods: Boolean = false) {
// first build golden image with Java processor so we can use JavaPoet's API
- val golden = buildMethodsViaJavaPoet(source)
- runKspTest(
- sources = listOf(source),
- succeed = true
+ val golden = buildMethodsViaJavaPoet(source, ignoreInheritedMethods)
+ runProcessorTestIncludingKsp(
+ sources = listOf(source)
) { invocation ->
- val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
- element.getDeclaredMethods().filter {
- // TODO b/171572318
- !invocation.isKsp || it.name != "throwsException"
- }.forEachIndexed { index, method ->
- val subject = MethodSpecHelper.overridingWithFinalParams(
- method,
- element.type
- ).build().toString()
- assertThat(subject).isEqualTo(golden[index])
+ val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
+ methods.forEachIndexed { index, method ->
+
+ if (invocation.isKsp && method.name == "throwsException") {
+ // TODO b/171572318
+ } else {
+ val subject = MethodSpecHelper.overridingWithFinalParams(
+ method,
+ target.type
+ ).build().toString()
+ assertThat(subject).isEqualTo(golden[index])
+ }
}
}
}
- private fun buildMethodsViaJavaPoet(source: Source): List<String> {
+ private fun buildMethodsViaJavaPoet(
+ source: Source,
+ ignoreInheritedMethods: Boolean
+ ): List<String> {
lateinit var result: List<String>
runKaptTest(
sources = listOf(source),
succeed = true
- ) {
- val processingEnv = (it.processingEnv as JavacProcessingEnv)
- val element = processingEnv.elementUtils.getTypeElement("foo.bar.Baz")
- result = ElementFilter.methodsIn(element.enclosedElements)
+ ) { invocation ->
+ val (target, methods) = invocation.getOverrideTestTargets(
+ ignoreInheritedMethods
+ )
+ val element = (target as JavacTypeElement).element
+ result = methods
.map {
+ (it as JavacMethodElement).element
+ }.map {
generateFromJavapoet(
it,
MoreTypes.asDeclared(element.asType()),
- processingEnv.typeUtils
+ invocation.javaTypeUtils
).build().toString()
}
}
return result
}
+ /**
+ * Get test targets. There is an edge case where it is not possible to implement an interface
+ * in java, b/174313780. [ignoreInheritedMethods] helps avoid that case.
+ */
+ private fun XTestInvocation.getOverrideTestTargets(
+ ignoreInheritedMethods: Boolean
+ ): Pair<XTypeElement, List<XMethodElement>> {
+ val objectMethodNames = processingEnv
+ .requireTypeElement("java.lang.Object")
+ .getAllNonPrivateInstanceMethods()
+ .map {
+ it.name
+ }
+ val target = processingEnv.requireTypeElement("foo.bar.Baz")
+ val methods = if (ignoreInheritedMethods) {
+ target.getDeclaredMethods().filter { !it.isStatic() }
+ } else {
+ target.getAllNonPrivateInstanceMethods()
+ }
+ val selectedMethods = methods.filter {
+ it.isOverrideableIgnoringContainer()
+ }.filterNot {
+ it.name in objectMethodNames
+ }
+ return target to selectedMethods
+ }
+
private fun generateFromJavapoet(
method: ExecutableElement,
owner: DeclaredType,
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index d1e5326..d84af6e 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -23,6 +23,7 @@
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
@@ -35,7 +36,7 @@
class XElementTest {
@Test
fun modifiers() {
- runProcessorTest(
+ runProcessorTestIncludingKsp(
listOf(
Source.java(
"foo.bar.Baz",
@@ -145,7 +146,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
listOf(genericBase, boundedChild)
) {
fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
@@ -172,7 +173,12 @@
}
validateElement(
element = it.processingEnv.requireTypeElement("foo.bar.Base"),
- tTypeName = TypeVariableName.get("T"),
+ tTypeName = if (it.isKsp) {
+ // when inheritance resolution happens, KSP resolves them to object
+ TypeName.OBJECT
+ } else {
+ TypeVariableName.get("T")
+ },
rTypeName = TypeVariableName.get("R")
)
validateElement(
@@ -204,7 +210,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -251,7 +257,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("java.lang.Object")
@@ -274,7 +280,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
sources = listOf(subject)
) {
val inner = ClassName.get("foo.bar", "Baz.Inner")
@@ -311,7 +317,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 5caee8c..a270b74 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -98,6 +98,26 @@
}
@Test
+ fun isVarArgs_kotlin() {
+ val subject = Source.kotlin(
+ "Subject.kt",
+ """
+ interface Subject {
+ fun method(vararg inputs: String)
+ suspend fun suspendMethod(vararg inputs: String);
+ }
+ """.trimIndent()
+ )
+ runProcessorTestIncludingKsp(
+ sources = listOf(subject)
+ ) {
+ val element = it.processingEnv.requireTypeElement("Subject")
+ assertThat(element.getMethod("method").isVarArgs()).isTrue()
+ assertThat(element.getMethod("suspendMethod").isVarArgs()).isFalse()
+ }
+ }
+
+ @Test
fun kotlinDefaultImpl() {
val subject = Source.kotlin(
"Baz.kt",
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 46b1037..859e553 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -24,7 +24,9 @@
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.TypeName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -66,6 +68,7 @@
}
""".trimIndent()
)
+ // TODO run with KSP once https://github.com/google/ksp/issues/167 is fixed
runProcessorTest(
sources = listOf(source)
) {
@@ -161,7 +164,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runProcessorTestIncludingKsp(
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -251,4 +254,141 @@
}
}
}
+
+ @Test
+ fun changeNullability_primitives() {
+ runProcessorTestIncludingKsp { invocation ->
+ PRIMITIVE_TYPES.forEach { primitiveTypeName ->
+ val primitive = invocation.processingEnv.requireType(primitiveTypeName)
+ assertThat(primitive.nullability).isEqualTo(NONNULL)
+ val nullable = primitive.makeNullable()
+ assertThat(nullable.nullability).isEqualTo(NULLABLE)
+ assertThat(nullable.typeName).isEqualTo(primitiveTypeName.box())
+
+ // When a boxed primitive is marked as non-null, it should stay as boxed primitive
+ // Even though this might be counter-intutive (because making it nullable will box
+ // it) it is more consistent as it is completely valid to annotate a boxed primitive
+ // with non-null while you cannot annoteted a primitive with nullable as it is not
+ // a valid state.
+ val boxedPrimitive = invocation.processingEnv.requireType(primitiveTypeName.box())
+ val nonNull = boxedPrimitive.makeNonNullable()
+ assertThat(nonNull.nullability).isEqualTo(NONNULL)
+ assertThat(nonNull.typeName).isEqualTo(primitiveTypeName.box())
+ }
+ }
+ }
+
+ @Test
+ fun changeNullability_typeArguments() {
+ // we need to make sure we don't convert type arguments into primitives!!
+ val kotlinSrc = Source.kotlin(
+ "KotlinClas.kt",
+ """
+ class KotlinClass(val subject: List<Int?>)
+ """.trimIndent()
+ )
+ val javaSrc = Source.java(
+ "JavaClass",
+ """
+ class JavaClass {
+ java.util.List<Integer> subject;
+ }
+ """.trimIndent()
+ )
+ runProcessorTestIncludingKsp(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
+ listOf("KotlinClass", "JavaClass").forEach {
+ val subject = invocation.processingEnv.requireTypeElement(it)
+ .getField("subject").type
+ check(subject.isDeclared())
+ val typeArg = subject.typeArguments.first()
+ assertThat(typeArg.typeName).isEqualTo(TypeName.INT.box())
+ typeArg.makeNonNullable().let {
+ assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+ assertThat(it.nullability).isEqualTo(XNullability.NONNULL)
+ }
+ typeArg.makeNonNullable().makeNullable().let {
+ assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+ assertThat(it.nullability).isEqualTo(NULLABLE)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun changeNullability_declared() {
+ runProcessorTestIncludingKsp { invocation ->
+ val subject = invocation.processingEnv.requireType("java.util.List")
+ subject.makeNullable().let {
+ assertThat(it.nullability).isEqualTo(NULLABLE)
+ assertThat(it.isDeclared()).isTrue()
+ }
+ subject.makeNonNullable().let {
+ assertThat(it.nullability).isEqualTo(NONNULL)
+ assertThat(it.isDeclared()).isTrue()
+ }
+ // ksp defaults to non-null so we do double conversion here to ensure it flips
+ // nullability
+ subject.makeNullable().makeNonNullable().let {
+ assertThat(it.nullability).isEqualTo(NONNULL)
+ assertThat(it.isDeclared()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun changeNullability_arrayTypes() {
+ runProcessorTestIncludingKsp { invocation ->
+ val subject = invocation.processingEnv.getArrayType(
+ invocation.processingEnv.requireType("java.util.List")
+ )
+ subject.makeNullable().let {
+ assertThat(it.nullability).isEqualTo(NULLABLE)
+ assertThat(it.isArray()).isTrue()
+ }
+ subject.makeNonNullable().let {
+ assertThat(it.nullability).isEqualTo(NONNULL)
+ assertThat(it.isArray()).isTrue()
+ }
+ // ksp defaults to non-null so we do double conversion here to ensure it flips
+ // nullability
+ subject.makeNullable().makeNonNullable().let {
+ assertThat(it.nullability).isEqualTo(NONNULL)
+ assertThat(it.isArray()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun makeNullable_void() {
+ val src = Source.java(
+ "Foo.java",
+ """
+ class Foo {
+ void subject() {}
+ }
+ """.trimIndent()
+ )
+ runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ val voidType = invocation.processingEnv.requireTypeElement("Foo")
+ .getMethod("subject").returnType
+ assertThat(voidType.typeName).isEqualTo(TypeName.VOID)
+ voidType.makeNullable().let {
+ assertThat(it.nullability).isEqualTo(NULLABLE)
+ assertThat(it.typeName).isEqualTo(TypeName.VOID.box())
+ }
+ }
+ }
+
+ companion object {
+ val PRIMITIVE_TYPES = listOf(
+ TypeName.BOOLEAN,
+ TypeName.BYTE,
+ TypeName.SHORT,
+ TypeName.INT,
+ TypeName.LONG,
+ TypeName.CHAR,
+ TypeName.FLOAT,
+ TypeName.DOUBLE,
+ )
+ }
}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
index f53ce78..1bc7787 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
@@ -22,8 +22,10 @@
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.className
import androidx.room.compiler.processing.util.getField
+import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.runKspTest
import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import org.junit.Test
@@ -174,4 +176,53 @@
}
}
}
+
+ @Test
+ fun asMemberOfStatics() {
+ val kotlinSrc = Source.kotlin(
+ "KotlinClass.kt",
+ """
+ class KotlinClass {
+ companion object {
+ @JvmStatic
+ var staticProp: String = ""
+ @JvmStatic
+ fun staticFun(x:Int) {}
+ }
+ }
+ """.trimIndent()
+ )
+ val javaSrc = Source.java(
+ "JavaClass",
+ """
+ class JavaClass {
+ void staticFun(int x) {}
+ static String staticProp;
+ }
+ """.trimIndent()
+ )
+ runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+ listOf("KotlinClass", "JavaClass").forEach {
+ val typeElement = invocation.processingEnv.requireTypeElement(it)
+ typeElement.getMethod("staticFun").let { staticFun ->
+ val asMember = staticFun.asMemberOf(typeElement.type)
+ assertThat(asMember.returnType.typeName).isEqualTo(TypeName.VOID)
+ assertThat(
+ asMember.parameterTypes.single().typeName
+ ).isEqualTo(TypeName.INT)
+ // different codepath, execute it as well
+ assertThat(
+ staticFun.parameters.single().asMemberOf(typeElement.type).typeName
+ ).isEqualTo(TypeName.INT)
+ }
+ typeElement.getField("staticProp").let { staticProp ->
+ assertThat(
+ staticProp.asMemberOf(typeElement.type).typeName
+ ).isEqualTo(
+ ClassName.get(String::class.java)
+ )
+ }
+ }
+ }
+ }
}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
index 10c8f93..437bdc3 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
@@ -440,6 +440,9 @@
)
runKspTest(sources = listOf(src), succeed = true) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("CompanionSubject")
+ assertThat(subject.getAllFieldNames()).containsExactly(
+ "mutableStatic", "immutableStatic"
+ )
assertThat(subject.getDeclaredMethods().names()).containsExactly(
"getMutableStatic", "setMutableStatic", "getImmutableStatic"
)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
index 0bc2a7d..1e24f10 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
@@ -20,9 +20,13 @@
import androidx.room.compiler.processing.ksp.KspProcessingEnv
import com.google.devtools.ksp.processing.Resolver
import javax.lang.model.util.Elements
+import javax.lang.model.util.Types
val XTestInvocation.kspResolver: Resolver
get() = (processingEnv as KspProcessingEnv).resolver
val XTestInvocation.javaElementUtils: Elements
- get() = (processingEnv as JavacProcessingEnv).elementUtils
\ No newline at end of file
+ get() = (processingEnv as JavacProcessingEnv).elementUtils
+
+val XTestInvocation.javaTypeUtils: Types
+ get() = (processingEnv as JavacProcessingEnv).typeUtils
\ No newline at end of file
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
index f1acb65..d16de8c 100644
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
@@ -37,7 +37,6 @@
import com.google.crypto.tink.aead.AesGcmKeyManager;
import com.google.crypto.tink.daead.AesSivKeyManager;
import com.google.crypto.tink.daead.DeterministicAeadConfig;
-import com.google.crypto.tink.daead.DeterministicAeadFactory;
import com.google.crypto.tink.integration.android.AndroidKeysetManager;
import com.google.crypto.tink.subtle.Base64;
@@ -376,8 +375,8 @@
.withMasterKeyUri(KEYSTORE_PATH_URI + "_androidx_security_master_key_")
.build().getKeysetHandle();
- DeterministicAead deterministicAead = DeterministicAeadFactory.getPrimitive(
- daeadKeysetHandle);
+ DeterministicAead deterministicAead =
+ daeadKeysetHandle.getPrimitive(DeterministicAead.class);
byte[] encryptedKey = deterministicAead.encryptDeterministically(testKey.getBytes(UTF_8),
tinkTestPrefs.getBytes());
String encodedKey = Base64.encode(encryptedKey);
diff --git a/settings.gradle b/settings.gradle
index c04ee3b..a688c1c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -222,6 +222,7 @@
includeProject(":compose:material:material-icons-core", "compose/material/material-icons-core", [BuildType.COMPOSE])
includeProject(":compose:material:material-icons-core:material-icons-core-samples", "compose/material/material-icons-core/samples", [BuildType.COMPOSE])
includeProject(":compose:material:material-icons-extended", "compose/material/material-icons-extended", [BuildType.COMPOSE])
+includeProject(":compose:material:material-ripple", "compose/material/material-ripple", [BuildType.COMPOSE])
includeProject(":compose:material:material:icons:generator", "compose/material/material/icons/generator", [BuildType.COMPOSE])
includeProject(":compose:material:material:integration-tests:material-demos", "compose/material/material/integration-tests/material-demos", [BuildType.COMPOSE])
includeProject(":compose:material:material:integration-tests:material-studies", "compose/material/material/integration-tests/material-studies", [BuildType.COMPOSE])
diff --git a/studiow b/studiow
new file mode 100755
index 0000000..577071b
--- /dev/null
+++ b/studiow
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+if [ -n "$1" ]; then
+ export ANDROIDX_PROJECTS=${1^^}
+else
+ export ANDROIDX_PROJECTS=MAIN
+ echo "Supported projects sets include:"
+ echo "- MAIN for non-Compose Jetpack libraries"
+ echo "- COMPOSE for Compose and dependencies"
+ echo "- FLAN for Fragment, Lifecycle, Activity, and Navigation"
+ echo "- ALL for all libraries"
+ echo
+ echo "No project set specified, using MAIN..."
+fi
+shift
+source gradlew studio "$@"
+
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index 5409f896..ee7fa64 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -35,8 +35,8 @@
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 56fd228..5b9d604 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -35,8 +35,8 @@
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 2870cb3..be317d9 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -35,8 +35,8 @@
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/build.gradle b/wear/wear-watchface-client/build.gradle
index 4b4eaef..b5c1d3f 100644
--- a/wear/wear-watchface-client/build.gradle
+++ b/wear/wear-watchface-client/build.gradle
@@ -51,7 +51,7 @@
android {
defaultConfig {
- minSdkVersion 27
+ minSdkVersion 25
}
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/wear/wear-watchface-client"
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 96889ba..7a6a332 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -40,9 +40,16 @@
import androidx.wear.watchface.client.WatchFaceControlClientImpl
import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.data.ComplicationBoundsType
+import androidx.wear.watchface.samples.BLUE_STYLE
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.GREEN_STYLE
+import androidx.wear.watchface.samples.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertFalse
@@ -486,6 +493,57 @@
assertThat(contentDescriptionLabels[2].getTextAt(context.resources, 0))
.isEqualTo("ID Right")
}
+
+ @Test
+ fun setUserStyle() {
+ val interactiveInstanceFuture =
+ service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+ "testId",
+ deviceConfig,
+ systemState,
+ mapOf(
+ COLOR_STYLE_SETTING to GREEN_STYLE,
+ WATCH_HAND_LENGTH_STYLE_SETTING to "0.25",
+ DRAW_HOUR_PIPS_STYLE_SETTING to "false",
+ COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS
+ ),
+ complications
+ )
+
+ Mockito.`when`(surfaceHolder.surfaceFrame)
+ .thenReturn(Rect(0, 0, 400, 400))
+
+ // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
+ createEngine()
+
+ // Wait for the instance to be created.
+ val interactiveInstance =
+ interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+
+ // Note this map doesn't include all the categories, which is fine the others will be set
+ // to their defaults.
+ interactiveInstance.setUserStyle(
+ mapOf(
+ COLOR_STYLE_SETTING to BLUE_STYLE,
+ WATCH_HAND_LENGTH_STYLE_SETTING to "0.9",
+ )
+ )
+
+ val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+ RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+ 100,
+ 1234567,
+ null,
+ complications
+ )
+
+ try {
+ // Note the hour hand pips and both complications should be visible in this image.
+ bitmap.assertAgainstGolden(screenshotRule, "setUserStyle")
+ } finally {
+ interactiveInstance.close()
+ }
+ }
}
internal class TestExampleCanvasAnalogWatchFaceService(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
index 889a3a5..847643a 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
@@ -31,7 +31,7 @@
/** The type of the complication's bounds. */
@ComplicationBoundsType public val boundsType: Int,
- /** The [ComplicationType]s supported for this complication. */
+ /** The [ComplicationType]s supported by this complication. */
public val supportedTypes: List<ComplicationType>,
/** The [DefaultComplicationProviderPolicy] for this complication. */
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index e432cc0..521ef55 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -20,6 +20,7 @@
import android.os.IBinder
import android.support.wearable.watchface.SharedMemoryImage
import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.control.IHeadlessWatchFace
@@ -29,7 +30,13 @@
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
-/** Controls a stateless remote headless watch face. */
+/**
+ * Controls a stateless remote headless watch face. This is mostly intended for use by watch face
+ * editor UIs which need to generate screenshots for various styling configurations without
+ * affecting the current watchface.
+ *
+ * Note clients should call [close] when finished.
+ */
public interface HeadlessWatchFaceClient : AutoCloseable {
/** The UTC reference preview time for this watch face in milliseconds since the epoch. */
public val previewReferenceTimeMillis: Long
@@ -63,6 +70,7 @@
* @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
* face with the given settings.
*/
+ @RequiresApi(27)
public fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
@@ -85,6 +93,7 @@
* @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
* face with the given settings, or `null` if [complicationId] is unrecognized.
*/
+ @RequiresApi(27)
public fun takeComplicationScreenshot(
complicationId: Int,
renderParameters: RenderParameters,
@@ -117,6 +126,7 @@
{ ComplicationState(it.complicationState) }
)
+ @RequiresApi(27)
override fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
@@ -141,6 +151,7 @@
)
)
+ @RequiresApi(27)
override fun takeComplicationScreenshot(
complicationId: Int,
renderParameters: RenderParameters,
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
index 10b2b1b..46bb1a9 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
@@ -25,6 +25,7 @@
import android.support.wearable.watchface.SharedMemoryImage
import androidx.annotation.IntDef
import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.RenderParameters
@@ -50,6 +51,8 @@
/**
* Controls a stateful remote interactive watch face with an interface tailored for SysUI the
* WearOS 3.0 launcher app. Typically this will be used for the current active watch face.
+ *
+ * Note clients should call [close] when finished.
*/
public interface InteractiveWatchFaceSysUiClient : AutoCloseable {
@@ -71,7 +74,10 @@
*/
public const val TAP_TYPE_TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
- /** Constructs a [InteractiveWatchFaceSysUiClient] from an [IBinder]. */
+ /**
+ * Constructs an [InteractiveWatchFaceSysUiClient] from the [IBinder] returned by
+ * [asBinder].
+ */
@JvmStatic
public fun createFromBinder(binder: IBinder): InteractiveWatchFaceSysUiClient =
InteractiveWatchFaceSysUiClientImpl(binder)
@@ -136,6 +142,7 @@
* @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
* face with the given settings.
*/
+ @RequiresApi(27)
public fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
@@ -181,6 +188,7 @@
)
}
+ @RequiresApi(27)
override fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
index 71e7ecf..e8b1169 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
@@ -20,6 +20,7 @@
import android.os.IBinder
import android.support.wearable.watchface.SharedMemoryImage
import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
@@ -27,16 +28,21 @@
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.data.UserStyleWireFormat
/**
* Controls a stateful remote interactive watch face with an interface tailored for WCS the
* WearOS 3.0 system server responsible for watch face management. Typically this will be used for
* the current active watch face.
+ *
+ * Note clients should call [close] when finished.
*/
public interface InteractiveWatchFaceWcsClient : AutoCloseable {
public companion object {
- /** Constructs a [InteractiveWatchFaceWcsClient] from an [IBinder]. */
+ /**
+ * Constructs an [InteractiveWatchFaceWcsClient] from the [IBinder] returned by [asBinder].
+ */
@JvmStatic
public fun createFromBinder(binder: IBinder): InteractiveWatchFaceWcsClient =
InteractiveWatchFaceWcsClientImpl(binder)
@@ -62,6 +68,7 @@
* @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
* face with the given settings.
*/
+ @RequiresApi(27)
public fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
@@ -79,6 +86,13 @@
*/
public fun setUserStyle(userStyle: UserStyle)
+ /**
+ * Sets the watch face's current UserStyle represented as a Map<String, String>. This can be
+ * helpful to avoid having to construct a [UserStyle] which requires the [UserStyleSchema]
+ * which is an additional IPC. Note this may alter [complicationState].
+ */
+ public fun setUserStyle(userStyle: Map<String, String>)
+
/** Returns the ID of this watch face instance. */
public val instanceId: String
@@ -110,6 +124,7 @@
)
}
+ @RequiresApi(27)
override fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
@IntRange(from = 0, to = 100)
@@ -141,6 +156,10 @@
iInteractiveWatchFaceWcs.setCurrentUserStyle(userStyle.toWireFormat())
}
+ override fun setUserStyle(userStyle: Map<String, String>) {
+ iInteractiveWatchFaceWcs.setCurrentUserStyle(UserStyleWireFormat(userStyle))
+ }
+
override val instanceId: String
get() = iInteractiveWatchFaceWcs.instanceId
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index e051212..d3498be 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -35,13 +35,17 @@
import com.google.common.util.concurrent.ListenableFuture
/**
- * Connects to a watch face's WatchFaceControlService which allows the user to control the
- * watch face.
+ * Connects to a watch face's WatchFaceControlService which allows the user to control the watch
+ * face.
*/
public interface WatchFaceControlClient : AutoCloseable {
public companion object {
- /** Constructs a client which connects to a watch face in the given android package. */
+ /**
+ * Constructs a [WatchFaceControlClient] which attempts to connect to a watch face in the
+ * android package [watchFacePackageName]. If this fails the [ListenableFuture]s returned by
+ * WatchFaceControlClient methods will fail with [ServiceNotBoundException].
+ */
@JvmStatic
public fun createWatchFaceControlClient(
/** Calling application's [Context]. */
diff --git a/wear/wear-watchface-complications-rendering/build.gradle b/wear/wear-watchface-complications-rendering/build.gradle
index 60b135a..2735d4b 100644
--- a/wear/wear-watchface-complications-rendering/build.gradle
+++ b/wear/wear-watchface-complications-rendering/build.gradle
@@ -54,7 +54,7 @@
aidl = true
}
defaultConfig {
- minSdkVersion 26
+ minSdkVersion 25
}
// Use Robolectric 4.+
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 95e4126..91c08a4 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
@@ -34,7 +34,6 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Resources;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
@@ -51,8 +50,8 @@
import androidx.annotation.NonNull;
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.RenderParameters;
import androidx.wear.watchface.Renderer;
import androidx.wear.watchface.WatchFace;
import androidx.wear.watchface.WatchFaceService;
@@ -720,18 +719,13 @@
WatchFaceType.ANALOG,
userStyleRepository,
new ComplicationsManager(new ArrayList<>(), userStyleRepository),
- new Renderer(surfaceHolder, userStyleRepository, watchState, 16L) {
- @NotNull
+ new Renderer.CanvasRenderer(
+ surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
+ 16L) {
@Override
- public Bitmap takeScreenshot$wear_watchface_debug(
- @NotNull Calendar calendar,
- @NonNull RenderParameters renderParameters) {
- return null;
- }
+ public void render(@NonNull Canvas canvas, @NonNull Rect bounds,
+ @NonNull Calendar calendar) {
- @Override
- public void renderInternal$wear_watchface_debug(
- @NotNull Calendar calendar) {
}
}
);
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
index 1161815..ad56258 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
@@ -88,8 +88,9 @@
}
/**
- * In memory storage for user style choices which allows listeners to be registered to observe
- * style changes.
+ * An in memory storage for user style choices represented as [UserStyle], listeners can be
+ * registered to observe style changes. The UserStyleRepository is initialized with a
+ * [UserStyleSchema].
*/
public class UserStyleRepository(
/**
@@ -98,37 +99,36 @@
*/
public val schema: UserStyleSchema
) {
- /** A listener for observing user style changes. */
+ /** A listener for observing [UserStyle] changes. */
public interface UserStyleListener {
- /** Called whenever the user style changes. */
+ /** Called whenever the [UserStyle] changes. */
@UiThread
public fun onUserStyleChanged(userStyle: UserStyle)
}
private val styleListeners = HashSet<UserStyleListener>()
- // The current style state which is initialized from the userStyleSettings.
- @SuppressWarnings("SyntheticAccessor")
- private val _style = UserStyle(
+ /**
+ * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
+ * callbacks if if any options have changed.
+ */
+ public var userStyle: UserStyle = UserStyle(
HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
for (setting in schema.userStyleSettings) {
this[setting] = setting.getDefaultOption()
}
}
)
-
- /** The current user controlled style for rendering etc... */
- public var userStyle: UserStyle
@UiThread
- get() = _style
+ get
@UiThread
set(style) {
var changed = false
val hashmap =
- _style.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
+ field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
for ((setting, option) in style.selectedOptions) {
// Ignore an unrecognized setting.
- val styleSetting = _style.selectedOptions[setting] ?: continue
+ val styleSetting = field.selectedOptions[setting] ?: continue
if (styleSetting.id != option.id) {
changed = true
}
@@ -140,7 +140,7 @@
}
for (styleListener in styleListeners) {
- styleListener.onUserStyleChanged(_style)
+ styleListener.onUserStyleChanged(field)
}
}
@@ -151,7 +151,7 @@
@SuppressLint("ExecutorRegistration")
public fun addUserStyleListener(userStyleListener: UserStyleListener) {
styleListeners.add(userStyleListener)
- userStyleListener.onUserStyleChanged(_style)
+ userStyleListener.onUserStyleChanged(userStyle)
}
/** Removes a [UserStyleListener] previously added by [addUserStyleListener]. */
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 978e023..9e5c35a 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -32,10 +32,12 @@
import java.security.InvalidParameterException
/**
- * Watch faces often have user configurable styles. The definition of what is a style is left up
- * to the watch face but it typically incorporates a variety of settings such as: color,
- * visual theme for watch hands, font, tick shape, complications, audio elements, etc...
- * A UserStyleSetting represents one of these dimensions.
+ * Watch faces often have user configurable styles, the definition of what is a style is left up to
+ * the watch face but it typically incorporates a variety of settings such as: color, visual theme
+ * for watch hands, font, tick shape, complications, audio elements, etc...
+ *
+ * A UserStyleSetting represents one of these dimensions. See also [UserStyleSchema] which defines
+ * the list of UserStyleSettings provided by the watch face.
*/
public sealed class UserStyleSetting(
/** Identifier for the element, must be unique. */
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index dd7e387..165eb26 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
property @UiThread public boolean isHighlighted;
}
- public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
- }
-
public final class Complication {
method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
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, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
}
- public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public void onGlContextCreated();
- method @UiThread public void onGlSurfaceCreated(int width, int height);
- method @UiThread public abstract void render(android.icu.util.Calendar calendar);
- }
-
- public final class GlesRendererKt {
- }
-
public final class GlesTextureComplication {
ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
@@ -147,8 +130,8 @@
public class ObservableWatchData<T> {
method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread public T getValue();
- method public final T getValueOr(T p);
- method public final boolean hasValue();
+ method @UiThread public final T getValueOr(T p);
+ method @UiThread public final boolean hasValue();
method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread protected void setValue(T v);
property @UiThread public T value;
@@ -174,8 +157,7 @@
public static final class RenderParameters.Companion {
}
- public abstract class Renderer {
- ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ public abstract sealed class Renderer {
method public final float getCenterX();
method public final float getCenterY();
method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +179,23 @@
property public final android.view.SurfaceHolder surfaceHolder;
}
+ public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ }
+
+ public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public void onGlContextCreated();
+ method @UiThread public void onGlSurfaceCreated(int width, int height);
+ method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+ }
+
+ public final class RendererKt {
+ }
+
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);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +233,7 @@
public final class WatchFaceKt {
}
- @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+ public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
ctor public WatchFaceService();
method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +243,10 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
- method public int getScreenShape();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +258,6 @@
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
- property public final int screenShape;
}
}
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index dd7e387..165eb26 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
property @UiThread public boolean isHighlighted;
}
- public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
- }
-
public final class Complication {
method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
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, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
}
- public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public void onGlContextCreated();
- method @UiThread public void onGlSurfaceCreated(int width, int height);
- method @UiThread public abstract void render(android.icu.util.Calendar calendar);
- }
-
- public final class GlesRendererKt {
- }
-
public final class GlesTextureComplication {
ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
@@ -147,8 +130,8 @@
public class ObservableWatchData<T> {
method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread public T getValue();
- method public final T getValueOr(T p);
- method public final boolean hasValue();
+ method @UiThread public final T getValueOr(T p);
+ method @UiThread public final boolean hasValue();
method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread protected void setValue(T v);
property @UiThread public T value;
@@ -174,8 +157,7 @@
public static final class RenderParameters.Companion {
}
- public abstract class Renderer {
- ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ public abstract sealed class Renderer {
method public final float getCenterX();
method public final float getCenterY();
method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +179,23 @@
property public final android.view.SurfaceHolder surfaceHolder;
}
+ public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ }
+
+ public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public void onGlContextCreated();
+ method @UiThread public void onGlSurfaceCreated(int width, int height);
+ method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+ }
+
+ public final class RendererKt {
+ }
+
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);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +233,7 @@
public final class WatchFaceKt {
}
- @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+ public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
ctor public WatchFaceService();
method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +243,10 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
- method public int getScreenShape();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +258,6 @@
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
- property public final int screenShape;
}
}
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index c6773f25..72cd5dcd 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
property @UiThread public boolean isHighlighted;
}
- public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
- }
-
public final class Complication {
method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
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, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
}
- public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
- ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
- method @UiThread public void onGlContextCreated();
- method @UiThread public void onGlSurfaceCreated(int width, int height);
- method @UiThread public abstract void render(android.icu.util.Calendar calendar);
- }
-
- public final class GlesRendererKt {
- }
-
public final class GlesTextureComplication {
ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
@@ -151,7 +134,6 @@
method public boolean getHasBurnInProtection();
method public boolean getHasLowBitAmbient();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> getInterruptionFilter();
- method public int getScreenShape();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
@@ -160,7 +142,6 @@
method public void setHasBurnInProtection(boolean p);
method public void setHasLowBitAmbient(boolean p);
method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
- method public void setScreenShape(int p);
property public final long analogPreviewReferenceTimeMillis;
property public final long digitalPreviewReferenceTimeMillis;
property public final boolean hasBurnInProtection;
@@ -169,14 +150,13 @@
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient;
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging;
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible;
- property public final int screenShape;
}
public class ObservableWatchData<T> {
method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread public T getValue();
- method public final T getValueOr(T p);
- method public final boolean hasValue();
+ method @UiThread public final T getValueOr(T p);
+ method @UiThread public final boolean hasValue();
method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
method @UiThread protected void setValue(T v);
property @UiThread public T value;
@@ -204,8 +184,7 @@
public static final class RenderParameters.Companion {
}
- public abstract class Renderer {
- ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ public abstract sealed class Renderer {
method public final float getCenterX();
method public final float getCenterY();
method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -227,6 +206,23 @@
property public final android.view.SurfaceHolder surfaceHolder;
}
+ public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ }
+
+ public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+ method @UiThread public void onGlContextCreated();
+ method @UiThread public void onGlSurfaceCreated(int width, int height);
+ method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+ }
+
+ public final class RendererKt {
+ }
+
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);
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -281,7 +277,7 @@
public final class WatchFaceKt {
}
- @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+ public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
ctor public WatchFaceService();
method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.view.SurfaceHolder? getWallpaperSurfaceHolderOverride();
@@ -292,11 +288,10 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
- method public int getScreenShape();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -308,7 +303,6 @@
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
- property public final int screenShape;
}
}
diff --git a/wear/wear-watchface/build.gradle b/wear/wear-watchface/build.gradle
index beca881..f4a1682 100644
--- a/wear/wear-watchface/build.gradle
+++ b/wear/wear-watchface/build.gradle
@@ -61,7 +61,7 @@
android {
defaultConfig {
- minSdkVersion 26
+ minSdkVersion 25
testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
}
diff --git a/wear/wear-watchface/samples/build.gradle b/wear/wear-watchface/samples/build.gradle
index ec111e8..ec2f092 100644
--- a/wear/wear-watchface/samples/build.gradle
+++ b/wear/wear-watchface/samples/build.gradle
@@ -44,10 +44,10 @@
android {
defaultConfig {
- minSdkVersion 26
+ minSdkVersion 25
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_7
- targetCompatibility = JavaVersion.VERSION_1_7
+ sourceCompatibility 1.8
+ targetCompatibility 1.8
}
}
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 19dee75..2773588 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
@@ -29,12 +29,12 @@
import androidx.wear.complications.DefaultComplicationProviderPolicy
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.Complication
import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
@@ -68,6 +68,16 @@
private const val NUMBER_RADIUS_FRACTION = 0.45f
+val COLOR_STYLE_SETTING = "color_style_setting"
+val RED_STYLE = "red_style"
+val GREEN_STYLE = "green_style"
+val BLUE_STYLE = "blue_style"
+
+val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
+
+val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+
+val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
val NO_COMPLICATIONS = "NO_COMPLICATIONS"
val LEFT_COMPLICATION = "LEFT_COMPLICATION"
val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
@@ -96,43 +106,43 @@
surfaceHolder: SurfaceHolder,
watchState: WatchState
): WatchFace {
- val watchFaceStyle = WatchFaceColorStyle.create(context, "red_style")
+ val watchFaceStyle = WatchFaceColorStyle.create(context, RED_STYLE)
val colorStyleSetting = ListUserStyleSetting(
- "color_style_setting",
- "Colors",
- "Watchface colorization",
+ COLOR_STYLE_SETTING,
+ context.getString(R.string.colors_style_setting),
+ context.getString(R.string.colors_style_setting_description),
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- "red_style",
- "Red",
+ RED_STYLE,
+ context.getString(R.string.colors_style_red),
Icon.createWithResource(context, R.drawable.red_style)
),
ListUserStyleSetting.ListOption(
- "green_style",
- "Green",
+ GREEN_STYLE,
+ context.getString(R.string.colors_style_green),
Icon.createWithResource(context, R.drawable.green_style)
),
ListUserStyleSetting.ListOption(
- "blue_style",
- "Blue",
+ BLUE_STYLE,
+ context.getString(R.string.colors_style_blue),
Icon.createWithResource(context, R.drawable.blue_style)
)
),
listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
)
val drawHourPipsStyleSetting = BooleanUserStyleSetting(
- "draw_hour_pips_style_setting",
- "Hour Pips",
- "Whether to draw or not",
+ DRAW_HOUR_PIPS_STYLE_SETTING,
+ context.getString(R.string.watchface_pips_setting),
+ context.getString(R.string.watchface_pips_setting_description),
null,
true,
listOf(Layer.BASE_LAYER)
)
val watchHandLengthStyleSetting = DoubleRangeUserStyleSetting(
- "watch_hand_length_style_setting",
- "Hand length",
- "Scale of watch hands",
+ WATCH_HAND_LENGTH_STYLE_SETTING,
+ context.getString(R.string.watchface_hand_length_setting),
+ context.getString(R.string.watchface_hand_length_setting_description),
null,
0.25,
1.0,
@@ -142,14 +152,14 @@
// These are style overrides applied on top of the complications passed into
// complicationsManager below.
val complicationsStyleSetting = ComplicationsUserStyleSetting(
- "complications_style_setting",
- "Complications",
- "Number and position",
+ COMPLICATIONS_STYLE_SETTING,
+ context.getString(R.string.watchface_complications_setting),
+ context.getString(R.string.watchface_complications_setting_description),
icon = null,
complicationConfig = listOf(
ComplicationsUserStyleSetting.ComplicationsOption(
LEFT_AND_RIGHT_COMPLICATIONS,
- "Both",
+ context.getString(R.string.watchface_complications_setting_both),
null,
// NB this list is empty because each [ComplicationOverlay] is applied on top of
// the initial config.
@@ -157,7 +167,7 @@
),
ComplicationsUserStyleSetting.ComplicationsOption(
NO_COMPLICATIONS,
- "None",
+ context.getString(R.string.watchface_complications_setting_none),
null,
listOf(
ComplicationOverlay(
@@ -172,7 +182,7 @@
),
ComplicationsUserStyleSetting.ComplicationsOption(
LEFT_COMPLICATION,
- "Left",
+ context.getString(R.string.watchface_complications_setting_left),
null,
listOf(
ComplicationOverlay(
@@ -183,7 +193,7 @@
),
ComplicationsUserStyleSetting.ComplicationsOption(
RIGHT_COMPLICATION,
- "Right",
+ context.getString(R.string.watchface_complications_setting_right),
null,
listOf(
ComplicationOverlay(
@@ -266,7 +276,7 @@
private val drawPipsStyleSetting: BooleanUserStyleSetting,
private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
surfaceHolder,
userStyleRepository,
watchState,
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 94cc130..66a46ec 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
@@ -38,15 +38,16 @@
import android.view.SurfaceHolder
import android.view.animation.AnimationUtils
import android.view.animation.PathInterpolator
+import androidx.annotation.ColorInt
import androidx.wear.complications.DefaultComplicationProviderPolicy
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.Complication
import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
@@ -193,6 +194,19 @@
internal val CENTERING_ADJUSTMENT_INTERPOLATOR =
PathInterpolator(0.4f, 0f, 0.2f, 1f)
+@ColorInt
+internal fun colorRgb(red: Float, green: Float, blue: Float) =
+ 0xff000000.toInt() or
+ ((red * 255.0f + 0.5f).toInt() shl 16) or
+ ((green * 255.0f + 0.5f).toInt() shl 8) or
+ (blue * 255.0f + 0.5f).toInt()
+
+internal fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
+
+internal fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
+
+internal fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
+
/**
* Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
* `backgroundColor` background.
@@ -201,11 +215,15 @@
* @param alphaFraction the fraction of the alpha value, range from 0 to 1
* @param backgroundColor the background color
*/
-internal fun getRGBColor(color: Color, alphaFraction: Float, backgroundColor: Color): Int {
- return Color.rgb(
- lerp(backgroundColor.red(), color.red(), alphaFraction),
- lerp(backgroundColor.green(), color.green(), alphaFraction),
- lerp(backgroundColor.blue(), color.blue(), alphaFraction)
+internal fun getRGBColor(
+ @ColorInt color: Int,
+ alphaFraction: Float,
+ @ColorInt backgroundColor: Int
+): Int {
+ return colorRgb(
+ lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
+ lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
+ lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
)
}
@@ -417,12 +435,11 @@
/** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
internal fun multiplyColor(colorInt: Int, multiplier: Float): Int {
- val color = Color.valueOf(colorInt)
- // NB color.red() etc return a value in the range [0..1]
- return Color.rgb(
- color.red() * multiplier,
- color.green() * multiplier,
- color.blue() * multiplier
+ val adjustedMultiplier = multiplier / 255.0f
+ return colorRgb(
+ Color.red(colorInt).toFloat() * adjustedMultiplier,
+ Color.green(colorInt).toFloat() * adjustedMultiplier,
+ Color.blue(colorInt).toFloat() * adjustedMultiplier,
)
}
@@ -450,26 +467,26 @@
surfaceHolder: SurfaceHolder,
watchState: WatchState
): WatchFace {
- val watchFaceStyle = WatchFaceColorStyle.create(this, "red_style")
+ val watchFaceStyle = WatchFaceColorStyle.create(this, RED_STYLE)
val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
- "color_style_setting",
- "Colors",
- "Watchface colorization",
+ COLOR_STYLE_SETTING,
+ getString(R.string.colors_style_setting),
+ getString(R.string.colors_style_setting_description),
icon = null,
options = listOf(
UserStyleSetting.ListUserStyleSetting.ListOption(
- "red_style",
- "Red",
+ RED_STYLE,
+ getString(R.string.colors_style_red),
Icon.createWithResource(this, R.drawable.red_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
- "green_style",
- "Green",
+ GREEN_STYLE,
+ getString(R.string.colors_style_green),
Icon.createWithResource(this, R.drawable.green_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
- "blue_style",
- "Blue",
+ BLUE_STYLE,
+ getString(R.string.colors_style_blue),
Icon.createWithResource(this, R.drawable.blue_style)
)
),
@@ -620,7 +637,7 @@
private val watchState: WatchState,
private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
surfaceHolder,
userStyleRepository,
watchState,
@@ -1050,9 +1067,9 @@
}
canvas.drawColor(
getRGBColor(
- Color.valueOf(backgroundColor),
+ backgroundColor,
drawProperties.backgroundAlpha,
- Color.valueOf(Color.BLACK)
+ Color.BLACK
)
)
}
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 458071d..b942f55 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
@@ -33,9 +33,9 @@
import androidx.wear.watchface.Complication
import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.DrawMode
-import androidx.wear.watchface.GlesRenderer
import androidx.wear.watchface.GlesTextureComplication
import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
@@ -155,7 +155,7 @@
watchState: WatchState,
private val colorStyleSetting: ListUserStyleSetting,
private val complication: Complication
-) : GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
/** Projection transformation matrix. Converts from 3D to 2D. */
private val projectionMatrix = FloatArray(16)
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 8da96bb..e294c01 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
@@ -28,11 +28,11 @@
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationType
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.CanvasRenderer
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.Complication
import androidx.wear.watchface.CanvasComplicationDrawable
import androidx.wear.watchface.ComplicationsManager
+import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
@@ -141,7 +141,7 @@
userStyleRepository
)
- val renderer = object : CanvasRenderer(
+ val renderer = object : Renderer.CanvasRenderer(
surfaceHolder,
userStyleRepository,
watchState,
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
deleted file mode 100644
index ed1e13d..0000000
--- a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
- -->
-
-<resources>
- <string name="app_name" translatable="false">Example Watchfaces</string>
- <string name="canvas_analog_watch_face_name"
- translatable="false">Example Analog Watchface</string>
- <string name="canvas_digital_watch_face_name"
- translatable="false">Example Digital Watchface</string>
- <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
-
- <!-- Name of watchface style [CHAR LIMIT=80] -->
- <string name="red_style_name">Red Style</string>
-
- <!-- Name of watchface style [CHAR LIMIT=80] -->
- <string name="green_style_name">Green Style</string>
-</resources>
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..90f3dd8
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/values/strings.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<resources>
+ <string name="app_name" translatable="false">Example Watchfaces</string>
+ <string name="canvas_analog_watch_face_name"
+ translatable="false">Example Analog Watchface</string>
+ <string name="canvas_digital_watch_face_name"
+ translatable="false">Example Digital Watchface</string>
+ <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
+
+ <!-- Name of watchface style [CHAR LIMIT=20] -->
+ <string name="red_style_name">Red Style</string>
+
+ <!-- Name of watchface style [CHAR LIMIT=20] -->
+ <string name="green_style_name">Green Style</string>
+
+ <!-- Name of watchface style category for selecting the color theme [CHAR LIMIT=20] -->
+ <string name="colors_style_setting">Colors</string>
+
+ <!-- Subtitle for the menu option to select the watch face color theme [CHAR LIMIT=20] -->
+ <string name="colors_style_setting_description">Watchface colorization</string>
+
+ <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+ <string name="colors_style_red">Red</string>
+
+ <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+ <string name="colors_style_green">Green</string>
+
+ <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+ <string name="colors_style_blue">Blue</string>
+
+ <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
+ <string name="watchface_pips_setting">Hour Pips</string>
+
+ <!-- Subtitle for the menu option to configure if we should draw pips to mark each hour
+ on the watch face [CHAR LIMIT=20] -->
+ <string name="watchface_pips_setting_description">Whether to draw or not</string>
+
+ <!-- A menu option to select a widget that lets us configure the length of the watch hands
+ [CHAR LIMIT=20] -->
+ <string name="watchface_hand_length_setting">Hand length</string>
+
+ <!-- Sub title for the menu option to select a widget that lets us configure the length of the
+ watch hands [CHAR LIMIT=20] -->
+ <string name="watchface_hand_length_setting_description">Scale of watch hands</string>
+
+ <!-- A menu option to select a widget that lets us configure the Complications (a watch
+ making term) [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting">Complications</string>
+
+ <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+ (a watch making term) [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting_description">Number and position</string>
+
+ <!-- Menu option within the complications configuration widget that lets us select both
+ the left and the right complication for rendering. [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting_both">Both</string>
+
+ <!-- Menu option within the complications configuration widget that lets us select no
+ complications for rendering. [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting_none">None</string>
+
+ <!-- Menu option within the complications configuration widget that lets us select only the left
+ complication for rendering. [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting_left">Left</string>
+
+ <!-- Menu option within the complications configuration widget that lets us select only the
+ right complication for rendering. [CHAR LIMIT=20] -->
+ <string name="watchface_complications_setting_right">Right</string>
+</resources>
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 5042dba..2c9375c 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -46,8 +46,10 @@
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.GREEN_STYLE
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.data.UserStyleWireFormat
import org.junit.After
@@ -304,7 +306,7 @@
initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
handler.post {
interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
- UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+ UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
)
engineWrapper.draw()
}
@@ -442,7 +444,7 @@
handler.post {
// Change the style
interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
- UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+ UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
)
// Simulate device shutting down.
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
deleted file mode 100644
index 3b81fea..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
+++ /dev/null
@@ -1,132 +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.wear.watchface
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Rect
-import android.icu.util.Calendar
-import android.view.SurfaceHolder
-import androidx.annotation.IntDef
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-/** @hide */
-@IntDef(
- value = [
- CanvasType.SOFTWARE,
- CanvasType.HARDWARE
- ]
-)
-public annotation class CanvasType {
- public companion object {
- /** A software canvas will be requested. */
- public const val SOFTWARE: Int = 0
-
- /**
- * A hardware canvas will be requested. This is usually faster than software rendering,
- * however it can sometimes increase battery usage by rendering at a higher frame rate.
- */
- public const val HARDWARE: Int = 1
- }
-}
-
-/**
- * Watch faces that require [Canvas] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class CanvasRenderer(
- /** The [SurfaceHolder] that [render] will draw into. */
- surfaceHolder: SurfaceHolder,
-
- /** The associated [UserStyleRepository]. */
- userStyleRepository: UserStyleRepository,
-
- /** The associated [WatchState]. */
- watchState: WatchState,
-
- /** The type of canvas to use. */
- @CanvasType private val canvasType: Int,
-
- /**
- * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
- * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
- * recommended to use lower frame rates if possible for better battery life. Variable frame
- * 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)
- interactiveDrawModeUpdateDelayMillis: Long
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
-
- internal override fun renderInternal(
- calendar: Calendar
- ) {
- val canvas = (
- if (canvasType == CanvasType.HARDWARE) {
- surfaceHolder.lockHardwareCanvas()
- } else {
- surfaceHolder.lockCanvas()
- }
- ) ?: return
- try {
- if (watchState.isVisible.value) {
- render(canvas, surfaceHolder.surfaceFrame, calendar)
- } else {
- canvas.drawColor(Color.BLACK)
- }
- } finally {
- surfaceHolder.unlockCanvasAndPost(canvas)
- }
- }
-
- /** {@inheritDoc} */
- internal override fun takeScreenshot(
- calendar: Calendar,
- renderParameters: RenderParameters
- ): Bitmap {
- val bitmap = Bitmap.createBitmap(
- screenBounds.width(),
- screenBounds.height(),
- Bitmap.Config.ARGB_8888
- )
- val prevRenderParameters = this.renderParameters
- this.renderParameters = renderParameters
- render(Canvas(bitmap), screenBounds, calendar)
- this.renderParameters = prevRenderParameters
- return bitmap
- }
-
- /**
- * Sub-classes should override this to implement their rendering logic which should respect
- * the current [DrawMode]. For correct functioning watch faces must use the supplied
- * [Calendar] and avoid using any other ways of getting the time.
- *
- * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
- * the [SurfaceHolder] backing the display
- * @param bounds A [Rect] describing the bonds of the canvas to draw into
- * @param calendar The current [Calendar]
- */
- @UiThread
- public abstract fun render(
- canvas: Canvas,
- bounds: Rect,
- calendar: Calendar
- )
-}
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 374b3fb..b1b4326 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
@@ -24,6 +24,7 @@
import android.icu.util.Calendar
import android.support.wearable.complications.ComplicationData
import androidx.annotation.UiThread
+import androidx.wear.complications.ComplicationHelperActivity
import androidx.wear.complications.DefaultComplicationProviderPolicy
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.IdAndComplicationData
@@ -31,25 +32,28 @@
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.style.Layer
-/** Common interface for rendering complications onto a [Canvas]. */
+/** Interface for rendering complications onto a [Canvas]. */
public interface CanvasComplication {
/**
- * Called when the CanvasComplication attaches to a [Complication].
+ * Called when the CanvasComplication attaches to a [Complication]. This will get called during
+ * [Complication] initialization and if [Complication.renderer] is assigned with this
+ * CanvasComplication.
*/
@UiThread
public fun onAttach(complication: Complication)
/**
- * Called when the CanvasComplication detaches from a [Complication].
+ * Called when the CanvasComplication detaches from a [Complication]. This will get called if
+ * [Complication.renderer] is assigned to a different CanvasComplication.
*/
@UiThread
public fun onDetach()
/**
- * Draws the complication into the canvas with the specified bounds. This will usually be
- * called by user watch face drawing code, but the system may also call it for complication
- * selection UI rendering. The width and height will be the same as that computed by
- * computeBounds but the translation and canvas size may differ.
+ * Draws the complication defined by [idAndData] into the canvas with the specified bounds. This
+ * will usually be called by user watch face drawing code, but the system may also call it
+ * for complication selection UI rendering. The width and height will be the same as that
+ * computed by computeBounds but the translation and canvas size may differ.
*
* @param canvas The [Canvas] to render into
* @param bounds A [Rect] describing the bounds of the complication
@@ -65,8 +69,8 @@
)
/**
- * Whether the complication should be drawn highlighted. This is to provide visual
- * feedback when the user taps on a complication.
+ * Whether the complication should be drawn highlighted. This is to provide visual feedback when
+ * the user taps on a complication.
*/
@Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@get:JvmName("isHighlighted")
@@ -78,13 +82,17 @@
}
/**
- * A complication rendered with [ComplicationDrawable] which renders complications in a
- * material design style. This renderer can't be shared by multiple complications.
+ * A complication rendered with [ComplicationDrawable] which renders complications in a material
+ * design style. This renderer can't be shared by multiple complications.
*/
public open class CanvasComplicationDrawable(
/** The [ComplicationDrawable] to render with. */
drawable: ComplicationDrawable,
+ /**
+ * The watch's [WatchState] which contains details pertaining to (low-bit) ambient mode and
+ * burn in protection needed to render correctly.
+ */
private val watchState: WatchState
) : CanvasComplication {
@@ -153,15 +161,15 @@
if (renderParameters.highlightedComplicationId == null ||
renderParameters.highlightedComplicationId == idAndData?.complicationId
) {
- drawHighlight(canvas, bounds, calendar)
+ drawOutline(canvas, bounds, calendar)
}
}
LayerMode.HIDE -> return
}
}
- /** Used (indirectly) by the editor, draws a highlight around the complication. */
- public open fun drawHighlight(
+ /** Used (indirectly) by the editor, draws a dashed line around the complication. */
+ public open fun drawOutline(
canvas: Canvas,
bounds: Rect,
calendar: Calendar
@@ -170,8 +178,8 @@
}
/**
- * Whether or not the complication should be drawn highlighted. Used to provide visual
- * feedback when the complication is tapped.
+ * Whether or not the complication should be drawn highlighted. Used to provide visual feedback
+ * when the complication is tapped.
*/
override var isHighlighted: Boolean
@Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@@ -185,9 +193,7 @@
drawable.isHighlighted = value
}
- /**
- * The [IdAndComplicationData] to use when rendering the complication.
- */
+ /** The [IdAndComplicationData] to use when rendering the complication. */
override var idAndData: IdAndComplicationData? = null
@UiThread
set(value) {
@@ -220,27 +226,34 @@
*/
@JvmStatic
public fun createRoundRectComplicationBuilder(
- /** The watch face's ID for this complication. */
+ /**
+ * The watch face's ID for this complication. Can be any integer but should be unique
+ * within the watch face.
+ */
id: Int,
/**
- * The renderer for this Complication. Renderers may not be sharable between complications.
+ * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+ * between complications.
*/
renderer: CanvasComplication,
/**
* The types of complication supported by this Complication. Passed into
* [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
- * configuration.
+ * configuration. This list should be non-empty.
*/
supportedTypes: List<ComplicationType>,
- /** The [DefaultComplicationProviderPolicy] to use. */
+ /**
+ * The [DefaultComplicationProviderPolicy] used to select the initial complication
+ * provider.
+ */
defaultProviderPolicy: DefaultComplicationProviderPolicy,
/**
- * The fractional bounds for the complication which are clamped to the unit square
- * [0..1], and subsequently converted to screen space coordinates. NB 0 and 1 are
+ * The initial fractional bounds for the complication which are clamped to the unit
+ * square [0..1], and subsequently converted to screen space coordinates. NB 0 and 1 are
* included in the unit square.
*/
unitSquareBounds: RectF
@@ -267,22 +280,29 @@
*/
@JvmStatic
public fun createBackgroundComplicationBuilder(
- /** The watch face's ID for this complication. */
+ /**
+ * The watch face's ID for this complication. Can be any integer but should be unique
+ * within the watch face.
+ */
id: Int,
/**
- * The renderer for this Complication. Renderers may not be sharable between complications.
+ * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+ * between complications.
*/
renderer: CanvasComplication,
/**
* The types of complication supported by this Complication. Passed into
* [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
- * configuration.
+ * configuration. This list should be non-empty.
*/
supportedTypes: List<ComplicationType>,
- /** The [DefaultComplicationProviderPolicy] to use. */
+ /**
+ * The [DefaultComplicationProviderPolicy] used to select the initial complication
+ * provider.
+ */
defaultProviderPolicy: DefaultComplicationProviderPolicy
): Builder = Builder(
id,
@@ -306,7 +326,9 @@
private var defaultProviderType = ComplicationType.NOT_CONFIGURED
/**
- * Sets the default complication provider data type.
+ * Sets the initial [ComplicationType] to use with the initial complication provider.
+ * Note care should be taken to ensure [defaultProviderType] is compatible with the
+ * [DefaultComplicationProviderPolicy].
*/
public fun setDefaultProviderType(
defaultProviderType: ComplicationType
@@ -340,22 +362,25 @@
private lateinit var complicationsManager: ComplicationsManager
private lateinit var invalidateListener: InvalidateListener
- private var _unitSquareBounds = unitSquareBounds
internal var unitSquareBoundsDirty = true
/**
* The screen space unit-square bounds of the complication. This is converted to pixels during
* rendering.
+ *
+ * Note it's not allowed to change the bounds of a background complication because
+ * they are assumed to always cover the entire screen.
*/
- public var unitSquareBounds: RectF
+ public var unitSquareBounds: RectF = unitSquareBounds
@UiThread
- get() = _unitSquareBounds
+ get
@UiThread
set(value) {
- if (_unitSquareBounds == value) {
+ require(boundsType != ComplicationBoundsType.BACKGROUND)
+ if (field == value) {
return
}
- _unitSquareBounds = value
+ field = value
unitSquareBoundsDirty = true
// The caller might modify a number of complications. For efficiency we need to coalesce
@@ -363,22 +388,19 @@
complicationsManager.scheduleUpdate()
}
- private var _enabled = true
internal var enabledDirty = true
- /**
- * Whether or not the complication should be drawn and accept taps.
- */
- public var enabled: Boolean
+ /** Whether or not the complication should be drawn and accept taps. */
+ public var enabled: Boolean = true
@JvmName("isEnabled")
@UiThread
- get() = _enabled
+ get
@UiThread
set(value) {
- if (_enabled == value) {
+ if (field == value) {
return
}
- _enabled = value
+ field = value
enabledDirty = true
// The caller might enable/disable a number of complications. For efficiency we need
@@ -388,40 +410,34 @@
}
}
- private var _renderer = canvasComplication
-
- /**
- * The [CanvasComplication] used to render the complication.
- */
- public var renderer: CanvasComplication
+ /** The [CanvasComplication] used to render the complication. */
+ public var renderer: CanvasComplication = canvasComplication
@UiThread
- get() = _renderer
+ get
@UiThread
set(value) {
- if (_renderer == value) {
+ if (field == value) {
return
}
renderer.onDetach()
value.idAndData = renderer.idAndData
- _renderer = value
+ field = value
value.onAttach(this)
}
- private var _supportedTypes = supportedTypes
internal var supportedTypesDirty = true
- /**
- * The types of complications the complication supports.
- */
- public var supportedTypes: List<ComplicationType>
+ /** The types of complications the complication supports. Must be non-empty. */
+ public var supportedTypes: List<ComplicationType> = supportedTypes
@UiThread
- get() = _supportedTypes
+ get
@UiThread
set(value) {
- if (_supportedTypes == value) {
+ if (field == value) {
return
}
- _supportedTypes = value
+ require(value.isNotEmpty())
+ field = value
supportedTypesDirty = true
// The caller might modify a number of complications. For efficiency we need to
@@ -431,22 +447,21 @@
}
}
- private var _defaultProviderPolicy = defaultProviderPolicy
internal var defaultProviderPolicyDirty = true
/**
* The [DefaultComplicationProviderPolicy] which defines the default complications providers
- * selected when the user hasn't yet made a choice. See also [.defaultProviderType].
+ * selected when the user hasn't yet made a choice. See also [defaultProviderType].
*/
- public var defaultProviderPolicy: DefaultComplicationProviderPolicy
+ public var defaultProviderPolicy: DefaultComplicationProviderPolicy = defaultProviderPolicy
@UiThread
- get() = _defaultProviderPolicy
+ get
@UiThread
set(value) {
- if (_defaultProviderPolicy == value) {
+ if (field == value) {
return
}
- _defaultProviderPolicy = value
+ field = value
defaultProviderPolicyDirty = true
// The caller might modify a number of complications. For efficiency we need to
@@ -459,11 +474,11 @@
internal var defaultProviderTypeDirty = true
/**
- * The default [ComplicationData.ComplicationType] to use alongside [.defaultProviderPolicy].
+ * The default [ComplicationData.ComplicationType] to use alongside [defaultProviderPolicy].
*/
public var defaultProviderType: ComplicationType = defaultProviderType
@UiThread
- get() = field
+ get
@UiThread
set(value) {
if (field == value) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
index 57699ba..4d5bb5d 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
@@ -30,9 +30,9 @@
public class ComplicationOutlineRenderer {
public companion object {
// Dashed lines are used for complication selection.
- internal val DASH_WIDTH = 10.0f
- internal var DASH_GAP = 2.0f
- internal var DASH_LENGTH = 5.0f
+ internal const val DASH_WIDTH = 10.0f
+ internal const val DASH_GAP = 2.0f
+ internal const val DASH_LENGTH = 5.0f
internal val dashPaint = Paint().apply {
strokeWidth = DASH_WIDTH
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 b9d9486..05e66c6 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
@@ -39,18 +39,17 @@
private fun getComponentName(context: Context) = ComponentName(
context.packageName,
- context.javaClass.typeName
+ context.javaClass.name
)
/**
- * The [Complication]s associated with the [WatchFace]. Dynamic creation of
- * complications isn't supported, however complications can be enabled and disabled, perhaps as
- * part of a user style see [androidx.wear.watchface.style.UserStyleSetting].
+ * The [Complication]s associated with the [WatchFace]. Dynamic creation of complications isn't
+ * supported, however complications can be enabled and disabled, perhaps as part of a
+ * user style see [androidx.wear.watchface.style.UserStyleSetting] and
+ * [ComplicationsUserStyleSetting].
*/
public class ComplicationsManager(
- /**
- * The complications associated with the watch face, may be empty.
- */
+ /** The complications associated with the watch face, may be empty. */
complicationCollection: Collection<Complication>,
/**
@@ -59,6 +58,10 @@
*/
private val userStyleRepository: UserStyleRepository
) {
+ /**
+ * Interface used to report user taps on the complication. See [addTapListener] and
+ * [removeTapListener].
+ */
public interface TapCallback {
/**
* Called when the user single taps on a complication.
@@ -81,12 +84,11 @@
private lateinit var renderer: Renderer
private lateinit var pendingUpdate: CancellableUniqueTask
- // A map of IDs to complications.
+ /** A map of complication IDs to complications. */
public val complications: Map<Int, Complication> =
complicationCollection.associateBy(Complication::id)
private class InitialComplicationConfig(
- val id: Int,
val unitSquareBounds: RectF,
val enabled: Boolean,
val supportedTypes: List<ComplicationType>,
@@ -102,7 +104,6 @@
{ it.id },
{
InitialComplicationConfig(
- it.id,
it.unitSquareBounds,
it.enabled,
it.supportedTypes,
@@ -188,7 +189,7 @@
}
}
- /** Returns the [Complication] corresponding to id or null. */
+ /** Returns the [Complication] corresponding to [id], if there is one, or `null`. */
public operator fun get(id: Int): Complication? = complications[id]
internal fun scheduleUpdate() {
@@ -299,8 +300,8 @@
}
/**
- * Brings attention to the complication by briefly highlighting it to provide visual
- * feedback when the user has tapped on it.
+ * Brings attention to the complication by briefly highlighting it to provide visual feedback
+ * when the user has tapped on it.
*
* @param complicationId The watch face's ID of the complication to briefly highlight
*/
@@ -324,7 +325,7 @@
}
/**
- * Returns the id of the complication at coordinates x, y or {@code null} if there isn't one.
+ * Returns the id of the complication at coordinates x, y or `null` if there isn't one.
*
* @param x The x coordinate of the point to perform a hit test
* @param y The y coordinate of the point to perform a hit test
@@ -337,9 +338,9 @@
}?.value
/**
- * Returns the background complication if there is one or {@code null} otherwise.
+ * Returns the background complication if there is one or `null` otherwise.
*
- * @return The background complication if there is one or {@code null} otherwise
+ * @return The background complication if there is one or `null` otherwise
*/
public fun getBackgroundComplication(): Complication? =
complications.entries.firstOrNull {
@@ -410,8 +411,7 @@
}
/**
- * Adds a [TapCallback] which is called whenever the user interacts with a
- * complication.
+ * Adds a [TapCallback] which is called whenever the user interacts with a complication.
*/
@UiThread
@SuppressLint("ExecutorRegistration")
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
deleted file mode 100644
index 8e7cde4..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
+++ /dev/null
@@ -1,312 +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.wear.watchface
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.icu.util.Calendar
-import android.opengl.EGL14
-import android.opengl.EGLConfig
-import android.opengl.EGLContext
-import android.opengl.EGLDisplay
-import android.opengl.EGLSurface
-import android.opengl.GLES20
-import android.util.Log
-import android.view.SurfaceHolder
-import androidx.annotation.CallSuper
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-import java.nio.ByteBuffer
-
-internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
- EGL14.EGL_RENDERABLE_TYPE,
- EGL14.EGL_OPENGL_ES2_BIT,
- EGL14.EGL_RED_SIZE,
- 8,
- EGL14.EGL_GREEN_SIZE,
- 8,
- EGL14.EGL_BLUE_SIZE,
- 8,
- EGL14.EGL_ALPHA_SIZE,
- 8,
- EGL14.EGL_NONE
-)
-
-private val EGL_CONTEXT_ATTRIB_LIST =
- intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
-
-internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
-
-/**
- * Watch faces that require [GLES20] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class GlesRenderer @JvmOverloads constructor(
- /** The [SurfaceHolder] that [render] will draw into. */
- surfaceHolder: SurfaceHolder,
-
- /** The associated [UserStyleRepository]. */
- userStyleRepository: UserStyleRepository,
-
- /** The associated [WatchState]. */
- watchState: WatchState,
-
- /**
- * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
- * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
- * recommended to use lower frame rates if possible for better battery life. Variable frame
- * 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)
- interactiveDrawModeUpdateDelayMillis: Long,
-
- /** Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBAB8888 back buffer. */
- private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
-
- /** The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty. */
- private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
- /** @hide */
- private companion object {
- private const val TAG = "Gles2WatchFace"
- }
-
- private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
- if (this == EGL14.EGL_NO_DISPLAY) {
- throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
- }
- // Initialize the display. The major and minor version numbers are passed back.
- val version = IntArray(2)
- if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
- throw RuntimeException("eglInitialize failed")
- }
- }
-
- private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
-
- @SuppressWarnings("SyntheticAccessor")
- private var eglContext: EGLContext? = EGL14.eglCreateContext(
- eglDisplay,
- eglConfig,
- EGL14.EGL_NO_CONTEXT,
- EGL_CONTEXT_ATTRIB_LIST,
- 0
- )
-
- init {
- if (eglContext == EGL14.EGL_NO_CONTEXT) {
- throw RuntimeException("eglCreateContext failed")
- }
- }
-
- private var eglSurface: EGLSurface? = null
- private var calledOnGlContextCreated = false
-
- /**
- * Chooses the EGLConfig to use.
- * @throws RuntimeException if [EGL14.eglChooseConfig] fails
- */
- private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
- val numEglConfigs = IntArray(1)
- val eglConfigs = arrayOfNulls<EGLConfig>(1)
- if (!EGL14.eglChooseConfig(
- eglDisplay,
- eglConfigAttribList,
- 0,
- eglConfigs,
- 0,
- eglConfigs.size,
- numEglConfigs,
- 0
- )
- ) {
- throw RuntimeException("eglChooseConfig failed")
- }
- if (numEglConfigs[0] == 0) {
- throw RuntimeException("no matching EGL configs")
- }
- return eglConfigs[0]!!
- }
-
- private fun createWindowSurface(width: Int, height: Int) {
- if (eglSurface != null) {
- if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
- Log.w(TAG, "eglDestroySurface failed")
- }
- }
- eglSurface = EGL14.eglCreateWindowSurface(
- eglDisplay,
- eglConfig,
- surfaceHolder.surface,
- eglSurfaceAttribList,
- 0
- )
- if (eglSurface == EGL14.EGL_NO_SURFACE) {
- throw RuntimeException("eglCreateWindowSurface failed")
- }
-
- makeContextCurrent()
- GLES20.glViewport(0, 0, width, height)
- if (!calledOnGlContextCreated) {
- calledOnGlContextCreated = true
- onGlContextCreated()
- }
- onGlSurfaceCreated(width, height)
- }
-
- @CallSuper
- override fun onDestroy() {
- if (eglSurface != null) {
- if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
- Log.w(TAG, "eglDestroySurface failed")
- }
- eglSurface = null
- }
- if (eglContext != null) {
- if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
- Log.w(TAG, "eglDestroyContext failed")
- }
- eglContext = null
- }
- if (eglDisplay != null) {
- if (!EGL14.eglTerminate(eglDisplay)) {
- Log.w(TAG, "eglTerminate failed")
- }
- eglDisplay = null
- }
- }
-
- /**
- * Sets our GL context to be the current one. This method *must* be called before any
- * OpenGL APIs are used.
- */
- private fun makeContextCurrent() {
- if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
- throw RuntimeException("eglMakeCurrent failed")
- }
- }
-
- internal override fun onPostCreate() {
- surfaceHolder.addCallback(object : SurfaceHolder.Callback {
- @SuppressLint("SyntheticAccessor")
- override fun surfaceChanged(
- holder: SurfaceHolder,
- format: Int,
- width: Int,
- height: Int
- ) {
- createWindowSurface(width, height)
- }
-
- @SuppressLint("SyntheticAccessor")
- override fun surfaceDestroyed(holder: SurfaceHolder) {
- if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
- Log.w(TAG, "eglDestroySurface failed")
- }
- eglSurface = null
- }
-
- override fun surfaceCreated(holder: SurfaceHolder) {
- }
- })
-
- createWindowSurface(
- surfaceHolder.surfaceFrame.width(),
- surfaceHolder.surfaceFrame.height()
- )
- }
-
- /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
- @UiThread
- public open fun onGlContextCreated() {}
-
- /**
- * Called when a new GL surface is created. It's safe to use GL APIs in this method.
- *
- * @param width width of surface in pixels
- * @param height height of surface in pixels
- */
- @UiThread
- public open fun onGlSurfaceCreated(width: Int, height: Int) {}
-
- internal override fun renderInternal(
- calendar: Calendar
- ) {
- makeContextCurrent()
- render(calendar)
- if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
- Log.w(TAG, "eglSwapBuffers failed")
- }
- }
-
- /** {@inheritDoc} */
- internal override fun takeScreenshot(
- calendar: Calendar,
- renderParameters: RenderParameters
- ): Bitmap {
- val width = screenBounds.width()
- val height = screenBounds.height()
- val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
- makeContextCurrent()
- val prevRenderParameters = this.renderParameters
- this.renderParameters = renderParameters
- render(calendar)
- this.renderParameters = prevRenderParameters
- GLES20.glFinish()
- GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf)
- // The image is flipped when using read pixels because the first pixel in the OpenGL buffer
- // is in bottom left.
- verticalFlip(pixelBuf, width, height)
- val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
- bitmap.copyPixelsFromBuffer(pixelBuf)
- return bitmap
- }
-
- private fun verticalFlip(
- buffer: ByteBuffer,
- width: Int,
- height: Int
- ) {
- var i = 0
- val tmp = ByteArray(width * 4)
- while (i++ < height / 2) {
- buffer[tmp]
- System.arraycopy(
- buffer.array(),
- buffer.limit() - buffer.position(),
- buffer.array(),
- buffer.position() - width * 4,
- width * 4
- )
- System.arraycopy(tmp, 0, buffer.array(), buffer.limit() - buffer.position(), width * 4)
- }
- buffer.rewind()
- }
-
- /**
- * Sub-classes should override this to implement their rendering logic which should respect
- * the current [DrawMode]. For correct functioning watch faces must use the supplied
- * [Calendar] and avoid using any other ways of getting the time.
- *
- * @param calendar The current [Calendar]
- */
- @UiThread
- public abstract fun render(calendar: Calendar)
-}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
index 1a8cf62..4caca47 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
@@ -25,7 +25,10 @@
import android.opengl.GLUtils
import androidx.annotation.Px
-/** Helper for rendering a complication to a GLES20 texture. */
+/**
+ * Helper for rendering a [CanvasComplication] to a GLES20 texture. To use call [renderToTexture]
+ * and then [bind] before drawing.
+ */
public class GlesTextureComplication(
/** The [CanvasComplication] to render to texture. */
public val canvasComplication: CanvasComplication,
@@ -50,7 +53,7 @@
private val canvas = Canvas(bitmap)
private val bounds = Rect(0, 0, textureWidth, textureHeight)
- /** Renders the complication to an OpenGL texture. */
+ /** Renders [canvasComplication] to an OpenGL texture. */
public fun renderToTexture(calendar: Calendar, renderParameters: RenderParameters) {
canvas.drawColor(Color.BLACK)
canvasComplication.render(canvas, bounds, calendar, renderParameters)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index d7f7ada..52ab4b0 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
@@ -19,7 +19,7 @@
import androidx.annotation.UiThread
/**
- * An observable UI thread only data holder class.
+ * An observable UI thread only data holder class (see [Observer]).
*
* @param <T> The type of data hold by this instance
*/
@@ -30,11 +30,13 @@
private val toBeRemoved = HashSet<Observer<T>>()
/** Whether or not this ObservableWatchData contains a value. */
+ @UiThread
public fun hasValue(): Boolean = _value != null
/**
* Returns the value contained within this ObservableWatchData or default if there isn't one.
*/
+ @UiThread
public fun getValueOr(default: T): T = if (_value != null) {
_value!!
} else {
@@ -67,9 +69,9 @@
}
/**
- * Adds the given observer to the observers list. The events are dispatched on the ui thread.
- * If there's any data held within the ObservableWatchData it will be immediately delivered to
- * the observer.
+ * Adds the given [Observer] to the observers list. If [hasValue] would return true then
+ * [Observer.onChanged] will be called. Subsequently [Observer.onChanged] will also be called
+ * any time [value] changes. All of these callbacks are assumed to occur on the UI thread.
*/
@UiThread
public fun addObserver(observer: Observer<T>) {
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 8a6ff58..bb4754b 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
@@ -16,17 +16,78 @@
package androidx.wear.watchface
+import android.annotation.SuppressLint
import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Rect
import android.icu.util.Calendar
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLDisplay
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.util.Log
import android.view.SurfaceHolder
+import androidx.annotation.CallSuper
+import androidx.annotation.IntDef
import androidx.annotation.IntRange
import androidx.annotation.Px
import androidx.annotation.UiThread
+import androidx.wear.watchface.Renderer.CanvasRenderer
+import androidx.wear.watchface.Renderer.GlesRenderer
import androidx.wear.watchface.style.UserStyleRepository
+import java.nio.ByteBuffer
+
+/**
+ * Describes the type of [Canvas] a [CanvasRenderer] should request from a [SurfaceHolder].
+ *
+ * @hide
+ */
+@IntDef(
+ value = [
+ CanvasType.SOFTWARE,
+ CanvasType.HARDWARE
+ ]
+)
+public annotation class CanvasType {
+ public companion object {
+ /** A software canvas will be requested. */
+ public const val SOFTWARE: Int = 0
+
+ /**
+ * A hardware canvas will be requested. This is usually faster than software rendering,
+ * however it can sometimes increase battery usage by rendering at a higher frame rate.
+ *
+ * NOTE this is only supported on API level 26 and above. On lower API levels we fall back
+ * to a software canvas.
+ */
+ public const val HARDWARE: Int = 1
+ }
+}
+
+internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
+ EGL14.EGL_RENDERABLE_TYPE,
+ EGL14.EGL_OPENGL_ES2_BIT,
+ EGL14.EGL_RED_SIZE,
+ 8,
+ EGL14.EGL_GREEN_SIZE,
+ 8,
+ EGL14.EGL_BLUE_SIZE,
+ 8,
+ EGL14.EGL_ALPHA_SIZE,
+ 8,
+ EGL14.EGL_NONE
+)
+
+private val EGL_CONTEXT_ATTRIB_LIST =
+ intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
+
+internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
/** The base class for [CanvasRenderer] and [GlesRenderer]. */
-public abstract class Renderer(
+public sealed class Renderer(
/** The [SurfaceHolder] that [renderInternal] will draw into. */
public val surfaceHolder: SurfaceHolder,
@@ -135,8 +196,8 @@
): Bitmap
/**
- * Called when the [DrawMode] has been updated. Will always be called before the first
- * call to onDraw().
+ * Called when the [RenderParameters] has been updated. Will always be called before the first
+ * call to [CanvasRenderer.render] or [GlesRenderer.render].
*/
@UiThread
protected open fun onRenderParametersChanged(renderParameters: RenderParameters) {
@@ -176,10 +237,15 @@
public open fun shouldAnimate(): Boolean =
watchState.isVisible.value && !watchState.isAmbient.value
- /** Schedules a call to [renderInternal] to draw the next frame. */
+ /**
+ * Schedules a call to either [CanvasRenderer.render] or [GlesRenderer.render] to draw the next
+ * frame.
+ */
@UiThread
public fun invalidate() {
- watchFaceHostApi.invalidate()
+ if (this::watchFaceHostApi.isInitialized) {
+ watchFaceHostApi.invalidate()
+ }
}
/**
@@ -187,6 +253,383 @@
* [invalidate], this method is thread-safe and may be called on any thread.
*/
public fun postInvalidate() {
- watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+ if (this::watchFaceHostApi.isInitialized) {
+ watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+ }
+ }
+
+ /**
+ * Watch faces that require [Canvas] rendering should extend their [Renderer] from this class.
+ */
+ public abstract class CanvasRenderer(
+ /**
+ * The [SurfaceHolder] from which a [Canvas] to will be obtained and passed into [render].
+ */
+ surfaceHolder: SurfaceHolder,
+
+ /** The watch face's associated [UserStyleRepository]. */
+ userStyleRepository: UserStyleRepository,
+
+ /** The watch face's associated [WatchState]. */
+ watchState: WatchState,
+
+ /** The type of canvas to request. */
+ @CanvasType private val canvasType: Int,
+
+ /**
+ * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+ * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+ * are recommended to use lower frame rates if possible for better battery life. Variable
+ * frame 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)
+ interactiveDrawModeUpdateDelayMillis: Long
+ ) : Renderer(
+ surfaceHolder,
+ userStyleRepository,
+ watchState,
+ interactiveDrawModeUpdateDelayMillis
+ ) {
+
+ @SuppressWarnings("UnsafeNewApiCall") // We check if the SDK is new enough.
+ internal override fun renderInternal(
+ calendar: Calendar
+ ) {
+ val canvas = (
+ if (canvasType == CanvasType.HARDWARE && android.os.Build.VERSION.SDK_INT >= 26) {
+ surfaceHolder.lockHardwareCanvas() // Requires API level 26.
+ } else {
+ surfaceHolder.lockCanvas()
+ }
+ ) ?: return
+ try {
+ if (watchState.isVisible.value) {
+ render(canvas, surfaceHolder.surfaceFrame, calendar)
+ } else {
+ canvas.drawColor(Color.BLACK)
+ }
+ } finally {
+ surfaceHolder.unlockCanvasAndPost(canvas)
+ }
+ }
+
+ /** {@inheritDoc} */
+ internal override fun takeScreenshot(
+ calendar: Calendar,
+ renderParameters: RenderParameters
+ ): Bitmap {
+ val bitmap = Bitmap.createBitmap(
+ screenBounds.width(),
+ screenBounds.height(),
+ Bitmap.Config.ARGB_8888
+ )
+ val prevRenderParameters = this.renderParameters
+ this.renderParameters = renderParameters
+ render(Canvas(bitmap), screenBounds, calendar)
+ this.renderParameters = prevRenderParameters
+ return bitmap
+ }
+
+ /**
+ * Sub-classes should override this to implement their rendering logic which should respect
+ * the current [DrawMode]. For correct functioning the CanvasRenderer must use the supplied
+ * [Calendar] in favor of any other ways of getting the time.
+ *
+ * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
+ * the [SurfaceHolder] backing the display
+ * @param bounds A [Rect] describing the bonds of the canvas to draw into
+ * @param calendar The current [Calendar]
+ */
+ @UiThread
+ public abstract fun render(
+ canvas: Canvas,
+ bounds: Rect,
+ calendar: Calendar
+ )
+ }
+
+ /**
+ * Watch faces that require [GLES20] rendering should extend their [Renderer] from this class.
+ */
+ public abstract class GlesRenderer @JvmOverloads constructor(
+ /** The [SurfaceHolder] whose [android.view.Surface] [render] will draw into. */
+ surfaceHolder: SurfaceHolder,
+
+ /** The associated [UserStyleRepository]. */
+ userStyleRepository: UserStyleRepository,
+
+ /** The associated [WatchState]. */
+ watchState: WatchState,
+
+ /**
+ * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+ * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+ * are recommended to use lower frame rates if possible for better battery life. Variable
+ * frame 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)
+ interactiveDrawModeUpdateDelayMillis: Long,
+
+ /**
+ * Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBA8888 back buffer.
+ */
+ private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
+
+ /**
+ * The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty.
+ */
+ private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
+ ) : Renderer(
+ surfaceHolder,
+ userStyleRepository,
+ watchState,
+ interactiveDrawModeUpdateDelayMillis
+ ) {
+ /** @hide */
+ private companion object {
+ private const val TAG = "Gles2WatchFace"
+ }
+
+ private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
+ if (this == EGL14.EGL_NO_DISPLAY) {
+ throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
+ }
+ // Initialize the display. The major and minor version numbers are passed back.
+ val version = IntArray(2)
+ if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
+ throw RuntimeException("eglInitialize failed")
+ }
+ }
+
+ private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
+
+ @SuppressWarnings("SyntheticAccessor")
+ private var eglContext: EGLContext? = EGL14.eglCreateContext(
+ eglDisplay,
+ eglConfig,
+ EGL14.EGL_NO_CONTEXT,
+ EGL_CONTEXT_ATTRIB_LIST,
+ 0
+ )
+
+ init {
+ if (eglContext == EGL14.EGL_NO_CONTEXT) {
+ throw RuntimeException("eglCreateContext failed")
+ }
+ }
+
+ private var eglSurface: EGLSurface? = null
+ private var calledOnGlContextCreated = false
+
+ /**
+ * Chooses the EGLConfig to use.
+ * @throws RuntimeException if [EGL14.eglChooseConfig] fails
+ */
+ private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
+ val numEglConfigs = IntArray(1)
+ val eglConfigs = arrayOfNulls<EGLConfig>(1)
+ if (!EGL14.eglChooseConfig(
+ eglDisplay,
+ eglConfigAttribList,
+ 0,
+ eglConfigs,
+ 0,
+ eglConfigs.size,
+ numEglConfigs,
+ 0
+ )
+ ) {
+ throw RuntimeException("eglChooseConfig failed")
+ }
+ if (numEglConfigs[0] == 0) {
+ throw RuntimeException("no matching EGL configs")
+ }
+ return eglConfigs[0]!!
+ }
+
+ private fun createWindowSurface(width: Int, height: Int) {
+ if (eglSurface != null) {
+ if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+ Log.w(TAG, "eglDestroySurface failed")
+ }
+ }
+ eglSurface = EGL14.eglCreateWindowSurface(
+ eglDisplay,
+ eglConfig,
+ surfaceHolder.surface,
+ eglSurfaceAttribList,
+ 0
+ )
+ if (eglSurface == EGL14.EGL_NO_SURFACE) {
+ throw RuntimeException("eglCreateWindowSurface failed")
+ }
+
+ makeContextCurrent()
+ GLES20.glViewport(0, 0, width, height)
+ if (!calledOnGlContextCreated) {
+ calledOnGlContextCreated = true
+ onGlContextCreated()
+ }
+ onGlSurfaceCreated(width, height)
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ if (eglSurface != null) {
+ if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+ Log.w(TAG, "eglDestroySurface failed")
+ }
+ eglSurface = null
+ }
+ if (eglContext != null) {
+ if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
+ Log.w(TAG, "eglDestroyContext failed")
+ }
+ eglContext = null
+ }
+ if (eglDisplay != null) {
+ if (!EGL14.eglTerminate(eglDisplay)) {
+ Log.w(TAG, "eglTerminate failed")
+ }
+ eglDisplay = null
+ }
+ }
+
+ /**
+ * Sets our GL context to be the current one. This method *must* be called before any OpenGL
+ * APIs are used.
+ */
+ private fun makeContextCurrent() {
+ if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
+ throw RuntimeException("eglMakeCurrent failed")
+ }
+ }
+
+ internal override fun onPostCreate() {
+ surfaceHolder.addCallback(object : SurfaceHolder.Callback {
+ @SuppressLint("SyntheticAccessor")
+ override fun surfaceChanged(
+ holder: SurfaceHolder,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ createWindowSurface(width, height)
+ }
+
+ @SuppressLint("SyntheticAccessor")
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+ Log.w(TAG, "eglDestroySurface failed")
+ }
+ eglSurface = null
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ }
+ })
+
+ createWindowSurface(
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
+ }
+
+ /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
+ @UiThread
+ public open fun onGlContextCreated() {
+ }
+
+ /**
+ * Called when a new GL surface is created. It's safe to use GL APIs in this method.
+ *
+ * @param width width of surface in pixels
+ * @param height height of surface in pixels
+ */
+ @UiThread
+ public open fun onGlSurfaceCreated(width: Int, height: Int) {
+ }
+
+ internal override fun renderInternal(
+ calendar: Calendar
+ ) {
+ makeContextCurrent()
+ render(calendar)
+ if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
+ Log.w(TAG, "eglSwapBuffers failed")
+ }
+ }
+
+ /** {@inheritDoc} */
+ internal override fun takeScreenshot(
+ calendar: Calendar,
+ renderParameters: RenderParameters
+ ): Bitmap {
+ val width = screenBounds.width()
+ val height = screenBounds.height()
+ val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
+ makeContextCurrent()
+ val prevRenderParameters = this.renderParameters
+ this.renderParameters = renderParameters
+ render(calendar)
+ this.renderParameters = prevRenderParameters
+ GLES20.glFinish()
+ GLES20.glReadPixels(
+ 0,
+ 0,
+ width,
+ height,
+ GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE,
+ pixelBuf
+ )
+ // The image is flipped when using read pixels because the first pixel in the OpenGL
+ // buffer is in bottom left.
+ verticalFlip(pixelBuf, width, height)
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ bitmap.copyPixelsFromBuffer(pixelBuf)
+ return bitmap
+ }
+
+ private fun verticalFlip(
+ buffer: ByteBuffer,
+ width: Int,
+ height: Int
+ ) {
+ var i = 0
+ val tmp = ByteArray(width * 4)
+ while (i++ < height / 2) {
+ buffer[tmp]
+ System.arraycopy(
+ buffer.array(),
+ buffer.limit() - buffer.position(),
+ buffer.array(),
+ buffer.position() - width * 4,
+ width * 4
+ )
+ System.arraycopy(
+ tmp,
+ 0,
+ buffer.array(),
+ buffer.limit() - buffer.position(),
+ width * 4
+ )
+ }
+ buffer.rewind()
+ }
+
+ /**
+ * Sub-classes should override this to implement their rendering logic which should respect
+ * the current [DrawMode]. For correct functioning the GlesRenderer must use the supplied
+ * [Calendar] in favor of any other ways of getting the time.
+ *
+ * @param calendar The current [Calendar]
+ */
+ @UiThread
+ public abstract fun render(calendar: Calendar)
}
}
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 cdb2fff..a101a9c 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
@@ -105,8 +105,8 @@
}
/**
- * A WatchFace is constructed by a user's [WatchFaceService] and brings together rendering,
- * styling, complications and state observers.
+ * The return value of [WatchFaceService.createWatchFace] which brings together rendering, styling,
+ * complications and state observers.
*/
public class WatchFace(
/**
@@ -115,13 +115,13 @@
*/
@WatchFaceType internal var watchFaceType: Int,
- /** The {@UserStyleRepository} for this WatchFaceImpl. */
+ /** The [UserStyleRepository] for this WatchFace. */
internal val userStyleRepository: UserStyleRepository,
- /** The [ComplicationsManager] for this WatchFaceImpl. */
+ /** The [ComplicationsManager] for this WatchFace. */
internal var complicationsManager: ComplicationsManager,
- /** The [Renderer] for this WatchFaceImpl. */
+ /** The [Renderer] for this WatchFace. */
internal val renderer: Renderer
) {
internal var tapListener: TapListener? = null
@@ -210,7 +210,7 @@
}
}
- /** The preview time in milliseconds since the epoch, or null if not set. */
+ /** The UTC preview time in milliseconds since the epoch, or null if not set. */
@get:SuppressWarnings("AutoBoxing")
@IntRange(from = 0)
public var overridePreviewReferenceTimeMillis: Long? = null
@@ -363,7 +363,7 @@
private val componentName =
ComponentName(
watchFaceHostApi.getContext().packageName,
- watchFaceHostApi.getContext().javaClass.typeName
+ watchFaceHostApi.getContext().javaClass.name
)
internal fun getWatchFaceStyle() = WatchFaceStyle(
@@ -419,7 +419,7 @@
} else {
// The system doesn't support preference persistence we need to do it ourselves.
val preferencesFile =
- "watchface_prefs_${watchFaceHostApi.getContext().javaClass.typeName}.txt"
+ "watchface_prefs_${watchFaceHostApi.getContext().javaClass.name}.txt"
userStyleRepository.userStyle = UserStyle(
readPrefs(watchFaceHostApi.getContext(), preferencesFile),
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 4b41029..8067f95 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
@@ -121,22 +121,19 @@
/**
* WatchFaceService and [WatchFace] are a pair of classes intended to handle much of
* the boilerplate needed to implement a watch face without being too opinionated. The suggested
- * structure of an WatchFaceService based watch face is:
+ * structure of a WatchFaceService based watch face is:
*
* @sample androidx.wear.watchface.samples.kDocCreateExampleWatchFaceService
*
- * Base classes for complications and styles are provided along with a default UI for configuring
- * them. Complications are optional, however if required, WatchFaceService assumes all
- * complications can be enumerated up front and passed as a collection into [ComplicationsManager]'s
- * constructor which is passed to [WatchFace]'s constructor. Some watch faces support different
- * configurations (number & position) of complications and this can be achieved by rendering a
- * subset and only marking the ones you need as enabled (see
- * [UserStyleSetting.ComplicationsUserStyleSetting]).
+ * Sub classes of WatchFaceService are expected to implement [createWatchFace] which is the
+ * factory for making [WatchFace]s. All [Complication]s are assumed to be enumerated up upfront and
+ * passed as a collection into [ComplicationsManager]'s constructor which is in turn passed to
+ * [WatchFace]'s constructor. Complications can be enabled and disabled as needed although it's
+ * recommended to use [UserStyleSetting.ComplicationsUserStyleSetting] for this.
*
- * Many watch faces support styles, typically controlling the color and visual look of watch face
- * elements such as numeric fonts, watch hands and ticks. WatchFaceService doesn't take an
- * an opinion on what comprises a style beyond it should be representable as a map of categories to
- * options.
+ * Watch face styling (color and visual look of watch face elements such as numeric fonts, watch
+ * hands and ticks, etc...) is directly supported via [UserStyleSetting] and
+ * [UserStyleRepository].
*
* To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down
* time, and to loop between two instants. This is controlled by MOCK_TIME_INTENT intents
@@ -199,7 +196,6 @@
*
* Multiple watch faces can be defined in the same package, requiring multiple <service> tags.
*/
-@RequiresApi(26)
public abstract class WatchFaceService : WallpaperService() {
/** @hide */
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index f9888b2..26df645 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -19,25 +19,10 @@
import android.app.NotificationManager
import androidx.annotation.RestrictTo
-import androidx.annotation.IntDef
-
-/** @hide */
-@IntDef(
- value = [
- ScreenShape.ROUND,
- ScreenShape.RECTANGULAR
- ]
-)
-public annotation class ScreenShape {
- public companion object {
- /** The watch screen has a circular shape. */
- public const val ROUND: Int = 1
-
- /** The watch screen has a rectangular or square shape. */
- public const val RECTANGULAR: Int = 2
- }
-}
-
+/**
+ * Describes the current state of the wearable including some hardware details such as whether or
+ * not it supports burn in prevention and low-bit ambient.
+ */
public class WatchState(
/**
* The current user interruption settings. See [NotificationManager]. Based on the value
@@ -79,10 +64,6 @@
@get:JvmName("hasBurnInProtection")
public val hasBurnInProtection: Boolean,
- /** The physical shape of the screen. */
- @ScreenShape
- public val screenShape: Int,
-
/** UTC reference time for previews of analog watch faces in milliseconds since the epoch. */
public val analogPreviewReferenceTimeMillis: Long,
@@ -100,8 +81,6 @@
public val isVisible: MutableObservableWatchData<Boolean> = MutableObservableWatchData()
public var hasLowBitAmbient: Boolean = false
public var hasBurnInProtection: Boolean = false
- @ScreenShape
- public var screenShape: Int = 0
public var analogPreviewReferenceTimeMillis: Long = 0
public var digitalPreviewReferenceTimeMillis: Long = 0
@@ -112,7 +91,6 @@
isVisible = isVisible,
hasLowBitAmbient = hasLowBitAmbient,
hasBurnInProtection = hasBurnInProtection,
- screenShape = screenShape,
analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis
)
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 e8a1f0d..1165fbe9 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
@@ -169,7 +169,7 @@
userStyleRepository: UserStyleRepository,
watchState: WatchState,
interactiveFrameRateMs: Long
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
surfaceHolder,
userStyleRepository,
watchState,
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 1ad9e17..da685aa 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
@@ -1852,4 +1852,17 @@
assertThat(leftComplication.isActiveAt(2000000)).isTrue()
assertThat(leftComplication.isActiveAt(2000001)).isFalse()
}
+
+ @Test
+ fun invalidateRendererBeforeFullInit() {
+ renderer = TestRenderer(
+ surfaceHolder,
+ UserStyleRepository(UserStyleSchema(emptyList())),
+ watchState.asWatchState(),
+ INTERACTIVE_UPDATE_RATE_MS
+ )
+
+ // This should not throw an exception.
+ renderer.invalidate()
+ }
}
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
index 685668c..0c48269 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
@@ -18,7 +18,7 @@
import android.content.ComponentName
import android.content.Context
-import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.icu.util.Calendar
@@ -28,10 +28,10 @@
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationType
import androidx.wear.watchface.CanvasComplicationDrawable
+import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.Complication
import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.MutableWatchState
-import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFaceTestRunner
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
@@ -196,20 +196,14 @@
val complicationSet = ComplicationsManager(
complications,
userStyleRepository,
- object : Renderer(
+ object : Renderer.CanvasRenderer(
surfaceHolder,
userStyleRepository,
watchState.asWatchState(),
+ CanvasType.SOFTWARE,
INTERACTIVE_UPDATE_RATE_MS
) {
- override fun renderInternal(calendar: Calendar) {}
-
- override fun takeScreenshot(
- calendar: Calendar,
- renderParameters: RenderParameters
- ): Bitmap {
- throw RuntimeException("Not Implemented!")
- }
+ override fun render(canvas: Canvas, bounds: Rect, calendar: Calendar) {}
}
)
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index 2e3073a..c6f4bf9 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -317,14 +317,13 @@
}
public static interface WearArcLayout.ArcLayoutWidget {
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getSweepAngleDegrees();
method public int getThicknessPx();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
}
- public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+ public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
ctor public WearArcLayout.LayoutParams(int, int);
ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +341,7 @@
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getAnchorAngleDegrees();
method public int getAnchorType();
method public boolean getClockwise();
@@ -358,7 +357,6 @@
method public float getTextSize();
method public int getThicknessPx();
method public android.graphics.Typeface? getTypeface();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
method public void setAnchorAngleDegrees(float);
method public void setAnchorType(int);
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index 13c916ff..ebd25b4 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -317,14 +317,13 @@
}
public static interface WearArcLayout.ArcLayoutWidget {
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getSweepAngleDegrees();
method public int getThicknessPx();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
}
- public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+ public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
ctor public WearArcLayout.LayoutParams(int, int);
ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +341,7 @@
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getAnchorAngleDegrees();
method public int getAnchorType();
method public boolean getClockwise();
@@ -358,7 +357,6 @@
method public float getTextSize();
method public int getThicknessPx();
method public android.graphics.Typeface? getTypeface();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
method public void setAnchorAngleDegrees(float);
method public void setAnchorType(int);
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 07cb190..5640c27 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -321,14 +321,13 @@
}
public static interface WearArcLayout.ArcLayoutWidget {
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getSweepAngleDegrees();
method public int getThicknessPx();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
}
- public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+ public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
ctor public WearArcLayout.LayoutParams(int, int);
ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -349,7 +348,7 @@
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
- method public void checkInvalidAttributeAsChild(boolean);
+ method public void checkInvalidAttributeAsChild();
method public float getAnchorAngleDegrees();
method public int getAnchorType();
method public boolean getClockwise();
@@ -365,7 +364,6 @@
method public float getTextSize();
method public int getThicknessPx();
method public android.graphics.Typeface? getTypeface();
- method public boolean handleLayoutRotate(float);
method public boolean insideClickArea(float, float);
method public void setAnchorAngleDegrees(float);
method public void setAnchorType(@androidx.wear.widget.WearArcLayout.AnchorType int);
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
index d6b8bb7..39f35a4 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
@@ -23,19 +23,21 @@
import static androidx.wear.widget.util.AsyncViewActions.waitForMatchingView;
import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
-import android.app.Activity;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
import androidx.wear.test.R;
import androidx.wear.widget.util.WakeLockRule;
@@ -53,131 +55,198 @@
@Rule
public final WakeLockRule wakeLock = new WakeLockRule();
- @Rule
- public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
- new ActivityTestRule<>(
- DismissibleFrameLayoutTestActivity.class,
- true, /** initial touch mode */
- false /** launchActivity */
- );
-
@Test
public void testBackDismiss() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayout(true, false, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN back button pressed
- sendBackKey();
- // AND hidden
- assertHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutIntent())) {
+ configureDismissibleLayout(scenario, true, false, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN back button pressed
+ sendBackKey();
+ // AND hidden
+ assertHidden(R.id.dismissible_root);
+ // Back button up event is consumed, and not pass to the activity
+ scenario.onActivity(activity -> {
+ assertFalse(activity.mConsumeBackButtonUp);
+ });
+ }
}
@Test
public void testBackNotDismissIfDisabled() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayout(false, false, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN back button pressed
- sendBackKey();
- // AND the layout is still nor hidden
- assertNotHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutIntent())) {
+ configureDismissibleLayout(scenario, false, false, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN back button pressed
+ sendBackKey();
+ // AND the layout is still not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // Back button up event is not consumed, and continue to pass to the activity
+ scenario.onActivity(activity -> {
+ assertTrue(activity.mConsumeBackButtonUp);
+ });
+ }
}
@Test
public void testSwipeDismiss() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayout(false, true, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN perform a swipe dismiss
- onView(withId(R.id.dismissible_root)).perform(swipeRight());
- // AND hidden
- assertHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutIntent())) {
+ configureDismissibleLayout(scenario, false, true, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN perform a swipe dismiss
+ onView(withId(R.id.dismissible_root)).perform(swipeRight());
+ // AND hidden
+ assertHidden(R.id.dismissible_root);
+ }
}
@Test
public void testSwipeNotDismissIfDisabled() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayout(false, false, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN perform a swipe dismiss
- onView(withId(R.id.dismissible_root)).perform(swipeRight());
- // AND the layout is still nor hidden
- assertNotHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutIntent())) {
+ configureDismissibleLayout(scenario, false, false, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN perform a swipe dismiss
+ onView(withId(R.id.dismissible_root)).perform(swipeRight());
+ // AND the layout is still nor hidden
+ assertNotHidden(R.id.dismissible_root);
+ }
+ }
+
+ @Test
+ public void testDisableThenEnableBackDismiss() {
+ // GIVEN a freshly setup DismissibleFrameLayout
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutIntent())) {
+ final DismissibleFrameLayout[] testLayout = new DismissibleFrameLayout[1];
+ final DismissibleFrameLayoutTestActivity[] testActivity =
+ new DismissibleFrameLayoutTestActivity[1];
+ scenario.onActivity(activity -> {
+ testActivity[0] = activity;
+ testLayout[0] =
+ (DismissibleFrameLayout) activity.findViewById(R.id.dismissible_root);
+ testLayout[0].registerCallback(mDismissCallback);
+ // Disable back button dismiss
+ testLayout[0].setBackButtonDismissible(false);
+ });
+
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // The layout is not focused
+ assertFalse(testActivity[0].getCurrentFocus() == testLayout[0]);
+ // WHEN back button pressed
+ testActivity[0].mConsumeBackButtonUp = false;
+ sendBackKey();
+ // AND the layout is still not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // Back button up event is not consumed, and continue to pass to the activity
+ assertTrue(testActivity[0].mConsumeBackButtonUp);
+
+ // Enable backButton dismiss, we have to run this on the main thread
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ testLayout[0].setBackButtonDismissible(true);
+ }
+ });
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // The layout is focused
+ assertTrue(testActivity[0].getCurrentFocus() == testLayout[0]);
+ // WHEN back button pressed
+ testActivity[0].mConsumeBackButtonUp = false;
+ sendBackKey();
+ // AND the layout is hidden
+ assertHidden(R.id.dismissible_root);
+ // Back button up event is consumed without passing up to the activity
+ assertFalse(testActivity[0].mConsumeBackButtonUp);
+ }
}
@Test
public void testBackDismissWithRecyclerView() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayoutWithRecyclerView(
- true, false, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN back button pressed
- sendBackKey();
- // AND hidden
- assertHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+ configureDismissibleLayout(scenario, true, false, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN back button pressed
+ sendBackKey();
+ // AND hidden
+ assertHidden(R.id.dismissible_root);
+ // Back button up event is consumed, and not pass to the activity
+ scenario.onActivity(activity -> {
+ assertFalse(activity.mConsumeBackButtonUp);
+ });
+ }
}
@Test
public void testSwipeDismissWithRecyclerView() {
// GIVEN a freshly setup DismissibleFrameLayout
- setUpDismissibleLayoutWithRecyclerView(
- false, true, mDismissCallback);
- // CHECK the layout is not hidden
- assertNotHidden(R.id.dismissible_root);
- // WHEN perform a swipe dismiss
- onView(withId(R.id.dismissible_root)).perform(swipeRight());
- // AND hidden
- assertHidden(R.id.dismissible_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+ configureDismissibleLayout(scenario, false, true, mDismissCallback);
+ // CHECK the layout is not hidden
+ assertNotHidden(R.id.dismissible_root);
+ // WHEN perform a swipe dismiss
+ onView(withId(R.id.dismissible_root)).perform(swipeRight());
+ // AND hidden
+ assertHidden(R.id.dismissible_root);
+ }
}
/**
- * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
- * a single static child.
+ * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+ * with a single static child.
*/
- private void setUpDismissibleLayout(
- boolean backDismissible,
- boolean swipeable,
- @Nullable DismissibleFrameLayout.Callback callback) {
- activityRule.launchActivity(
- new Intent()
- .putExtra(
- LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
- androidx.wear.test.R.layout.dismissible_frame_layout_testcase));
-
- configureDismissibleLayout(backDismissible, swipeable, callback);
+ private Intent createDismissibleLayoutIntent() {
+ return new Intent()
+ .setClass(ApplicationProvider.getApplicationContext(),
+ DismissibleFrameLayoutTestActivity.class)
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.dismissible_frame_layout_testcase);
}
- private void setUpDismissibleLayoutWithRecyclerView(
- boolean backDismissible,
- boolean swipeable,
- @Nullable DismissibleFrameLayout.Callback callback) {
- Intent launchIntent = new Intent();
- launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.dismissible_frame_layout_recyclerview_testcase);
- launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
- activityRule.launchActivity(launchIntent);
-
- configureDismissibleLayout(backDismissible, swipeable, callback);
+ /**
+ * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+ * with a child of scrollable container.
+ */
+ private Intent createDismissibleLayoutWithRecyclerViewIntent() {
+ return new Intent()
+ .setClass(ApplicationProvider.getApplicationContext(),
+ DismissibleFrameLayoutTestActivity.class)
+ .putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.dismissible_frame_layout_recyclerview_testcase)
+ .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
}
private void configureDismissibleLayout(
+ ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
boolean backDismissible,
boolean swipeable,
@Nullable DismissibleFrameLayout.Callback callback) {
- Activity activity = activityRule.getActivity();
- DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
- testLayout.setBackButtonDismissible(backDismissible);
- testLayout.setSwipeDismissible(swipeable);
-
- if (callback != null) {
- testLayout.registerCallback(callback);
- }
+ scenario.onActivity(activity -> {
+ DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
+ testLayout.setBackButtonDismissible(backDismissible);
+ testLayout.setSwipeDismissible(swipeable);
+ if (callback != null) {
+ testLayout.registerCallback(callback);
+ }
+ });
}
private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
index f9132b3..2b2158c 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
@@ -18,6 +18,7 @@
import android.os.Bundle;
import android.view.Gravity;
+import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -29,6 +30,7 @@
public class DismissibleFrameLayoutTestActivity extends LayoutTestActivity {
public static final String EXTRA_LAYOUT_HORIZONTAL = "layout_horizontal";
+ public boolean mConsumeBackButtonUp = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -54,6 +56,14 @@
recyclerView.setAdapter(new MyRecyclerViewAdapter());
}
+ public boolean onKeyUp(int keyCode, KeyEvent evnet) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ mConsumeBackButtonUp = true;
+ return true;
+ }
+ return false;
+ }
+
private static class MyRecyclerViewAdapter
extends RecyclerView.Adapter<MyRecyclerViewAdapter.CustomViewHolder> {
@Override
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
index e493360..472b86e 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
@@ -27,13 +27,14 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import android.app.Activity;
import android.content.Intent;
import android.graphics.RectF;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.action.GeneralLocation;
import androidx.test.espresso.action.GeneralSwipeAction;
@@ -44,7 +45,6 @@
import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
import androidx.wear.test.R;
import androidx.wear.widget.util.ArcSwipe;
import androidx.wear.widget.util.FrameLocationAvoidingEdges;
@@ -65,14 +65,6 @@
@Rule
public final WakeLockRule wakeLock = new WakeLockRule();
- @Rule
- public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
- new ActivityTestRule<>(
- DismissibleFrameLayoutTestActivity.class,
- true, /** initial touch mode */
- false /** launchActivity */
- );
-
private int mLayoutWidth;
private int mLayoutHeight;
private int mXPositionOnScreen;
@@ -81,228 +73,265 @@
@Test
public void testCanScrollHorizontally() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- SwipeDismissFrameLayout testLayout =
- (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
- testLayout.setSwipeable(true);
- // WHEN we check that the layout is horizontally scrollable from left to right.
- // THEN the layout is found to be horizontally swipeable from left to right.
- assertTrue(testLayout.canScrollHorizontally(-20));
- // AND the layout is found to NOT be horizontally swipeable from right to left.
- assertFalse(testLayout.canScrollHorizontally(20));
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ scenario.onActivity(activity -> {
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ testLayout.setSwipeable(true);
+ // WHEN we check that the layout is horizontally scrollable from left to right.
+ // THEN the layout is found to be horizontally swipeable from left to right.
+ assertTrue(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
- // WHEN we switch off the swipe-to-dismiss functionality for the layout
- testLayout.setSwipeable(false);
- // THEN the layout is found NOT to be horizontally swipeable from left to right.
- assertFalse(testLayout.canScrollHorizontally(-20));
- // AND the layout is found to NOT be horizontally swipeable from right to left.
- assertFalse(testLayout.canScrollHorizontally(20));
+ // WHEN we switch off the swipe-to-dismiss functionality for the layout
+ testLayout.setSwipeable(false);
+ // THEN the layout is found NOT to be horizontally swipeable from left to right.
+ assertFalse(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
+ });
+ }
}
@Test
public void canScrollHorizontallyShouldBeFalseWhenInvisible() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
- // GIVEN the layout is invisible
- // Note: We have to run this on the main thread, because of thread checks in View.java.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- testLayout.setVisibility(View.INVISIBLE);
- }
- });
- // WHEN we check that the layout is horizontally scrollable
- // THEN the layout is found to be NOT horizontally swipeable from left to right.
- assertFalse(testLayout.canScrollHorizontally(-20));
- // AND the layout is found to NOT be horizontally swipeable from right to left.
- assertFalse(testLayout.canScrollHorizontally(20));
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+ scenario.onActivity(activity -> {
+ testLayout[0] =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ });
+ // GIVEN the layout is invisible
+ // Note: We have to run this on the main thread, because of thread checks in View.java.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ testLayout[0].setVisibility(View.INVISIBLE);
+ }
+ });
+ // WHEN we check that the layout is horizontally scrollable
+ // THEN the layout is found to be NOT horizontally swipeable from left to right.
+ assertFalse(testLayout[0].canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout[0].canScrollHorizontally(20));
+ }
}
@Test
public void canScrollHorizontallyShouldBeFalseWhenGone() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- final SwipeDismissFrameLayout testLayout =
- (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
- // GIVEN the layout is gone
- // Note: We have to run this on the main thread, because of thread checks in View.java.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- testLayout.setVisibility(View.GONE);
- }
- });
- // WHEN we check that the layout is horizontally scrollable
- // THEN the layout is found to be NOT horizontally swipeable from left to right.
- assertFalse(testLayout.canScrollHorizontally(-20));
- // AND the layout is found to NOT be horizontally swipeable from right to left.
- assertFalse(testLayout.canScrollHorizontally(20));
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+ scenario.onActivity(activity -> {
+ testLayout[0] =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ });
+ // GIVEN the layout is gone
+ // Note: We have to run this on the main thread, because of thread checks in View.java.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ testLayout[0].setVisibility(View.GONE);
+ }
+ });
+ // WHEN we check that the layout is horizontally scrollable
+ // THEN the layout is found to be NOT horizontally swipeable from left to right.
+ assertFalse(testLayout[0].canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout[0].canScrollHorizontally(20));
+ }
}
@Test
public void testSwipeDismissDisabledByDefault() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- SwipeDismissFrameLayout testLayout =
- (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
- // WHEN we check that the layout is dismissible
- // THEN the layout is find to be dismissible
- assertFalse(testLayout.isSwipeable());
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ scenario.onActivity(activity -> {
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ // WHEN we check that the layout is dismissible
+ // THEN the layout is find to be dismissible
+ assertFalse(testLayout.isSwipeable());
+ });
+ }
}
@Test
public void testSwipeDismissesViewIfEnabled() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
- .setSwipeable(true);
- // WHEN we perform a swipe to dismiss
- onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
- // AND hidden
- assertHidden(R.id.swipe_dismiss_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ setUpSwipeableAndCallback(scenario, true);
+ // WHEN we perform a swipe to dismiss
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // AND hidden
+ assertHidden(R.id.swipe_dismiss_root);
+ }
}
@Test
public void testSwipeDoesNotDismissViewIfDisabled() {
// GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- SwipeDismissFrameLayout testLayout =
- (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
- testLayout.setSwipeable(false);
- // WHEN we perform a swipe to dismiss
- onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
- // THEN the layout is not hidden
- assertNotHidden(R.id.swipe_dismiss_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ setUpSwipeableAndCallback(scenario, false);
+ // WHEN we perform a swipe to dismiss
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
}
@Test
public void testAddRemoveCallback() {
// GIVEN a freshly setup SwipeDismissFrameLayout
- setUpSimpleLayout();
- Activity activity = activityRule.getActivity();
- SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
- // WHEN we remove the swipe callback
- testLayout.removeCallback(mDismissCallback);
- onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
- // THEN the layout is not hidden
- assertNotHidden(R.id.swipe_dismiss_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+ setUpSwipeableAndCallback(scenario, true);
+ // WHEN we remove the swipe callback
+ scenario.onActivity(activity -> {
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ testLayout.removeCallback(mDismissCallback);
+ });
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
}
@Test
public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable {
// GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
- setUpSwipeDismissWithHorizontalRecyclerView();
- activityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Activity activity = activityRule.getActivity();
- RecyclerView testLayout = activity.findViewById(R.id.recycler_container);
- // Scroll to a position from which the child is scrollable.
- testLayout.scrollToPosition(50);
- }
- });
-
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // WHEN we perform a swipe to dismiss from the center of the screen.
- onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
- // THEN the layout is not hidden
- assertNotHidden(R.id.swipe_dismiss_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(
+ createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+ )) {
+ setUpSwipeableAndCallback(scenario, true);
+ scenario.onActivity(activity -> {
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ RecyclerView testRecyclerView = activity.findViewById(R.id.recycler_container);
+ testRecyclerView.scrollToPosition(50);
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // WHEN we perform a swipe to dismiss from the center of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
}
@Test
public void testEdgeSwipeDoesDismissViewIfScrollable() {
// GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
- setUpSwipeDismissWithHorizontalRecyclerView();
- Activity activity = activityRule.getActivity();
- ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
- .setSwipeable(true);
- // WHEN we perform a swipe to dismiss from the left edge of the screen.
- onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
- // THEN the layout is hidden
- assertHidden(R.id.swipe_dismiss_root);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(
+ createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+ )) {
+ setUpSwipeableAndCallback(scenario, true);
+ // WHEN we perform a swipe to dismiss from the left edge of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
+ // THEN the layout is hidden
+ assertHidden(R.id.swipe_dismiss_root);
+ }
}
@Test
@FlakyTest
public void testArcSwipeDoesNotTriggerDismiss() {
// GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content
- setUpSwipeDismissWithVerticalRecyclerView();
- int center = mLayoutHeight / 2;
- int halfBound = mLayoutWidth / 2;
- RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
- // WHEN the view is scrolled on an arc from top to bottom.
- onView(withId(R.id.swipe_dismiss_root)).perform(
- swipeTopFromBottomOnArcAvoidingEdge(bounds));
- // THEN the layout is not dismissed and not hidden.
- assertNotHidden(R.id.swipe_dismiss_root);
- // AND the content view is scrolled.
- assertScrolledY(R.id.recycler_container);
+ try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+ ActivityScenario.launch(
+ createSwipeDismissWithVerticalRecyclerViewLaunchIntent()
+ )) {
+ setUpSwipeableAndCallback(scenario, true);
+ int center = mLayoutHeight / 2;
+ int halfBound = mLayoutWidth / 2;
+ RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
+ // WHEN the view is scrolled on an arc from top to bottom.
+ onView(withId(R.id.swipe_dismiss_root)).perform(
+ swipeTopFromBottomOnArcAvoidingEdge(bounds));
+ // THEN the layout is not dismissed and not hidden.
+ assertNotHidden(R.id.swipe_dismiss_root);
+ // AND the content view is scrolled.
+ assertScrolledY(R.id.recycler_container);
+ }
}
/**
- * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
- * a single static child.
+ * Creates intent for launching the simplest possible layout for test cases - a
+ * {@link SwipeDismissFrameLayout} with a single static child.
*/
- private void setUpSimpleLayout() {
- activityRule.launchActivity(
- new Intent()
- .putExtra(
- LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.swipe_dismiss_layout_testcase_1));
- setDismissCallback();
+ private Intent createSimpleLayoutLaunchIntent() {
+ return new Intent()
+ .setClass(ApplicationProvider.getApplicationContext(),
+ DismissibleFrameLayoutTestActivity.class)
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_1);
}
/**
- * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
- * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link
- * RecyclerView} as a child, ready to accept an adapter.
+ * Creates intent for launching a slightly more involved layout for testing swipe-to-dismiss
+ * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+ * horizontal {@link RecyclerView} as a child, ready to accept an adapter.
*/
- private void setUpSwipeDismissWithHorizontalRecyclerView() {
- Intent launchIntent = new Intent();
- launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.swipe_dismiss_layout_testcase_2);
- launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
- activityRule.launchActivity(launchIntent);
- setDismissCallback();
+ private Intent createSwipeDismissWithHorizontalRecyclerViewLaunchIntent() {
+ return new Intent()
+ .setClass(ApplicationProvider.getApplicationContext(),
+ DismissibleFrameLayoutTestActivity.class)
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_2)
+ .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
}
/**
- * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
- * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link
- * WearableRecyclerView} as a child, ready to accept an adapter.
+ * Creates intent for launching slightly more involved layout for testing swipe-to-dismiss
+ * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+ * vertical {@link WearableRecyclerView} as a child, ready to accept an adapter.
*/
- private void setUpSwipeDismissWithVerticalRecyclerView() {
- Intent launchIntent = new Intent();
- launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.swipe_dismiss_layout_testcase_2);
+ private Intent createSwipeDismissWithVerticalRecyclerViewLaunchIntent() {
+ Intent launchIntent = new Intent()
+ .setClass(ApplicationProvider.getApplicationContext(),
+ DismissibleFrameLayoutTestActivity.class)
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_2);
launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false);
- activityRule.launchActivity(launchIntent);
- setDismissCallback();
+
+ return launchIntent;
}
- private void setDismissCallback() {
- setCallback(mDismissCallback);
- }
-
- private void setCallback(SwipeDismissFrameLayout.Callback callback) {
- Activity activity = activityRule.getActivity();
- SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+ private void setDismissCallback(SwipeDismissFrameLayout testLayout) {
int[] locationOnScreen = new int[2];
testLayout.getLocationOnScreen(locationOnScreen);
mXPositionOnScreen = locationOnScreen[0];
mYPositionOnScreen = locationOnScreen[1];
mLayoutWidth = testLayout.getWidth();
mLayoutHeight = testLayout.getHeight();
- testLayout.addCallback(callback);
+ testLayout.addCallback(mDismissCallback);
+ }
+
+ private void setUpSwipeableAndCallback(
+ ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
+ boolean swipeable
+ ) {
+ scenario.onActivity(activity -> {
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ setDismissCallback(testLayout);
+ testLayout.setSwipeable(swipeable);
+ });
}
private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
index 465ec78..57f0901 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
@@ -31,6 +31,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
+import androidx.core.view.children
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
@@ -49,6 +50,9 @@
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.test.R
import androidx.wear.widget.util.AsyncViewActions.waitForMatchingView
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_CENTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_OUTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_INNER
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.any
import org.hamcrest.Matcher
@@ -223,6 +227,100 @@
)
}
+ // Extension functions to make the margin test more readable.
+ fun WearArcLayout.addSeparator() {
+ addView(
+ WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+ text = " "
+ minSweepDegrees = 10f
+ setBackgroundColor(Color.rgb(100, 100, 100))
+ clockwise = true
+ textSize = 40f
+ }
+ )
+ }
+
+ fun WearArcLayout.addText(
+ text0: String,
+ color: Int,
+ marginLeft: Int = 0,
+ marginTop: Int = 0,
+ marginRight: Int = 0,
+ marginBottom: Int = 0,
+ vAlign: Int = VALIGN_CENTER
+ ) {
+ addView(
+ WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+ text = text0
+ setBackgroundColor(color)
+ clockwise = true
+ textSize = 14f
+ layoutParams = WearArcLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ ).apply {
+ setMargins(marginLeft, marginTop, marginRight, marginBottom)
+ verticalAlignment = vAlign
+ }
+ }
+ )
+ }
+
+ private fun createArcWithMargin() =
+ WearArcLayout(ApplicationProvider.getApplicationContext()).apply {
+ anchorType = WearArcLayout.ANCHOR_CENTER
+ addSeparator()
+ addText("RI", Color.RED, marginTop = 16, vAlign = VALIGN_INNER)
+ addText("GI", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_INNER)
+ addText("BI", Color.BLUE, marginBottom = 16, vAlign = VALIGN_INNER)
+ addSeparator()
+ addText("Red", Color.RED, marginTop = 16)
+ addText("Green", Color.GREEN, marginTop = 8, marginBottom = 8)
+ addText("Blue", Color.BLUE, marginBottom = 16)
+ addSeparator()
+ addText("RO", Color.RED, marginTop = 16, vAlign = VALIGN_OUTER)
+ addText("GO", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_OUTER)
+ addText("BO", Color.BLUE, marginBottom = 16, vAlign = VALIGN_OUTER)
+ addSeparator()
+ addText("L", Color.WHITE, marginRight = 20)
+ addSeparator()
+ addText("C", Color.WHITE, marginRight = 10, marginLeft = 10)
+ addSeparator()
+ addText("R", Color.WHITE, marginLeft = 20)
+ addSeparator()
+ }
+
+ private fun createTwoArcsWithMargin() = listOf(
+ // First arc goes on top
+ createArcWithMargin(),
+
+ // Second arc in the bottom, and we change al children to go counterclockwise.
+ createArcWithMargin().apply {
+ anchorAngleDegrees = 180f
+ children.forEach {
+ (it as? WearCurvedTextView) ?.clockwise = false
+ }
+ }
+ )
+
+ @Test
+ fun testMargins() {
+ doOneTest(
+ "margin_test",
+ createTwoArcsWithMargin()
+ )
+ }
+
+ @Test
+ fun testMarginsCcw() {
+ doOneTest(
+ "margin_ccw_test",
+ createTwoArcsWithMargin().map {
+ it.apply { clockwise = false }
+ }
+ )
+ }
+
// Generates a click in the x,y coordinates in the view's coordinate system.
fun customClick(x: Float, y: Float) = ViewActions.actionWithAssertions(
GeneralClickAction(
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
index 20ff4f1..c09c06a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
@@ -227,12 +227,14 @@
text = "This is a clockwise text for testing background"
clockwise = true
anchorAngleDegrees = 170.0f
+ anchorType = WearArcLayout.ANCHOR_START
setBackgroundColor(Color.rgb(0, 100, 100))
},
WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
text = "Another clockwise text"
clockwise = true
anchorAngleDegrees = 70.0f
+ anchorType = WearArcLayout.ANCHOR_START
setBackgroundColor(Color.rgb(0, 100, 100))
}
)
@@ -249,12 +251,14 @@
text = "This is a counterclockwise text for testing background"
clockwise = false
anchorAngleDegrees = 100.0f
+ anchorType = WearArcLayout.ANCHOR_START
setBackgroundColor(Color.rgb(0, 100, 100))
},
WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
text = "Another counterclockwise text"
clockwise = false
anchorAngleDegrees = 230.0f
+ anchorType = WearArcLayout.ANCHOR_START
setBackgroundColor(Color.rgb(0, 100, 100))
}
)
diff --git a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
index 2a78640..7648800 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
@@ -25,6 +25,8 @@
import androidx.annotation.UiThread;
import androidx.wear.utils.ActivityAnimationUtil;
+import org.jetbrains.annotations.NotNull;
+
/**
* Controller that handles the back button click for dismiss the frame layout
*
@@ -37,22 +39,26 @@
BackButtonDismissController(Context context, DismissibleFrameLayout layout) {
super(context, layout);
- // Dismiss upon back button press
+ // set this to true will also ensure that this view is focusable
layout.setFocusableInTouchMode(true);
+ // Dismiss upon back button press
layout.requestFocus();
layout.setOnKeyListener(
- (view, keyCode, event) -> {
- if (keyCode == KeyEvent.KEYCODE_BACK
- && event.getAction() == KeyEvent.ACTION_UP) {
- dismiss();
- return true;
- }
- return false;
- });
+ (view, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK
+ && event.getAction() == KeyEvent.ACTION_UP
+ && dismiss());
}
- private void dismiss() {
- if (mDismissListener == null) return;
+ void disable(@NotNull DismissibleFrameLayout layout) {
+ setOnDismissListener(null);
+ layout.setOnKeyListener(null);
+ // setting this to false will also ensure that this view is not focusable in touch mode
+ layout.setFocusable(false);
+ layout.clearFocus();
+ }
+
+ private boolean dismiss() {
+ if (mDismissListener == null) return false;
Animation exitAnimation = ActivityAnimationUtil.getStandardActivityAnimation(
mContext, ActivityAnimationUtil.CLOSE_EXIT,
@@ -79,5 +85,6 @@
mDismissListener.onDismissStarted();
mDismissListener.onDismissed();
}
+ return true;
}
}
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 ce76639..448df50 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
@@ -192,7 +192,7 @@
mBackButtonDismissController.setOnDismissListener(mDismissListener);
}
} else if (mBackButtonDismissController != null) {
- mBackButtonDismissController.setOnDismissListener(null);
+ mBackButtonDismissController.disable(this);
mBackButtonDismissController = null;
}
}
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
index 4535a4c..3a5cc6c 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
@@ -92,17 +92,8 @@
/**
* Check whether the widget contains invalid attributes as a child of WearArcLayout
- *
- * @param clockwise the layout direction of the container
*/
- void checkInvalidAttributeAsChild(boolean clockwise);
-
- /**
- * Return whether the widget will handle the layout rotation requested by the container
- * If return true, make sure that the layout rotation is done inside the widget since the
- * container will skip this process.
- */
- boolean handleLayoutRotate(float angle);
+ void checkInvalidAttributeAsChild();
/**
* Return true when the given point is in the clickable area of the child widget.
@@ -120,7 +111,7 @@
*
* <p>Note that the {@code rotate} parameter is ignored when drawing "Fullscreen" elements.
*/
- public static class LayoutParams extends ViewGroup.LayoutParams {
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/** Vertical alignment of elements within the arc. */
/** @hide */
@@ -265,6 +256,8 @@
// Temporary variables using during a draw cycle.
private float mCurrentCumulativeAngle = 0;
private int mAnglesIndex = 0;
+ @SuppressWarnings("SyntheticAccessor")
+ private final ChildArcAngles mChildArcAngles = new ChildArcAngles();
public WearArcLayout(@NonNull Context context) {
this(context, null);
@@ -358,18 +351,22 @@
// ArcLayoutWidget is a special case. Because of how it draws, fit it to the size
// of the whole widget.
+ int childMeasuredHeight;
if (child instanceof ArcLayoutWidget) {
- ArcLayoutWidget widget = (ArcLayoutWidget) child;
- maxChildHeightPx = max(maxChildHeightPx, widget.getThicknessPx());
+ childMeasuredHeight = ((ArcLayoutWidget) child).getThicknessPx();
} else {
measureChild(
child,
getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width),
getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height)
);
- maxChildHeightPx = max(maxChildHeightPx, child.getMeasuredHeight());
+ childMeasuredHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
+
}
+ LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
+ maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight
+ + childLayoutParams.topMargin + childLayoutParams.bottomMargin);
}
mThicknessPx = maxChildHeightPx;
@@ -383,23 +380,14 @@
}
if (child instanceof ArcLayoutWidget) {
- ArcLayoutWidget curvedContainerChild = (ArcLayoutWidget) child;
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
- int childThicknessPx = curvedContainerChild.getThicknessPx();
- int thicknessDiffPx = mThicknessPx - childThicknessPx;
-
- float insetPx = 0;
-
- if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_CENTER) {
- insetPx = thicknessDiffPx / 2f;
- } else if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_INNER) {
- insetPx = thicknessDiffPx;
- }
+ float insetPx = getChildTopInset(child);
int innerChildMeasureSpec =
MeasureSpec.makeMeasureSpec(
maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY);
+
measureChild(
child,
getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.width),
@@ -444,7 +432,7 @@
// vertical position.
int leftPx =
round((getMeasuredWidth() / 2f) - (child.getMeasuredWidth() / 2f));
- int topPx = getChildTopInset(child);
+ int topPx = round(getChildTopInset(child));
child.layout(
leftPx,
@@ -551,60 +539,49 @@
// Rotate the canvas to make the children render in the right place.
canvas.save();
- float arcAngle = calculateArcAngle(child);
- float preRotation = arcAngle / 2f;
+ calculateArcAngle(child, mChildArcAngles);
+ float preRotation = mChildArcAngles.leftMarginAsAngle
+ + mChildArcAngles.actualChildAngle / 2f;
float multiplier = mClockwise ? 1f : -1f;
// Store the center angle of each child to handle touch events.
- float middleAngle = multiplier * (mCurrentCumulativeAngle + arcAngle / 2);
+ float middleAngle = multiplier * (mCurrentCumulativeAngle + preRotation);
mAngles[mAnglesIndex++] = middleAngle;
if (child == mTouchedView) {
// We keep this updated, in case the view has changed angle.
mTouchedViewAngle = middleAngle;
}
+ // Rotate the child widget.
+ canvas.rotate(
+ multiplier * (mCurrentCumulativeAngle + preRotation),
+ getMeasuredWidth() / 2f,
+ getMeasuredHeight() / 2f);
+
if (child instanceof ArcLayoutWidget) {
- ArcLayoutWidget childWidget = (ArcLayoutWidget) child;
- childWidget.checkInvalidAttributeAsChild(mClockwise);
-
- // Special case for ArcLayoutWidget. This doesn't need pre-rotating to get the center
- // of canvas lines up, as it should already know how to draw itself correctly from
- // the "current" rotation. The layout rotation is always passed to the child widget,
- // if the child has not handled this rotation by itself, the parent will have to
- // rotate the canvas to apply this layout.
- if (!childWidget.handleLayoutRotate(multiplier * mCurrentCumulativeAngle)) {
- canvas.rotate(
- multiplier * mCurrentCumulativeAngle,
- getMeasuredWidth() / 2f,
- getMeasuredHeight() / 2f
- );
- }
+ ((ArcLayoutWidget) child).checkInvalidAttributeAsChild();
} else {
- canvas.rotate(
- multiplier * (mCurrentCumulativeAngle + preRotation),
- getMeasuredWidth() / 2f,
- getMeasuredHeight() / 2f);
-
// Do we need to do some counter rotation?
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
- // For counterclockwise layout, especially when mixing standard Android widget with
- // ArcLayoutWidget as children, we might need to rotate the standard widget to make
- // them with the same upwards direction.
float angleToRotate = 0f;
- if (layoutParams.getRotate() && !mClockwise) {
- angleToRotate = 180f;
- }
- // Un-rotate about the top of the canvas, around the center of the actual child.
- // This compounds with the initial rotation into a translation.
- if (!layoutParams.getRotate()) {
+ if (layoutParams.getRotate()) {
+ // For counterclockwise layout, especially when mixing standard Android widget with
+ // ArcLayoutWidget as children, we might need to rotate the standard widget to make
+ // them have the same upwards direction.
+ if (!mClockwise) {
+ angleToRotate = 180f;
+ }
+ } else {
+ // Un-rotate about the top of the canvas, around the center of the actual child.
+ // This compounds with the initial rotation into a translation.
angleToRotate = -multiplier * (mCurrentCumulativeAngle + preRotation);
}
// Do the actual rotation. Note that the strange rotation center is because the child
// view is x-centered but at the top of this container.
- int childInset = getChildTopInset(child);
+ float childInset = getChildTopInset(child);
canvas.rotate(
angleToRotate,
getMeasuredWidth() / 2f,
@@ -612,7 +589,7 @@
);
}
- mCurrentCumulativeAngle += arcAngle;
+ mCurrentCumulativeAngle += mChildArcAngles.getTotalAngle();
boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime);
@@ -630,7 +607,8 @@
float totalArcAngle = 0;
for (int i = 0; i < getChildCount(); i++) {
- totalArcAngle += calculateArcAngle(getChildAt(i));
+ calculateArcAngle(getChildAt(i), mChildArcAngles);
+ totalArcAngle += mChildArcAngles.getTotalAngle();
}
if (mAnchorType == ANCHOR_CENTER) {
@@ -642,31 +620,55 @@
return 0;
}
- private float calculateArcAngle(@NonNull View view) {
+ private static float widthToAngleDegrees(float widthPx, float radiusPx) {
+ return (float) Math.toDegrees(2 * asin(widthPx / radiusPx / 2f));
+ }
+
+ private void calculateArcAngle(@NonNull View view, @NonNull ChildArcAngles childAngles) {
if (view.getVisibility() == GONE) {
- return 0f;
+ childAngles.leftMarginAsAngle = 0;
+ childAngles.rightMarginAsAngle = 0;
+ childAngles.actualChildAngle = 0;
+ return;
}
+ float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
+
+ LayoutParams childLayoutParams = (LayoutParams) view.getLayoutParams();
+
+ childAngles.leftMarginAsAngle =
+ widthToAngleDegrees(childLayoutParams.leftMargin, radiusPx);
+ childAngles.rightMarginAsAngle =
+ widthToAngleDegrees(childLayoutParams.rightMargin, radiusPx);
+
if (view instanceof ArcLayoutWidget) {
- return ((ArcLayoutWidget) view).getSweepAngleDegrees();
+ childAngles.actualChildAngle = ((ArcLayoutWidget) view).getSweepAngleDegrees();
} else {
- float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
- return (float) Math.toDegrees(2 * asin(view.getMeasuredWidth() / radiusPx / 2f));
+ childAngles.actualChildAngle =
+ widthToAngleDegrees(view.getMeasuredWidth(), radiusPx);
}
}
- private int getChildTopInset(@NonNull View child) {
+ private float getChildTopInset(@NonNull View child) {
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
- int thicknessDiffPx = mThicknessPx - child.getMeasuredHeight();
+ int childHeight = child instanceof ArcLayoutWidget
+ ? ((ArcLayoutWidget) child).getThicknessPx()
+ : child.getMeasuredHeight();
+
+ int thicknessDiffPx =
+ mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin
+ - childHeight;
+
+ int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin;
switch (childLayoutParams.getVerticalAlignment()) {
case LayoutParams.VALIGN_OUTER:
- return 0;
+ return margin;
case LayoutParams.VALIGN_CENTER:
- return round(thicknessDiffPx / 2f);
+ return margin + thicknessDiffPx / 2f;
case LayoutParams.VALIGN_INNER:
- return thicknessDiffPx;
+ return margin + thicknessDiffPx;
default:
// Nortmally unreachable...
return 0;
@@ -734,4 +736,14 @@
mClockwise = clockwise;
invalidate();
}
+
+ private static class ChildArcAngles {
+ public float leftMarginAsAngle;
+ public float rightMarginAsAngle;
+ public float actualChildAngle;
+
+ public float getTotalAngle() {
+ return leftMarginAsAngle + rightMarginAsAngle + actualChildAngle;
+ }
+ }
}
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
index 5a6a7fb..b2e659e 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
@@ -77,12 +77,8 @@
private float mPathRadius = 0f;
private float mTextSweepDegrees = 0f;
private float mBackgroundSweepDegrees = MAX_SWEEP_DEGREE;
- private boolean mHasParentArcLayout = false;
- private boolean mParentClockwise = true;
private int mLastUsedTextAlignment = -1;
private float mLocalRotateAngle = 0f;
- private float mParentRotateAngle = 0f;
- private boolean mParentRotateAngleSet = false;
private int mAnchorType = UNSET_ANCHOR_TYPE;
private float mAnchorAngleDegrees = UNSET_ANCHOR_DEGREE;
@@ -215,10 +211,7 @@
* were set for a widget in WearArcLayout
*/
@Override
- public void checkInvalidAttributeAsChild(boolean parentClockwise) {
- this.mHasParentArcLayout = true;
- this.mParentClockwise = parentClockwise;
-
+ public void checkInvalidAttributeAsChild() {
if (mAnchorType != UNSET_ANCHOR_TYPE) {
throw new IllegalArgumentException(
"WearCurvedTextView shall not set anchorType value when added into"
@@ -235,18 +228,6 @@
}
@Override
- public boolean handleLayoutRotate(float angle) {
- mParentRotateAngleSet = true;
-
- // Ensure we are redrawn when the parent rotates.
- if (mParentRotateAngle != angle) {
- doRedraw();
- }
- mParentRotateAngle = angle;
- return true;
- }
-
- @Override
public boolean insideClickArea(float x, float y) {
float radius2 = min(getWidth(), getHeight()) / 2f
- (mClockwise ? getPaddingTop() : getPaddingBottom());
@@ -340,8 +321,6 @@
}
float clockwiseFactor = mClockwise ? 1f : -1f;
- float parentClockwiseFactor =
- mHasParentArcLayout ? (mParentClockwise ? 1f : -1f) : clockwiseFactor;
float alignmentFactor = 0.5f;
switch (getTextAlignment()) {
@@ -357,31 +336,25 @@
alignmentFactor = 0.5f; // TEXT_ALIGNMENT_CENTER
}
- float anchorTypeFactor = 0f;
+ float anchorTypeFactor;
switch (mAnchorType) {
case WearArcLayout.ANCHOR_START:
- anchorTypeFactor = 0f;
- break;
- case WearArcLayout.ANCHOR_CENTER:
anchorTypeFactor = 0.5f;
break;
case WearArcLayout.ANCHOR_END:
- anchorTypeFactor = 1f;
+ anchorTypeFactor = -0.5f;
break;
+ case WearArcLayout.ANCHOR_CENTER: // Center is the default.
default:
- anchorTypeFactor = parentClockwiseFactor == clockwiseFactor ? 0f : -1f;
+ anchorTypeFactor = 0f;
}
- float actualAnchorDegree =
- (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
- + ANCHOR_DEGREE_OFFSET;
+ mLocalRotateAngle = (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
+ + clockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
// Always draw the curved text on top center, then rotate the canvas to the right position
float backgroundStartAngle =
-clockwiseFactor * 0.5f * mBackgroundSweepDegrees + ANCHOR_DEGREE_OFFSET;
- mLocalRotateAngle =
- actualAnchorDegree - backgroundStartAngle
- - parentClockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
float textStartAngle =
backgroundStartAngle + clockwiseFactor * (float) (
@@ -462,20 +435,15 @@
return false;
}
- float x0 = event.getX();
- float y0 = event.getY();
- if (!mParentRotateAngleSet) {
- // If we are a stand-alone widget, we have to handle our rotation / anchor placement,
- // if we are part of an arc container, it's handled by it.
- double rotAngle = -Math.toRadians(mLocalRotateAngle);
+ float x0 = event.getX() - getWidth() / 2;
+ float y0 = event.getY() - getHeight() / 2;
- x0 -= getWidth() / 2;
- y0 -= getHeight() / 2;
- float tempX = (float)
- ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
- y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
- x0 = tempX;
- }
+ double rotAngle = -Math.toRadians(mLocalRotateAngle);
+
+ float tempX = (float)
+ ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
+ y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
+ x0 = tempX;
// Should we start handling the touch events?
if (!mHandlingTouch && insideClickArea(x0, y0)) {
@@ -503,7 +471,7 @@
boolean withBackground = getBackground() != null;
updatePathsIfNeeded(withBackground);
canvas.rotate(
- mLocalRotateAngle + mParentRotateAngle,
+ mLocalRotateAngle,
getWidth() / 2f,
getHeight() / 2f);