Merge "Clarify startActivityAndWait constraints" into androidx-main
diff --git a/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/RequiresPermission.jvm.kt b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/RequiresPermission.jvm.kt
index ca3a49b..9b72c8c 100644
--- a/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/RequiresPermission.jvm.kt
+++ b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/RequiresPermission.jvm.kt
@@ -41,8 +41,8 @@
*
* Example of requiring separate read and write permissions for a content provider:
* ```
- * @RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
- * @RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
+ * @RequiresPermission.Read(RequiresPermission(READ_HISTORY_BOOKMARKS))
+ * @RequiresPermission.Write(RequiresPermission(WRITE_HISTORY_BOOKMARKS))
* public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
* ```
*
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 6ebee26..8ec1c3d 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -94,6 +94,7 @@
public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
ctor public AppSearchBatchResult.Builder();
+ ctor public AppSearchBatchResult.Builder(androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!>);
method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
@@ -146,11 +147,15 @@
}
public static final class AppSearchSchema.Builder {
+ ctor public AppSearchSchema.Builder(androidx.appsearch.app.AppSearchSchema);
ctor public AppSearchSchema.Builder(String);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
method public androidx.appsearch.app.AppSearchSchema build();
+ method public androidx.appsearch.app.AppSearchSchema.Builder clearParentTypes();
+ method public androidx.appsearch.app.AppSearchSchema.Builder clearProperties();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
+ method public androidx.appsearch.app.AppSearchSchema.Builder setSchemaType(String);
}
public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -406,9 +411,13 @@
public static final class GetSchemaResponse.Builder {
ctor public GetSchemaResponse.Builder();
+ ctor public GetSchemaResponse.Builder(androidx.appsearch.app.GetSchemaResponse);
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
method public androidx.appsearch.app.GetSchemaResponse build();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemaTypeVisibilityConfig(String);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemaTypeVisibilityConfigs();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemas();
method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
@@ -443,9 +452,11 @@
}
public static final class JoinSpec.Builder {
+ ctor public JoinSpec.Builder(androidx.appsearch.app.JoinSpec);
ctor public JoinSpec.Builder(String);
method public androidx.appsearch.app.JoinSpec build();
method public androidx.appsearch.app.JoinSpec.Builder setAggregationScoringStrategy(int);
+ method public androidx.appsearch.app.JoinSpec.Builder setChildPropertyExpression(String);
method public androidx.appsearch.app.JoinSpec.Builder setMaxJoinedResultCount(int);
method public androidx.appsearch.app.JoinSpec.Builder setNestedSearch(String, androidx.appsearch.app.SearchSpec);
}
@@ -663,6 +674,7 @@
public static final class SearchSpec.Builder {
ctor public SearchSpec.Builder();
+ ctor public SearchSpec.Builder(androidx.appsearch.app.SearchSpec);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addEmbeddingParameters(androidx.appsearch.app.EmbeddingVector!...);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addEmbeddingParameters(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
@@ -686,6 +698,18 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS) public androidx.appsearch.app.SearchSpec.Builder addSearchStringParameters(java.lang.String!...);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS) public androidx.appsearch.app.SearchSpec.Builder addSearchStringParameters(java.util.List<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec build();
+ method public androidx.appsearch.app.SearchSpec.Builder clearEmbeddingParameters();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterNamespaces();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterPackageNames();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterProperties();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterSchemas();
+ method public androidx.appsearch.app.SearchSpec.Builder clearInformationalRankingExpressions();
+ method public androidx.appsearch.app.SearchSpec.Builder clearJoinSpec();
+ method public androidx.appsearch.app.SearchSpec.Builder clearProjections();
+ method public androidx.appsearch.app.SearchSpec.Builder clearPropertyWeights();
+ method public androidx.appsearch.app.SearchSpec.Builder clearResultGrouping();
+ method public androidx.appsearch.app.SearchSpec.Builder clearSearchSourceLogTag();
+ method public androidx.appsearch.app.SearchSpec.Builder clearSearchStringParameters();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
@@ -771,6 +795,7 @@
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
+ ctor public SetSchemaRequest.Builder(androidx.appsearch.app.SetSchemaRequest);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) throws androidx.appsearch.exceptions.AppSearchException;
@@ -781,9 +806,11 @@
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
method public androidx.appsearch.app.SetSchemaRequest build();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.SetSchemaRequest.Builder clearMigrators();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
+ method public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemas();
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
@@ -847,6 +874,10 @@
package androidx.appsearch.ast {
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public interface FunctionNode extends androidx.appsearch.ast.Node {
+ method public String getFunctionName();
+ }
+
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class NegationNode implements androidx.appsearch.ast.Node {
ctor public NegationNode(androidx.appsearch.ast.Node);
method public androidx.appsearch.ast.Node getChild();
@@ -870,6 +901,28 @@
}
+package androidx.appsearch.ast.operators {
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class AndNode implements androidx.appsearch.ast.Node {
+ ctor public AndNode(androidx.appsearch.ast.Node, androidx.appsearch.ast.Node, androidx.appsearch.ast.Node!...);
+ ctor public AndNode(java.util.List<androidx.appsearch.ast.Node!>);
+ method public void addChild(androidx.appsearch.ast.Node);
+ method public void removeChild(int);
+ method public void setChild(int, androidx.appsearch.ast.Node);
+ method public void setChildren(java.util.List<androidx.appsearch.ast.Node!>);
+ }
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class OrNode implements androidx.appsearch.ast.Node {
+ ctor public OrNode(androidx.appsearch.ast.Node, androidx.appsearch.ast.Node, androidx.appsearch.ast.Node!...);
+ ctor public OrNode(java.util.List<androidx.appsearch.ast.Node!>);
+ method public void addChild(androidx.appsearch.ast.Node);
+ method public void removeChild(int);
+ method public void setChild(int, androidx.appsearch.ast.Node);
+ method public void setChildren(java.util.List<androidx.appsearch.ast.Node!>);
+ }
+
+}
+
package androidx.appsearch.exceptions {
public class AppSearchException extends java.lang.Exception {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 6ebee26..8ec1c3d 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -94,6 +94,7 @@
public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
ctor public AppSearchBatchResult.Builder();
+ ctor public AppSearchBatchResult.Builder(androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!>);
method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
@@ -146,11 +147,15 @@
}
public static final class AppSearchSchema.Builder {
+ ctor public AppSearchSchema.Builder(androidx.appsearch.app.AppSearchSchema);
ctor public AppSearchSchema.Builder(String);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
method public androidx.appsearch.app.AppSearchSchema build();
+ method public androidx.appsearch.app.AppSearchSchema.Builder clearParentTypes();
+ method public androidx.appsearch.app.AppSearchSchema.Builder clearProperties();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
+ method public androidx.appsearch.app.AppSearchSchema.Builder setSchemaType(String);
}
public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -406,9 +411,13 @@
public static final class GetSchemaResponse.Builder {
ctor public GetSchemaResponse.Builder();
+ ctor public GetSchemaResponse.Builder(androidx.appsearch.app.GetSchemaResponse);
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
method public androidx.appsearch.app.GetSchemaResponse build();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemaTypeVisibilityConfig(String);
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemaTypeVisibilityConfigs();
+ method public androidx.appsearch.app.GetSchemaResponse.Builder clearSchemas();
method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
@@ -443,9 +452,11 @@
}
public static final class JoinSpec.Builder {
+ ctor public JoinSpec.Builder(androidx.appsearch.app.JoinSpec);
ctor public JoinSpec.Builder(String);
method public androidx.appsearch.app.JoinSpec build();
method public androidx.appsearch.app.JoinSpec.Builder setAggregationScoringStrategy(int);
+ method public androidx.appsearch.app.JoinSpec.Builder setChildPropertyExpression(String);
method public androidx.appsearch.app.JoinSpec.Builder setMaxJoinedResultCount(int);
method public androidx.appsearch.app.JoinSpec.Builder setNestedSearch(String, androidx.appsearch.app.SearchSpec);
}
@@ -663,6 +674,7 @@
public static final class SearchSpec.Builder {
ctor public SearchSpec.Builder();
+ ctor public SearchSpec.Builder(androidx.appsearch.app.SearchSpec);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addEmbeddingParameters(androidx.appsearch.app.EmbeddingVector!...);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addEmbeddingParameters(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
@@ -686,6 +698,18 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS) public androidx.appsearch.app.SearchSpec.Builder addSearchStringParameters(java.lang.String!...);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SEARCH_STRING_PARAMETERS) public androidx.appsearch.app.SearchSpec.Builder addSearchStringParameters(java.util.List<java.lang.String!>);
method public androidx.appsearch.app.SearchSpec build();
+ method public androidx.appsearch.app.SearchSpec.Builder clearEmbeddingParameters();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterNamespaces();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterPackageNames();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterProperties();
+ method public androidx.appsearch.app.SearchSpec.Builder clearFilterSchemas();
+ method public androidx.appsearch.app.SearchSpec.Builder clearInformationalRankingExpressions();
+ method public androidx.appsearch.app.SearchSpec.Builder clearJoinSpec();
+ method public androidx.appsearch.app.SearchSpec.Builder clearProjections();
+ method public androidx.appsearch.app.SearchSpec.Builder clearPropertyWeights();
+ method public androidx.appsearch.app.SearchSpec.Builder clearResultGrouping();
+ method public androidx.appsearch.app.SearchSpec.Builder clearSearchSourceLogTag();
+ method public androidx.appsearch.app.SearchSpec.Builder clearSearchStringParameters();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
@@ -771,6 +795,7 @@
public static final class SetSchemaRequest.Builder {
ctor public SetSchemaRequest.Builder();
+ ctor public SetSchemaRequest.Builder(androidx.appsearch.app.SetSchemaRequest);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) throws androidx.appsearch.exceptions.AppSearchException;
@@ -781,9 +806,11 @@
method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
method public androidx.appsearch.app.SetSchemaRequest build();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.SetSchemaRequest.Builder clearMigrators();
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
+ method public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemas();
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
@@ -847,6 +874,10 @@
package androidx.appsearch.ast {
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public interface FunctionNode extends androidx.appsearch.ast.Node {
+ method public String getFunctionName();
+ }
+
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class NegationNode implements androidx.appsearch.ast.Node {
ctor public NegationNode(androidx.appsearch.ast.Node);
method public androidx.appsearch.ast.Node getChild();
@@ -870,6 +901,28 @@
}
+package androidx.appsearch.ast.operators {
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class AndNode implements androidx.appsearch.ast.Node {
+ ctor public AndNode(androidx.appsearch.ast.Node, androidx.appsearch.ast.Node, androidx.appsearch.ast.Node!...);
+ ctor public AndNode(java.util.List<androidx.appsearch.ast.Node!>);
+ method public void addChild(androidx.appsearch.ast.Node);
+ method public void removeChild(int);
+ method public void setChild(int, androidx.appsearch.ast.Node);
+ method public void setChildren(java.util.List<androidx.appsearch.ast.Node!>);
+ }
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class OrNode implements androidx.appsearch.ast.Node {
+ ctor public OrNode(androidx.appsearch.ast.Node, androidx.appsearch.ast.Node, androidx.appsearch.ast.Node!...);
+ ctor public OrNode(java.util.List<androidx.appsearch.ast.Node!>);
+ method public void addChild(androidx.appsearch.ast.Node);
+ method public void removeChild(int);
+ method public void setChild(int, androidx.appsearch.ast.Node);
+ method public void setChildren(java.util.List<androidx.appsearch.ast.Node!>);
+ }
+
+}
+
package androidx.appsearch.exceptions {
public class AppSearchException extends java.lang.Exception {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java
index 0f8a50e..0852d6f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java
@@ -20,10 +20,18 @@
import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
+import org.junit.Rule;
import org.junit.Test;
public class AppSearchBatchResultCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testIsSuccess_true() {
AppSearchBatchResult<String, Integer> result =
@@ -116,4 +124,23 @@
AppSearchResult.newFailedResult(
AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testAppSearchBatchResultBuilder_copyConstructor() {
+ AppSearchBatchResult<String, Integer> result =
+ new AppSearchBatchResult.Builder<String, Integer>()
+ .setSuccess("keySuccess1", 1)
+ .setSuccess("keySuccess2", 2)
+ .setFailure(
+ "keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+ .setFailure(
+ "keyFailure2", AppSearchResult.RESULT_INTERNAL_ERROR, "message2")
+ .build();
+ AppSearchBatchResult<String, Integer> resultCopy = new AppSearchBatchResult.Builder<>(
+ result).build();
+ assertThat(resultCopy.getAll()).isEqualTo(result.getAll());
+ assertThat(resultCopy.getSuccesses()).isEqualTo(result.getSuccesses());
+ assertThat(resultCopy.getFailures()).isEqualTo(result.getFailures());
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 0e4952c..7d5e714 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -971,4 +971,56 @@
new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
.setIndexingType(-1).build());
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testAppSearchSchemaBuilder_copyConstructor() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ )
+ .addParentType("Document").build();
+ AppSearchSchema schemaCopy = new AppSearchSchema.Builder(schema).build();
+ assertThat(schemaCopy.getSchemaType()).isEqualTo(schema.getSchemaType());
+ assertThat(schemaCopy.getProperties()).isEqualTo(schema.getProperties());
+ assertThat(schemaCopy.getParentTypes()).isEqualTo(schema.getParentTypes());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testAppSearchSchemaBuilder_setSchemaType() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .setSchemaType("Email2").build();
+ assertThat(schema.getSchemaType()).isEqualTo("Email2");
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testAppSearchSchemaBuilder_clearProperties() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ )
+ .clearProperties().build();
+ assertThat(schema.getSchemaType()).isEqualTo("Email");
+ assertThat(schema.getProperties()).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testAppSearchSchemaBuilder_clearParentTypes() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+ .addParentType("Document").addParentType("Thing").clearParentTypes()
+ .build();
+ assertThat(schema.getSchemaType()).isEqualTo("Email");
+ assertThat(schema.getParentTypes()).isEmpty();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
index 4d5a4f9..c4a7722 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
@@ -25,15 +25,23 @@
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import com.google.common.collect.ImmutableSet;
+import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
import java.util.Map;
public class GetSchemaResponseCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testRebuild() {
byte[] sha256cert1 = new byte[32];
@@ -158,7 +166,6 @@
.READ_ASSISTANT_APP_SEARCH_DATA)));
}
-
@Test
public void setVisibilityConfig() {
SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
@@ -342,4 +349,233 @@
+ " this backend/Android API level combination.");
}
// @exportToFramework:endStrip()
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testGetSchemaResponseBuilder_copyConstructor() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+ SchemaVisibilityConfig schemaVisibilityConfig =
+ new SchemaVisibilityConfig.Builder().build();
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ GetSchemaResponse response =
+ new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1).addSchema(schema2)
+ .addSchemaTypeNotDisplayedBySystem("Email1")
+ .addSchemaTypeNotDisplayedBySystem("Email2")
+ .setSchemaTypeVisibleToPackages("Email1",
+ ImmutableSet.of(packageIdentifier1))
+ .setSchemaTypeVisibleToPackages("Email2",
+ ImmutableSet.of(packageIdentifier2))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ )
+ .setRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)))
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ .setPubliclyVisibleSchema("Email2", packageIdentifier2)
+ .setSchemaTypeVisibleToConfigs("Email1",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .setSchemaTypeVisibleToConfigs("Email2",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .build();
+ GetSchemaResponse responseCopy = new GetSchemaResponse.Builder(response).build();
+ assertThat(responseCopy.getVersion()).isEqualTo(response.getVersion());
+ assertThat(responseCopy.getSchemas()).isEqualTo(response.getSchemas());
+ assertThat(responseCopy.getPubliclyVisibleSchemas()).isEqualTo(
+ response.getPubliclyVisibleSchemas());
+ assertThat(responseCopy.getRequiredPermissionsForSchemaTypeVisibility()).isEqualTo(
+ response.getRequiredPermissionsForSchemaTypeVisibility());
+ assertThat(responseCopy.getSchemaTypesVisibleToConfigs()).isEqualTo(
+ response.getSchemaTypesVisibleToConfigs());
+ assertThat(responseCopy.getSchemaTypesVisibleToPackages()).isEqualTo(
+ response.getSchemaTypesVisibleToPackages());
+ assertThat(responseCopy.getSchemaTypesNotDisplayedBySystem()).isEqualTo(
+ response.getSchemaTypesNotDisplayedBySystem());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testGetSchemaResponseBuilder_clearSchemas() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ GetSchemaResponse response = new GetSchemaResponse.Builder()
+ .addSchema(schema1)
+ .addSchema(schema2)
+ .clearSchemas()
+ .build();
+ assertThat(response.getSchemas()).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testGetSchemaResponseBuilder_clearSchemaTypeVisibilityConfig() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+ SchemaVisibilityConfig schemaVisibilityConfig =
+ new SchemaVisibilityConfig.Builder().build();
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ GetSchemaResponse response =
+ new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1).addSchema(schema2)
+ .addSchemaTypeNotDisplayedBySystem("Email1")
+ .addSchemaTypeNotDisplayedBySystem("Email2")
+ .setSchemaTypeVisibleToPackages("Email1",
+ ImmutableSet.of(packageIdentifier1))
+ .setSchemaTypeVisibleToPackages("Email2",
+ ImmutableSet.of(packageIdentifier2))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ )
+ .setRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)))
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ .setPubliclyVisibleSchema("Email2", packageIdentifier2)
+ .setSchemaTypeVisibleToConfigs("Email1",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .setSchemaTypeVisibleToConfigs("Email2",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .clearSchemaTypeVisibilityConfig("Email1")
+ .build();
+ assertThat(response.getSchemaTypesNotDisplayedBySystem()).containsExactly("Email2");
+ assertThat(response.getSchemaTypesVisibleToPackages()).containsExactly("Email2",
+ ImmutableSet.of(packageIdentifier2));
+ assertThat(response.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+ "Email2", ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)));
+ assertThat(response.getPubliclyVisibleSchemas()).containsExactly("Email2",
+ packageIdentifier2);
+ assertThat(response.getSchemaTypesVisibleToConfigs()).containsExactly("Email2",
+ ImmutableSet.of(schemaVisibilityConfig));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testGetSchemaResponseBuilder_clearSchemaTypeVisibilityConfigs() {
+ byte[] sha256cert1 = new byte[32];
+ byte[] sha256cert2 = new byte[32];
+ Arrays.fill(sha256cert1, (byte) 1);
+ Arrays.fill(sha256cert2, (byte) 2);
+ PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+ PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+ SchemaVisibilityConfig schemaVisibilityConfig =
+ new SchemaVisibilityConfig.Builder().build();
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+ GetSchemaResponse response =
+ new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1).addSchema(schema2)
+ .addSchemaTypeNotDisplayedBySystem("Email1")
+ .addSchemaTypeNotDisplayedBySystem("Email2")
+ .setSchemaTypeVisibleToPackages("Email1",
+ ImmutableSet.of(packageIdentifier1))
+ .setSchemaTypeVisibleToPackages("Email2",
+ ImmutableSet.of(packageIdentifier2))
+ .setRequiredPermissionsForSchemaTypeVisibility("Email1",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_CALENDAR),
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+ )
+ .setRequiredPermissionsForSchemaTypeVisibility("Email2",
+ ImmutableSet.of(
+ ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+ SetSchemaRequest.READ_EXTERNAL_STORAGE),
+ ImmutableSet.of(SetSchemaRequest
+ .READ_ASSISTANT_APP_SEARCH_DATA)))
+ .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+ .setPubliclyVisibleSchema("Email2", packageIdentifier2)
+ .setSchemaTypeVisibleToConfigs("Email1",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .setSchemaTypeVisibleToConfigs("Email2",
+ ImmutableSet.of(schemaVisibilityConfig))
+ .clearSchemaTypeVisibilityConfig("Email1")
+ .clearSchemaTypeVisibilityConfigs()
+ .build();
+ assertThat(response.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+ assertThat(response.getSchemaTypesVisibleToPackages()).isEmpty();
+ assertThat(response.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
+ assertThat(response.getPubliclyVisibleSchemas()).isEmpty();
+ assertThat(response.getSchemaTypesVisibleToConfigs()).isEmpty();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
index a58b892..a307a22 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/JoinSpecCtsTest.java
@@ -20,10 +20,17 @@
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
+import org.junit.Rule;
import org.junit.Test;
public class JoinSpecCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Test
public void testBuild() {
@@ -64,4 +71,35 @@
assertThat(joinSpec.getNestedSearchSpec().getFilterNamespaces())
.isEqualTo(empty.getFilterNamespaces());
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testJoinSpecBuilder_copyConstructor() {
+ SearchSpec originalNestedSearchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("Action", "CallAction")
+ .build();
+ JoinSpec joinSpec = new JoinSpec.Builder("entityId")
+ .setMaxJoinedResultCount(5)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .setNestedSearch("joe", originalNestedSearchSpec)
+ .build();
+ JoinSpec joinSpecCopy = new JoinSpec.Builder(joinSpec).build();
+ assertThat(joinSpecCopy.getNestedQuery()).isEqualTo(joinSpec.getNestedQuery());
+ assertThat(joinSpecCopy.getNestedSearchSpec()).isEqualTo(joinSpec.getNestedSearchSpec());
+ assertThat(joinSpecCopy.getChildPropertyExpression()).isEqualTo(
+ joinSpec.getChildPropertyExpression());
+ assertThat(joinSpecCopy.getMaxJoinedResultCount()).isEqualTo(
+ joinSpec.getMaxJoinedResultCount());
+ assertThat(joinSpecCopy.getAggregationScoringStrategy()).isEqualTo(
+ joinSpec.getAggregationScoringStrategy());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testJoinSpecBuilder_setChildPropertyExpression() {
+ JoinSpec joinSpec = new JoinSpec.Builder("entityId")
+ .setChildPropertyExpression("entityId2")
+ .build();
+ assertThat(joinSpec.getChildPropertyExpression()).isEqualTo("entityId2");
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index 8dd3688..7a899c4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -828,4 +828,194 @@
assertThat(rebuild.getSearchStringParameters())
.containsExactly("A", "b", "C", "d").inOrder();
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testSearchSpecBuilder_copyConstructor() {
+ List<String> expectedPropertyPaths1 = ImmutableList.of("path1", "path2");
+ List<String> expectedPropertyPaths2 = ImmutableList.of("path3", "path4");
+ Map<String, Double> expectedPropertyWeights = ImmutableMap.of("property1", 1.0,
+ "property2", 2.0);
+ Map<PropertyPath, Double> expectedPropertyWeightPaths =
+ ImmutableMap.of(new PropertyPath("property1.nested"), 1.0);
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .addFilterNamespaces("namespace1", "namespace2")
+ .addFilterNamespaces(ImmutableList.of("namespace3"))
+ .addFilterSchemas("schemaTypes1", "schemaTypes2")
+ .addFilterSchemas(ImmutableList.of("schemaTypes3"))
+ .addFilterPackageNames("package1", "package2")
+ .addFilterPackageNames(ImmutableList.of("package3"))
+ .setSnippetCount(5)
+ .setSnippetCountPerProperty(10)
+ .setMaxSnippetSize(15)
+ .setResultCountPerPage(42)
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 37)
+ .addProjection("schemaTypes1", expectedPropertyPaths1)
+ .addProjection("schemaTypes2", expectedPropertyPaths2)
+ .setPropertyWeights("schemaTypes1", expectedPropertyWeights)
+ .setPropertyWeightPaths("schemaTypes2", expectedPropertyWeightPaths)
+ .setNumericSearchEnabled(true)
+ .setVerbatimSearchEnabled(true)
+ .setListFilterQueryLanguageEnabled(true)
+ .build();
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getTermMatch()).isEqualTo(searchSpec.getTermMatch());
+ assertThat(searchSpecCopy.getFilterNamespaces()).isEqualTo(
+ searchSpec.getFilterNamespaces());
+ assertThat(searchSpecCopy.getFilterSchemas()).isEqualTo(searchSpec.getFilterSchemas());
+ assertThat(searchSpecCopy.getFilterPackageNames()).isEqualTo(
+ searchSpec.getFilterPackageNames());
+ assertThat(searchSpecCopy.getSnippetCount()).isEqualTo(searchSpec.getSnippetCount());
+ assertThat(searchSpecCopy.getSnippetCountPerProperty()).isEqualTo(
+ searchSpec.getSnippetCountPerProperty());
+ assertThat(searchSpecCopy.getMaxSnippetSize()).isEqualTo(searchSpec.getMaxSnippetSize());
+ assertThat(searchSpecCopy.getResultCountPerPage()).isEqualTo(
+ searchSpec.getResultCountPerPage());
+ assertThat(searchSpecCopy.getOrder()).isEqualTo(searchSpec.getOrder());
+ assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+ assertThat(searchSpecCopy.getResultGroupingTypeFlags()).isEqualTo(
+ searchSpec.getResultGroupingTypeFlags());
+ assertThat(searchSpecCopy.getProjections()).isEqualTo(searchSpec.getProjections());
+ assertThat(searchSpecCopy.getResultGroupingLimit()).isEqualTo(
+ searchSpec.getResultGroupingLimit());
+ assertThat(searchSpecCopy.getPropertyWeights()).isEqualTo(searchSpec.getPropertyWeights());
+ assertThat(searchSpecCopy.getPropertyWeightPaths()).isEqualTo(
+ searchSpec.getPropertyWeightPaths());
+ assertThat(searchSpecCopy.isNumericSearchEnabled()).isEqualTo(
+ searchSpec.isNumericSearchEnabled());
+ assertThat(searchSpecCopy.isVerbatimSearchEnabled()).isEqualTo(
+ searchSpec.isVerbatimSearchEnabled());
+ assertThat(searchSpecCopy.isListFilterQueryLanguageEnabled()).isEqualTo(
+ searchSpec.isListFilterQueryLanguageEnabled());
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES})
+ public void testSearchSpecBuilder_clearBuilderParameters() {
+ JoinSpec joinSpec = new JoinSpec.Builder("query").build();
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterNamespaces("namespace1", "namespace2")
+ .addFilterSchemas("schemaTypes1", "schemaTypes2")
+ .addFilterPackageNames("package1", "package2")
+ .addFilterProperties("schemaTypes1", ImmutableList.of("path1", "path2"))
+ .addFilterProperties("schemaTypes2", ImmutableList.of("path3", "path4"))
+ .addProjection("schemaTypes1", ImmutableList.of("path1", "path2"))
+ .addProjection("schemaTypes2", ImmutableList.of("path3", "path4"))
+ .setPropertyWeights("schemaTypes1", ImmutableMap.of("property1", 1.0))
+ .setPropertyWeights("schemaTypes2", ImmutableMap.of("property2", 2.0))
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 37)
+ .setJoinSpec(joinSpec)
+ .clearFilterNamespaces()
+ .clearFilterPackageNames()
+ .clearFilterProperties()
+ .clearFilterSchemas()
+ .clearJoinSpec()
+ .clearProjections()
+ .clearPropertyWeights()
+ .clearResultGrouping()
+ .build();
+ assertThat(searchSpec.getFilterNamespaces()).isEmpty();
+ assertThat(searchSpec.getFilterPackageNames()).isEmpty();
+ assertThat(searchSpec.getFilterProperties()).isEmpty();
+ assertThat(searchSpec.getFilterSchemas()).isEmpty();
+ assertThat(searchSpec.getProjections()).isEmpty();
+ assertThat(searchSpec.getPropertyWeights()).isEmpty();
+ assertThat(searchSpec.getResultGroupingLimit()).isEqualTo(0);
+ assertThat(searchSpec.getResultGroupingTypeFlags()).isEqualTo(0);
+ assertThat(searchSpec.getJoinSpec()).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
+ public void testSearchSpecBuilder_copyConstructor_embeddingParameters() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+ SearchSpec searchSpec = new SearchSpec.Builder().addEmbeddingParameters(embedding1,
+ embedding2).build();
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getEmbeddingParameters()).isEqualTo(
+ searchSpec.getEmbeddingParameters());
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
+ public void testSearchSpecBuilder_copyConstructor_clearEmbeddingParameters() {
+ EmbeddingVector embedding1 = new EmbeddingVector(
+ new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+ EmbeddingVector embedding2 = new EmbeddingVector(
+ new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+ SearchSpec searchSpec = new SearchSpec.Builder().addEmbeddingParameters(embedding1,
+ embedding2).clearEmbeddingParameters().build();
+ assertThat(searchSpec.getEmbeddingParameters()).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS})
+ public void testSearchSpecBuilder_copyConstructor_informationalRankingExpressions() {
+ SearchSpec searchSpec = new SearchSpec.Builder().addInformationalRankingExpressions("info1",
+ "info2").build();
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getInformationalRankingExpressions()).isEqualTo(
+ searchSpec.getInformationalRankingExpressions());
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS})
+ public void testSearchSpecBuilder_clearInformationalRankingExpressions() {
+ SearchSpec searchSpec = new SearchSpec.Builder().addInformationalRankingExpressions("info1",
+ "info2").clearInformationalRankingExpressions().build();
+ assertThat(searchSpec.getInformationalRankingExpressions()).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG})
+ public void testSearchSpecBuilder_copyConstructor_searchSourceLogTag() {
+ SearchSpec searchSpec = new SearchSpec.Builder().setSearchSourceLogTag("source").build();
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getSearchSourceLogTag()).isEqualTo(
+ searchSpec.getSearchSourceLogTag());
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG})
+ public void testSearchSpecBuilder_clearSearchSourceLogTag() {
+ SearchSpec searchSpec = new SearchSpec.Builder().setSearchSourceLogTag(
+ "source").clearSearchSourceLogTag().build();
+ assertThat(searchSpec.getSearchSourceLogTag()).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS})
+ public void testSearchSpecBuilder_copyConstructor_searchStringParameters() {
+ SearchSpec searchSpec = new SearchSpec.Builder().addSearchStringParameters("param1",
+ "param2").build();
+ SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+ assertThat(searchSpecCopy.getSearchStringParameters()).isEqualTo(
+ searchSpec.getSearchStringParameters());
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS,
+ Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS})
+ public void testSearchSpecBuilder_clearSearchStringParameters() {
+ SearchSpec searchSpec = new SearchSpec.Builder().addSearchStringParameters("param1",
+ "param2").clearSearchStringParameters().build();
+ assertThat(searchSpec.getSearchStringParameters()).isEmpty();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
index 1876b87..315b037 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -33,11 +33,16 @@
import androidx.appsearch.app.SchemaVisibilityConfig;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import androidx.appsearch.testutil.AppSearchEmail;
import androidx.collection.ArrayMap;
import com.google.common.collect.ImmutableSet;
+import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
@@ -49,6 +54,9 @@
import java.util.Set;
public class SetSchemaRequestCtsTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testBuildSetSchemaRequest() {
AppSearchSchema.StringPropertyConfig prop1 =
@@ -105,66 +113,9 @@
AppSearchSchema schema3 =
new AppSearchSchema.Builder("type3").addProperty(requiredProp).build();
- Migrator expectedMigrator1 = new Migrator() {
- @Override
- public boolean shouldMigrate(int currentVersion, int finalVersion) {
- return true;
- }
-
- @NonNull
- @Override
- public GenericDocument onUpgrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
-
- @NonNull
- @Override
- public GenericDocument onDowngrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
- };
- Migrator expectedMigrator2 = new Migrator() {
- @Override
- public boolean shouldMigrate(int currentVersion, int finalVersion) {
- return true;
- }
-
- @NonNull
- @Override
- public GenericDocument onUpgrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
-
- @NonNull
- @Override
- public GenericDocument onDowngrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
- };
- Migrator expectedMigrator3 = new Migrator() {
- @Override
- public boolean shouldMigrate(int currentVersion, int finalVersion) {
- return true;
- }
-
- @NonNull
- @Override
- public GenericDocument onUpgrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
-
- @NonNull
- @Override
- public GenericDocument onDowngrade(int currentVersion, int finalVersion,
- @NonNull GenericDocument document) {
- return document;
- }
- };
+ Migrator expectedMigrator1 = new NoOpMigrator();
+ Migrator expectedMigrator2 = new NoOpMigrator();
+ Migrator expectedMigrator3 = new NoOpMigrator();
Map<String, Migrator> migratorMap = new ArrayMap<>();
migratorMap.put("type1", expectedMigrator1);
migratorMap.put("type2", expectedMigrator2);
@@ -1144,6 +1095,164 @@
.isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testSetSchemaRequestBuilder_copyConstructor() {
+ AppSearchSchema.StringPropertyConfig prop1 =
+ new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build();
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("type1").addProperty(prop1).build();
+ AppSearchSchema schema2 =
+ new AppSearchSchema.Builder("type2").addProperty(prop1).build();
+ AppSearchSchema schema3 =
+ new AppSearchSchema.Builder("type3").addProperty(prop1).build();
+ AppSearchSchema schema4 =
+ new AppSearchSchema.Builder("type4").addProperty(prop1).build();
+
+ PackageIdentifier packageIdentifier =
+ new PackageIdentifier("com.package.foo", new byte[]{100});
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1, schema2)
+ .addSchemas(Arrays.asList(schema3, schema4))
+ .setSchemaTypeDisplayedBySystem("type2", /*displayed=*/ false)
+ .setSchemaTypeVisibilityForPackage("type1", /*visible=*/ true,
+ packageIdentifier)
+ .addRequiredPermissionsForSchemaTypeVisibility("type3",
+ Collections.singleton(SetSchemaRequest.READ_CONTACTS))
+ .setPubliclyVisibleSchema("type4", packageIdentifier)
+ .addSchemaTypeVisibleToConfig("type1", new SchemaVisibilityConfig.Builder().build())
+ .setMigrator("type2", new NoOpMigrator())
+ .setForceOverride(true)
+ .setVersion(142857)
+ .build();
+
+ SetSchemaRequest requestCopy = new SetSchemaRequest.Builder(request).build();
+ assertThat(requestCopy.getSchemas()).isEqualTo(request.getSchemas());
+ assertThat(requestCopy.getSchemasNotDisplayedBySystem()).isEqualTo(
+ request.getSchemasNotDisplayedBySystem());
+ assertThat(requestCopy.getSchemasVisibleToPackages()).isEqualTo(
+ request.getSchemasVisibleToPackages());
+ assertThat(requestCopy.getRequiredPermissionsForSchemaTypeVisibility()).isEqualTo(
+ request.getRequiredPermissionsForSchemaTypeVisibility());
+ assertThat(requestCopy.getPubliclyVisibleSchemas()).isEqualTo(
+ request.getPubliclyVisibleSchemas());
+ assertThat(requestCopy.getSchemasVisibleToConfigs()).isEqualTo(
+ request.getSchemasVisibleToConfigs());
+ assertThat(requestCopy.getMigrators()).isEqualTo(request.getMigrators());
+ assertThat(requestCopy.getVersion()).isEqualTo(request.getVersion());
+ assertThat(requestCopy.isForceOverride()).isEqualTo(request.isForceOverride());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testSetSchemaRequestBuilder_copyConstructor_usesDeepCopies() {
+ // Previously, the copy constructor did not make deep copies of all fields, so modifying the
+ // builder could affect the request that the builder was created from
+ AppSearchSchema.StringPropertyConfig prop1 =
+ new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build();
+ AppSearchSchema schema1 =
+ new AppSearchSchema.Builder("type1").addProperty(prop1).build();
+ AppSearchSchema schema2 =
+ new AppSearchSchema.Builder("type2").addProperty(prop1).build();
+ AppSearchSchema schema3 =
+ new AppSearchSchema.Builder("type3").addProperty(prop1).build();
+ AppSearchSchema schema4 =
+ new AppSearchSchema.Builder("type4").addProperty(prop1).build();
+
+ PackageIdentifier packageIdentifier =
+ new PackageIdentifier("com.package.foo", new byte[]{100});
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1, schema2, schema3, schema4)
+ .setSchemaTypeVisibilityForPackage("type1", /*visible=*/ true,
+ packageIdentifier)
+ .addRequiredPermissionsForSchemaTypeVisibility("type3",
+ Collections.singleton(SetSchemaRequest.READ_CONTACTS))
+ .addSchemaTypeVisibleToConfig("type1", new SchemaVisibilityConfig.Builder().build())
+ .build();
+
+ PackageIdentifier otherPackageIdentifier =
+ new PackageIdentifier("com.package.bar", new byte[]{100});
+
+ // Create a copy builder and modify the visibility settings
+ SetSchemaRequest.Builder unused = new SetSchemaRequest.Builder(request)
+ .setSchemaTypeVisibilityForPackage("type1", /*visible=*/ true,
+ otherPackageIdentifier)
+ .addRequiredPermissionsForSchemaTypeVisibility("type3", Collections.singleton(
+ SetSchemaRequest.READ_SMS))
+ .addSchemaTypeVisibleToConfig("type1",
+ new SchemaVisibilityConfig.Builder().addAllowedPackage(
+ otherPackageIdentifier).build());
+
+ // Validate that changing the copy builder did not affect the original request
+ assertThat(request.getSchemasVisibleToPackages()).containsExactly("type1",
+ Collections.singleton(packageIdentifier));
+ assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly("type3",
+ Collections.singleton(Collections.singleton(SetSchemaRequest.READ_CONTACTS)));
+ assertThat(request.getSchemasVisibleToConfigs()).containsExactly("type1",
+ Collections.singleton(new SchemaVisibilityConfig.Builder().build()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testSetSchemaRequestBuilder_clearSchemas() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("type1").build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("type2").build();
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1, schema2)
+ .clearSchemas()
+ .build();
+ assertThat(request.getSchemas()).isEmpty();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public void testSetSchemaRequestBuilder_clearMigrators() {
+ AppSearchSchema schema1 = new AppSearchSchema.Builder("type1").build();
+ AppSearchSchema schema2 = new AppSearchSchema.Builder("type2").build();
+ Migrator migrator = new NoOpMigrator();
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(schema1, schema2)
+ .setMigrator("type1", migrator)
+ .setMigrator("type2", migrator)
+ .clearMigrators()
+ .build();
+ assertThat(request.getMigrators()).isEmpty();
+ }
+
+ /** Migrator that does nothing. */
+ private static class NoOpMigrator extends Migrator {
+ @Override
+ public boolean shouldMigrate(int currentVersion, int finalVersion) {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+ @NonNull GenericDocument document) {
+ return document;
+ }
+
+ @NonNull
+ @Override
+ public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+ @NonNull GenericDocument document) {
+ return document;
+ }
+ }
+
// @exportToFramework:startStrip()
@Document
static class Outer {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/AndNodeCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/AndNodeCtsTest.java
new file mode 100644
index 0000000..17cae7e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/AndNodeCtsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 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.cts.ast.operators;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.ast.Node;
+import androidx.appsearch.ast.TextNode;
+import androidx.appsearch.ast.operators.AndNode;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AndNodeCtsTest {
+
+ @Test
+ public void testConstructor_buildsAndNode() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+
+ List<Node> childNodes = List.of(foo, bar, baz);
+
+ AndNode andNode = new AndNode(childNodes);
+ assertThat(andNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testConstructor_buildsAndNodeVarArgs() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+
+ AndNode andNode = new AndNode(foo, bar, baz);
+ assertThat(andNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNullListPassed() {
+ assertThrows(NullPointerException.class, () -> new AndNode(null));
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNotEnoughNodes() {
+ TextNode foo = new TextNode("foo");
+ assertThrows(IllegalArgumentException.class,
+ () -> new AndNode(Collections.singletonList(foo)));
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNullArgPassed() {
+ TextNode foo = new TextNode("foo");
+ TextNode baz = new TextNode("baz");
+ assertThrows(NullPointerException.class, () -> new AndNode(foo, null, baz));
+ }
+
+ @Test
+ public void testSetChildren_throwsWhenNullListPassed() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ assertThrows(NullPointerException.class, () -> andNode.setChildren(null));
+ }
+
+ @Test
+ public void testSetChildren_throwsWhenNotEnoughNodes() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> andNode.setChildren(Collections.singletonList(foo)));
+ }
+
+ @Test
+ public void testAddChild_addsToBackOfList() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ andNode.addChild(baz);
+
+ assertThat(andNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testSetChild_throwsOnOutOfRangeIndex() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ assertThrows(IllegalArgumentException.class, () -> andNode.setChild(3, baz));
+ }
+
+ @Test
+ public void testSetChild_throwsOnNullNode() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ assertThrows(NullPointerException.class, () -> andNode.setChild(0, null));
+ }
+
+ @Test
+ public void testSetChild_setCorrectChild() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ andNode.setChild(0, baz);
+
+ assertThat(andNode.getChildren()).containsExactly(baz, bar).inOrder();
+ }
+
+ @Test
+ public void testGetChildren_returnsViewOf() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ List<Node> copyChildNodes = andNode.getChildren();
+
+ // Check that initially copy is equivalent to original list
+ assertThat(copyChildNodes).containsExactly(foo, bar).inOrder();
+ // Now make changes to the original list
+ TextNode baz = new TextNode("baz");
+ andNode.setChild(1, baz);
+
+ TextNode bat = new TextNode("bat");
+ andNode.addChild(bat);
+ // Check that the copied list is the same as the original list.
+ assertThat(copyChildNodes).containsExactly(foo, baz, bat).inOrder();
+ }
+
+ @Test
+ public void testRemoveChild_throwsIfListIsTooSmall() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ AndNode andNode = new AndNode(foo, bar);
+
+ assertThrows(IllegalStateException.class, () -> andNode.removeChild(0));
+ }
+
+ @Test
+ public void testRemoveChild_throwsIfIndexOutOfRange() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+ AndNode andNode = new AndNode(foo, bar, baz);
+
+ assertThrows(IllegalArgumentException.class, () -> andNode.removeChild(-1));
+ assertThrows(IllegalArgumentException.class, () -> andNode.removeChild(3));
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/OrNodeCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/OrNodeCtsTest.java
new file mode 100644
index 0000000..b954333
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/operators/OrNodeCtsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 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.cts.ast.operators;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.ast.Node;
+import androidx.appsearch.ast.TextNode;
+import androidx.appsearch.ast.operators.OrNode;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class OrNodeCtsTest {
+
+ @Test
+ public void testConstructor_buildsOrNode() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+
+ List<Node> childNodes = List.of(foo, bar , baz);
+
+ OrNode orNode = new OrNode(childNodes);
+ assertThat(orNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testConstructor_buildsOrNodeVarArgs() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+
+ OrNode orNode = new OrNode(foo, bar, baz);
+ assertThat(orNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNullListPassed() {
+ assertThrows(NullPointerException.class, () -> new OrNode(null));
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNotEnoughNodes() {
+ TextNode foo = new TextNode("foo");
+ assertThrows(IllegalArgumentException.class,
+ () -> new OrNode(Collections.singletonList(foo)));
+ }
+
+ @Test
+ public void testConstructor_throwsWhenNullArgPassed() {
+ TextNode foo = new TextNode("foo");
+ TextNode baz = new TextNode("baz");
+ assertThrows(NullPointerException.class,
+ () -> new OrNode(foo, null, baz));
+ }
+
+ @Test
+ public void testSetChildren_throwsWhenNullListPassed() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ assertThrows(NullPointerException.class, () -> orNode.setChildren(null));
+ }
+
+ @Test
+ public void testSetChildren_throwsWhenNotEnoughNodes() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> orNode.setChildren(Collections.singletonList(new TextNode("baz"))));
+ }
+
+ @Test
+ public void testAddChild_addsToBackOfList() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ orNode.addChild(baz);
+
+ assertThat(orNode.getChildren()).containsExactly(foo, bar, baz).inOrder();
+ }
+
+ @Test
+ public void testSetChild_throwsOnOutOfRangeIndex() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ assertThrows(IllegalArgumentException.class, () -> orNode.setChild(3, baz));
+ }
+
+ @Test
+ public void testSetChild_throwsOnNullNode() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ assertThrows(NullPointerException.class, () -> orNode.setChild(0, null));
+ }
+
+ @Test
+ public void testSetChild_setCorrectChild() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ TextNode baz = new TextNode("baz");
+ orNode.setChild(0, baz);
+ assertThat(orNode.getChildren()).containsExactly(baz, bar).inOrder();
+ }
+
+ @Test
+ public void testGetChildren_returnsCopy() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ List<Node> copyChildNodes = orNode.getChildren();
+
+ // Check that initially copy is equivalent to original list
+ assertThat(copyChildNodes).containsExactly(foo, bar).inOrder();
+ // Now make changes to the original list
+ TextNode baz = new TextNode("baz");
+ orNode.setChild(1, baz);
+
+ TextNode bat = new TextNode("bat");
+ orNode.addChild(bat);
+ // Check that the copied list is the same as the original list.
+ assertThat(copyChildNodes).containsExactly(foo, baz, bat).inOrder();
+ }
+
+ @Test
+ public void testRemoveChild_throwsIfListIsTooSmall() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ OrNode orNode = new OrNode(foo, bar);
+
+ assertThrows(IllegalStateException.class, () -> orNode.removeChild(0));
+ }
+
+ @Test
+ public void testRemoveChild_throwsIfIndexOutOfRange() {
+ TextNode foo = new TextNode("foo");
+ TextNode bar = new TextNode("bar");
+ TextNode baz = new TextNode("baz");
+ OrNode orNode = new OrNode(foo, bar, baz);
+
+ assertThrows(IllegalArgumentException.class, () -> orNode.removeChild(-1));
+ assertThrows(IllegalArgumentException.class, () -> orNode.removeChild(3));
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
index 0024d8d..e958bae 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -146,4 +146,11 @@
.isEqualTo("com.android.appsearch.flags"
+ ".enable_abstract_syntax_trees");
}
+
+ @Test
+ public void testFlagValue_enableAdditionalBuilderCopyConstructors() {
+ assertThat(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ .isEqualTo(
+ "com.android.appsearch.flags.enable_additional_builder_copy_constructors");
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
index 08a16d2..fb9a51f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -19,6 +19,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;
@@ -131,6 +133,18 @@
private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
private boolean mBuilt = false;
+ /** Creates a new {@link Builder}. */
+ public Builder() {
+ }
+
+ /** Creates a new {@link Builder} from the given {@link AppSearchBatchResult}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public Builder(@NonNull AppSearchBatchResult<KeyType, ValueType> appSearchBatchResult) {
+ mSuccesses.putAll(appSearchBatchResult.mSuccesses);
+ mFailures.putAll(appSearchBatchResult.mFailures);
+ mAll.putAll(appSearchBatchResult.mAll);
+ }
+
/**
* Associates the {@code key} with the provided successful return value.
*
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 b300d3e..c0104a0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -51,7 +51,6 @@
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Objects;
import java.util.Set;
/**
@@ -89,10 +88,10 @@
@Param(id = 2) @NonNull List<PropertyConfigParcel> propertyConfigParcels,
@Param(id = 3) @NonNull List<String> parentTypes,
@Param(id = 4) @NonNull String description) {
- mSchemaType = Objects.requireNonNull(schemaType);
- mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);
- mParentTypes = Objects.requireNonNull(parentTypes);
- mDescription = Objects.requireNonNull(description);
+ mSchemaType = Preconditions.checkNotNull(schemaType);
+ mPropertyConfigParcels = Preconditions.checkNotNull(propertyConfigParcels);
+ mParentTypes = Preconditions.checkNotNull(parentTypes);
+ mDescription = Preconditions.checkNotNull(description);
}
@Override
@@ -225,7 +224,7 @@
/** Builder for {@link AppSearchSchema objects}. */
public static final class Builder {
- private final String mSchemaType;
+ private String mSchemaType;
private String mDescription = "";
private ArrayList<PropertyConfigParcel> mPropertyConfigParcels = new ArrayList<>();
private LinkedHashSet<String> mParentTypes = new LinkedHashSet<>();
@@ -238,6 +237,31 @@
}
/**
+ * Creates a new {@link AppSearchSchema.Builder} from the given {@link AppSearchSchema}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public Builder(@NonNull AppSearchSchema schema) {
+ mSchemaType = schema.getSchemaType();
+ mDescription = schema.getDescription();
+ mPropertyConfigParcels.addAll(schema.mPropertyConfigParcels);
+ mParentTypes.addAll(schema.mParentTypes);
+ for (int i = 0; i < mPropertyConfigParcels.size(); i++) {
+ mPropertyNames.add(mPropertyConfigParcels.get(i).getName());
+ }
+ }
+
+ /** Sets the schema type name. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public AppSearchSchema.Builder setSchemaType(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ resetIfBuilt();
+ mSchemaType = schemaType;
+ return this;
+ }
+
+ /**
* Sets a natural language description of this schema type.
*
* <p> For more details about the description field, see {@link
@@ -250,7 +274,7 @@
@CanIgnoreReturnValue
@NonNull
public AppSearchSchema.Builder setDescription(@NonNull String description) {
- Objects.requireNonNull(description);
+ Preconditions.checkNotNull(description);
resetIfBuilt();
mDescription = description;
return this;
@@ -271,6 +295,19 @@
}
/**
+ * Clears all properties from the given type.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public AppSearchSchema.Builder clearProperties() {
+ resetIfBuilt();
+ mPropertyConfigParcels.clear();
+ mPropertyNames.clear();
+ return this;
+ }
+
+ /**
* Adds a parent type to the given type for polymorphism, so that the given type will be
* considered as a subtype of {@code parentSchemaType}.
*
@@ -341,6 +378,18 @@
return this;
}
+ /**
+ * Clears all parent types from the given type.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public AppSearchSchema.Builder clearParentTypes() {
+ resetIfBuilt();
+ mParentTypes.clear();
+ return this;
+ }
+
/** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
@NonNull
public AppSearchSchema build() {
@@ -872,7 +921,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public StringPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1111,7 +1160,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public LongPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1211,7 +1260,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public DoublePropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1273,7 +1322,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public BooleanPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1335,7 +1384,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public BytesPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1459,7 +1508,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public DocumentPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
@@ -1721,7 +1770,7 @@
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public EmbeddingPropertyConfig.Builder setDescription(@NonNull String description) {
- mDescription = Objects.requireNonNull(description);
+ mDescription = Preconditions.checkNotNull(description);
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
index 74f370e..0b0ae88 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -348,11 +348,27 @@
private Map<String, InternalVisibilityConfig.Builder> mVisibilityConfigBuilders;
private boolean mBuilt = false;
- /** Create a {@link Builder} object} */
+ /** Creates a new {@link Builder} */
public Builder() {
setVisibilitySettingSupported(true);
}
+ /** Creates a new {@link Builder} from the given {@link GetSchemaResponse}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public Builder(@NonNull GetSchemaResponse getSchemaResponse) {
+ setVisibilitySettingSupported(true);
+ mVersion = getSchemaResponse.mVersion;
+ mSchemas.addAll(getSchemaResponse.mSchemas);
+ if (getSchemaResponse.mVisibilityConfigs != null) {
+ int count = getSchemaResponse.mVisibilityConfigs.size();
+ for (int i = 0; i < count; i++) {
+ InternalVisibilityConfig config = getSchemaResponse.mVisibilityConfigs.get(i);
+ mVisibilityConfigBuilders.put(config.getSchemaType(),
+ new InternalVisibilityConfig.Builder(config));
+ }
+ }
+ }
+
/**
* Sets the database overall schema version.
*
@@ -361,12 +377,13 @@
@CanIgnoreReturnValue
@NonNull
public Builder setVersion(@IntRange(from = 0) int version) {
+ Preconditions.checkArgument(version >= 0, "Version must be a non-negative number.");
resetIfBuilt();
mVersion = version;
return this;
}
- /** Adds one {@link AppSearchSchema} to the schema list. */
+ /** Adds one {@link AppSearchSchema} to the schema list. */
@CanIgnoreReturnValue
@NonNull
public Builder addSchema(@NonNull AppSearchSchema schema) {
@@ -376,6 +393,16 @@
return this;
}
+ /** Clears all {@link AppSearchSchema}s from the list of schemas. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSchemas() {
+ resetIfBuilt();
+ mSchemas.clear();
+ return this;
+ }
+
/**
* Sets whether or not documents from the provided {@code schemaType} will be displayed
* and visible on any system UI surface.
@@ -552,6 +579,45 @@
}
/**
+ * Clears the visibility settings configured through
+ * {@link Builder#addSchemaTypeNotDisplayedBySystem},
+ * {@link Builder#setSchemaTypeVisibleToPackages},
+ * {@link Builder#setRequiredPermissionsForSchemaTypeVisibility},
+ * {@link Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}, and
+ * {@link Builder#setSchemaTypeVisibleToConfigs} for the given {@code schemaType}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSchemaTypeVisibilityConfig(@NonNull String schemaType) {
+ Preconditions.checkNotNull(schemaType);
+ resetIfBuilt();
+ if (mVisibilityConfigBuilders != null) {
+ mVisibilityConfigBuilders.remove(schemaType);
+ }
+ return this;
+ }
+
+ /**
+ * Clears the visibility settings configured through
+ * {@link Builder#addSchemaTypeNotDisplayedBySystem},
+ * {@link Builder#setSchemaTypeVisibleToPackages},
+ * {@link Builder#setRequiredPermissionsForSchemaTypeVisibility},
+ * {@link Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}, and
+ * {@link Builder#setSchemaTypeVisibleToConfigs} for all schema types.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSchemaTypeVisibilityConfigs() {
+ resetIfBuilt();
+ if (mVisibilityConfigBuilders != null) {
+ mVisibilityConfigBuilders.clear();
+ }
+ return this;
+ }
+
+ /**
* Method to set visibility setting. If this is called with false,
* {@link #getRequiredPermissionsForSchemaTypeVisibility()},
* {@link #getSchemaTypesNotDisplayedBySystem()}}, and
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
index 16aff14..bcebd55 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
@@ -282,7 +282,7 @@
private String mNestedQuery = "";
private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
- private final String mChildPropertyExpression;
+ private String mChildPropertyExpression;
private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
@AggregationScoringStrategy
private int mAggregationScoringStrategy =
@@ -291,7 +291,7 @@
/**
* Create a specification for the joining operation in search.
*
- * <p> The child property expressions Specifies how to join documents. Documents with
+ * <p> The child property expression specifies how to join documents. Documents with
* a child property expression equal to the qualified id of the parent will be retrieved.
*
* <p> Property expressions differ from {@link PropertyPath} as property expressions may
@@ -317,8 +317,8 @@
mChildPropertyExpression = childPropertyExpression;
}
- /** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ /** Creates a new {@link Builder} from the given {@link JoinSpec}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
public Builder(@NonNull JoinSpec joinSpec) {
Preconditions.checkNotNull(joinSpec);
mNestedQuery = joinSpec.getNestedQuery();
@@ -329,6 +329,33 @@
}
/**
+ * Sets the child property expression.
+ *
+ * <p> The child property expression specifies how to join documents. Documents with
+ * a child property expression equal to the qualified id of the parent will be retrieved.
+ *
+ * <p> Property expressions differ from {@link PropertyPath} as property expressions may
+ * refer to document properties or nested document properties such as "person.business.id"
+ * as well as a property expression. Currently the only property expression is
+ * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties
+ * and nested document properties.
+ *
+ * <p> In order to join a child document to a parent document, the child document must
+ * contain the parent's qualified id at the property expression specified by this
+ * method.
+ *
+ * @param childPropertyExpression the property to match in the child documents.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setChildPropertyExpression(@NonNull String childPropertyExpression) {
+ Preconditions.checkNotNull(childPropertyExpression);
+ mChildPropertyExpression = childPropertyExpression;
+ return this;
+ }
+
+ /**
* Sets the query and the SearchSpec for the documents being joined. This will score and
* rank the joined documents as well as filter the joined documents.
*
@@ -356,7 +383,6 @@
Preconditions.checkNotNull(nestedSearchSpec);
mNestedQuery = nestedQuery;
mNestedSearchSpec = nestedSearchSpec;
-
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 2e71571..ca736d9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -636,7 +636,7 @@
/**
* Get the maximum number of results to return for each group.
*
- * @return the maximum number of results to return for each group or Integer.MAX_VALUE if
+ * @return the maximum number of results to return for each group or 0 if
* {@link Builder#setResultGrouping(int, int)} was not called.
*/
public int getResultGroupingLimit() {
@@ -804,12 +804,12 @@
@Nullable private String mSearchSourceLogTag;
private boolean mBuilt = false;
- /** Constructs a new builder for {@link SearchSpec} objects. */
+ /** Constructs a new {@link Builder} for {@link SearchSpec} objects. */
public Builder() {
}
- /** @exportToFramework:hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ /** Constructs a new {@link Builder} from the given {@link SearchSpec}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
public Builder(@NonNull SearchSpec searchSpec) {
Objects.requireNonNull(searchSpec);
mSchemas = new ArrayList<>(searchSpec.getFilterSchemas());
@@ -945,6 +945,16 @@
}
// @exportToFramework:endStrip()
+ /** Clears all schema type filters. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearFilterSchemas() {
+ resetIfBuilt();
+ mSchemas.clear();
+ return this;
+ }
+
/**
* Adds property paths for the specified type to the property filter of
* {@link SearchSpec} Entry. Only returns documents that have matches under
@@ -1078,6 +1088,16 @@
}
// @exportToFramework:endStrip()
+ /** Clears the property filters for all schema types. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearFilterProperties() {
+ resetIfBuilt();
+ mTypePropertyFilters.clear();
+ return this;
+ }
+
/**
* Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
* have the specified namespaces.
@@ -1105,6 +1125,16 @@
return this;
}
+ /** Clears all namespace filters. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearFilterNamespaces() {
+ resetIfBuilt();
+ mNamespaces.clear();
+ return this;
+ }
+
/**
* Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
* were indexed from the specified packages.
@@ -1138,6 +1168,16 @@
return this;
}
+ /** Clears all package name filters. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearFilterPackageNames() {
+ resetIfBuilt();
+ mPackageNames.clear();
+ return this;
+ }
+
/**
* Sets the number of results per page in the returned object.
*
@@ -1359,6 +1399,16 @@
return this;
}
+ /** Clears all informational ranking expressions. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearInformationalRankingExpressions() {
+ resetIfBuilt();
+ mInformationalRankingExpressions.clear();
+ return this;
+ }
+
/**
* Sets an optional log tag to indicate the source of this search.
*
@@ -1393,6 +1443,16 @@
return this;
}
+ /** Clears the log tag that indicates the source of this search. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSearchSourceLogTag() {
+ resetIfBuilt();
+ mSearchSourceLogTag = null;
+ return this;
+ }
+
/**
* Sets the order of returned search results, the default is
* {@link #ORDER_DESCENDING}, meaning that results with higher scores come first.
@@ -1629,6 +1689,16 @@
}
// @exportToFramework:endStrip()
+ /** Clears the projections for all schema types. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearProjections() {
+ resetIfBuilt();
+ mProjectionTypePropertyMasks.clear();
+ return this;
+ }
+
/**
* Sets the maximum number of results to return for each group, where groups are defined
* by grouping type.
@@ -1659,6 +1729,17 @@
return this;
}
+ /** Clears the result grouping and limit. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearResultGrouping() {
+ resetIfBuilt();
+ mGroupingTypeFlags = 0;
+ mGroupingLimit = 0;
+ return this;
+ }
+
/**
* Sets property weights by schema type and property path.
*
@@ -1714,6 +1795,16 @@
return this;
}
+ /** Clears the property weights for all schema types. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearPropertyWeights() {
+ resetIfBuilt();
+ mTypePropertyWeights.clear();
+ return this;
+ }
+
/**
* Specifies which documents to join with, and how to join.
*
@@ -1733,6 +1824,16 @@
return this;
}
+ /** Clears the {@link JoinSpec}. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearJoinSpec() {
+ resetIfBuilt();
+ mJoinSpec = null;
+ return this;
+ }
+
/**
* Sets property weights by schema type and property path.
*
@@ -1920,6 +2021,16 @@
return this;
}
+ /** Clears the embedding parameters. */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearEmbeddingParameters() {
+ resetIfBuilt();
+ mEmbeddingParameters.clear();
+ return this;
+ }
+
/**
* Sets the default embedding metric type used for embedding search
* (see {@link AppSearchSession#search}) and ranking
@@ -1984,6 +2095,19 @@
}
/**
+ * Clears the list of String parameters that can be referenced in the query through the
+ * "getSearchStringParameter({index})" function.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSearchStringParameters() {
+ resetIfBuilt();
+ mSearchStringParameters.clear();
+ return this;
+ }
+
+ /**
* Sets the NUMERIC_SEARCH feature as enabled/disabled according to the enabled parameter.
*
* @param enabled Enables the feature if true, otherwise disables it.
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 4cb1019..67140f2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -407,6 +407,32 @@
private int mVersion = DEFAULT_VERSION;
private boolean mBuilt = false;
+ /** Creates a new {@link SetSchemaRequest.Builder}. */
+ public Builder() {
+ }
+
+ /**
+ * Creates a {@link SetSchemaRequest.Builder} from the given {@link SetSchemaRequest}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ public Builder(@NonNull SetSchemaRequest request) {
+ mSchemas.addAll(request.mSchemas);
+ mSchemasNotDisplayedBySystem.addAll(request.mSchemasNotDisplayedBySystem);
+ for (Map.Entry<String, Set<PackageIdentifier>> entry
+ : request.mSchemasVisibleToPackages.entrySet()) {
+ mSchemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ mSchemasVisibleToPermissions = deepCopy(request.mSchemasVisibleToPermissions);
+ mPubliclyVisibleSchemas.putAll(request.mPubliclyVisibleSchemas);
+ for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+ request.mSchemasVisibleToConfigs.entrySet()) {
+ mSchemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ mMigrators.putAll(request.mMigrators);
+ mForceOverride = request.mForceOverride;
+ mVersion = request.mVersion;
+ }
+
/**
* Adds one or more {@link AppSearchSchema} types to the schema.
*
@@ -507,6 +533,18 @@
// @exportToFramework:endStrip()
/**
+ * Clears all {@link AppSearchSchema}s from the list of schemas.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearSchemas() {
+ resetIfBuilt();
+ mSchemas.clear();
+ return this;
+ }
+
+ /**
* Sets whether or not documents from the provided {@code schemaType} will be displayed
* and visible on any system UI surface.
*
@@ -714,10 +752,28 @@
// @exportToFramework:startStrip()
/**
- * Specify that the schema should be publicly available, to packages which already have
- * visibility to {@code packageIdentifier}.
+ * Specify that the documents from the provided
+ * {@link androidx.appsearch.annotation.Document} annotated class should be publicly
+ * available, to packages which already have visibility to {@code packageIdentifier}. This
+ * visibility is determined by the result of
+ * {@link android.content.pm.PackageManager#canPackageQuery}.
*
- * @param documentClass the document to make publicly accessible.
+ * <p> It is possible for the packageIdentifier parameter to be different from the
+ * package performing the indexing. This might happen in the case of an on-device indexer
+ * processing information about various packages. The visibility will be the same
+ * regardless of which package indexes the document, as the visibility is based on the
+ * packageIdentifier parameter.
+ *
+ * <p> If this is called repeatedly with the same
+ * {@link androidx.appsearch.annotation.Document} annotated class, the
+ * {@link PackageIdentifier} in the last call will be used as the "from" package for that
+ * class (or schema).
+ *
+ * <p> Calling this with packageIdentifier set to null is valid, and will remove public
+ * visibility for the class (or schema).
+ *
+ * @param documentClass the {@link androidx.appsearch.annotation.Document} annotated class
+ * to make publicly accessible.
* @param packageIdentifier if an app can see this package via
* PackageManager#canPackageQuery, it will be able to see the
* documents of type {@code documentClass}.
@@ -861,6 +917,18 @@
return this;
}
+ /**
+ * Clears all {@link Migrator}s.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder clearMigrators() {
+ resetIfBuilt();
+ mMigrators.clear();
+ return this;
+ }
+
// @exportToFramework:startStrip()
/**
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/ast/FunctionNode.java b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/FunctionNode.java
new file mode 100644
index 0000000..4e479e0
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/FunctionNode.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.ast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * {@link Node} that represents a function.
+ *
+ * <p>Every function node will have a function name and some arguments represented as fields on
+ * the class extending {@link FunctionNode}.
+ *
+ * <p>FunctionNode should be implemented by a node that implements a specific function.
+ */
+@ExperimentalAppSearchApi
+@FlaggedApi(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+public interface FunctionNode extends Node {
+ /**
+ * Enums representing functions available to use in the query language.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({
+ })
+ @interface FunctionName {}
+
+ /**
+ * Gets the name of the node that extends the {@link FunctionNode}.
+ */
+ @NonNull
+ @FunctionName
+ String getFunctionName();
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/AndNode.java b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/AndNode.java
new file mode 100644
index 0000000..0b0e1a7
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/AndNode.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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.ast.operators;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
+import androidx.appsearch.ast.Node;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link Node} that represents logical AND of nodes.
+ */
+@ExperimentalAppSearchApi
+@FlaggedApi(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+public final class AndNode implements Node {
+ private List<Node> mChildren;
+
+ /**
+ * Constructor for {@link AndNode} that represents logical AND over all its child nodes.
+ *
+ * @param childNodes The list of {@link Node} of at least size two representing queries to be
+ * logically ANDed over.
+ */
+ public AndNode(@NonNull List<Node> childNodes) {
+ Preconditions.checkNotNull(childNodes);
+ Preconditions.checkArgument(childNodes.size() >= 2,
+ /*errorMessage=*/ "Number of nodes must be at least two.");
+ mChildren = new ArrayList<>(childNodes);
+ }
+
+ /**
+ * Convenience constructor for {@link AndNode} that represents logical AND over all its
+ * child nodes and takes in a varargs of nodes.
+ *
+ * @param firstChild The first node to be ANDed over, which is required.
+ * @param secondChild The second node to be ANDed over, which is required.
+ * @param additionalChildren Additional nodes to be ANDed over, which are optional.
+ */
+ public AndNode(@NonNull Node firstChild, @NonNull Node secondChild,
+ @NonNull Node... additionalChildren) {
+ ArrayList<Node> childNodes = new ArrayList<>();
+ childNodes.add(Preconditions.checkNotNull(firstChild));
+ childNodes.add(Preconditions.checkNotNull(secondChild));
+ childNodes.addAll(List.of(Preconditions.checkNotNull(additionalChildren)));
+ mChildren = childNodes;
+ }
+
+ /**
+ * Get the list of nodes being logically ANDed over by this node.
+ */
+ @Override
+ @NonNull
+ public List<Node> getChildren() {
+ return Collections.unmodifiableList(mChildren);
+ }
+
+ /**
+ * Set the nodes being logically ANDed over by this node.
+ *
+ * @param childNodes A list of {@link Node} of at least size two representing the nodes to be
+ * logically ANDed over in this node.
+ */
+ public void setChildren(@NonNull List<Node> childNodes) {
+ Preconditions.checkNotNull(childNodes);
+ Preconditions.checkArgument(childNodes.size() >= 2,
+ /*errorMessage=*/ "Number of nodes must be at least two.");
+ mChildren = new ArrayList<>(childNodes);
+ }
+
+ /**
+ * Add a child node to the end of the current list of child nodes {@link #mChildren}.
+ *
+ * @param childNode A {@link Node} to add to the end of the list of child nodes.
+ */
+ public void addChild(@NonNull Node childNode) {
+ mChildren.add(Preconditions.checkNotNull(childNode));
+ }
+
+ /**
+ * Replace the child node at the provided index with the provided {@link Node}.
+ *
+ * @param index The index at which to replace the child node in the list of child nodes. Must be
+ * in range of the size of {@link #mChildren}.
+ * @param childNode The {@link Node} that is replacing the childNode at the provided index.
+ */
+ public void setChild(int index, @NonNull Node childNode) {
+ Preconditions.checkArgumentInRange(index, /*lower=*/ 0, /*upper=*/ mChildren.size() - 1,
+ /*valueName=*/ "Index");
+ mChildren.set(index, Preconditions.checkNotNull(childNode));
+ }
+
+ /**
+ * Remove tbe child {@link Node} at the given index from the list of child nodes.
+ *
+ * <p>The list of child nodes must contain at least 3 nodes to perform this operation.
+ */
+ public void removeChild(int index) {
+ Preconditions.checkState(mChildren.size() > 2, "List of child nodes must"
+ + "contain at least 3 nodes in order to remove.");
+ Preconditions.checkArgumentInRange(index, /*lower=*/ 0, /*upper=*/ mChildren.size() - 1,
+ /*valueName=*/ "Index");
+ mChildren.remove(index);
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/OrNode.java b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/OrNode.java
new file mode 100644
index 0000000..f8bbd13
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/operators/OrNode.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 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.ast.operators;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
+import androidx.appsearch.ast.Node;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link Node} that represents logical OR of nodes.
+ */
+@ExperimentalAppSearchApi
+@FlaggedApi(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+public final class OrNode implements Node{
+ private List<Node> mChildren;
+
+ /**
+ * Constructor for {@link OrNode} that represents logical OR over all its child nodes.
+ *
+ * @param childNodes The nodes representing queries to be logically ORed over.
+ */
+ public OrNode(@NonNull List<Node> childNodes) {
+ Preconditions.checkNotNull(childNodes);
+ Preconditions.checkArgument(childNodes.size() >= 2,
+ /*errorMessage=*/ "Number of nodes must be at least two.");
+ mChildren = new ArrayList<>(childNodes);
+ }
+
+ /**
+ * Convenience constructor for {@link OrNode} that represents logical OR over all its
+ * child nodes and takes in a varargs of nodes.
+ *
+ * @param firstChild The first node to be ORed over, which is required.
+ * @param secondChild The second node to be ORed over, which is required.
+ * @param additionalChildren Additional nodes to be ORed over, which are optional.
+ */
+ public OrNode(@NonNull Node firstChild, @NonNull Node secondChild,
+ @NonNull Node... additionalChildren) {
+ ArrayList<Node> childNodes = new ArrayList<Node>();
+ childNodes.add(Preconditions.checkNotNull(firstChild));
+ childNodes.add(Preconditions.checkNotNull(secondChild));
+ childNodes.addAll(List.of(Preconditions.checkNotNull(additionalChildren)));
+ mChildren = childNodes;
+ }
+
+ /**
+ * Get the list of nodes being logically ORed over by this node.
+ */
+ @Override
+ @NonNull
+ public List<Node> getChildren() {
+ return Collections.unmodifiableList(mChildren);
+ }
+
+ /**
+ * Set the nodes being logically ORed over by this node.
+ *
+ * @param childNodes A list of {@link Node} representing the nodes to be logically ORed over
+ * in this node.
+ */
+ public void setChildren(@NonNull List<Node> childNodes) {
+ Preconditions.checkNotNull(childNodes);
+ Preconditions.checkArgument(childNodes.size() >= 2,
+ /*errorMessage=*/ "Number of nodes must be at least two.");
+ mChildren = new ArrayList<>(childNodes);
+ }
+
+ /**
+ * Add a child node to the end of the current list of child nodes {@link #mChildren}.
+ *
+ * @param childNode A {@link Node} to add to the end of the list of child nodes.
+ */
+ public void addChild(@NonNull Node childNode) {
+ mChildren.add(Preconditions.checkNotNull(childNode));
+ }
+
+ /**
+ * Replace the child node at the provided index with the provided {@link Node}.
+ *
+ * @param index The index at which to replace the child node in the list of child nodes. Must be
+ * in range of the size of {@link #mChildren}.
+ * @param childNode The {@link Node} that is replacing the childNode at the provided index.
+ */
+ public void setChild(int index, @NonNull Node childNode) {
+ Preconditions.checkArgumentInRange(index, /*lower=*/ 0, /*upper=*/ mChildren.size() - 1,
+ /*valueName=*/ "Index");
+ mChildren.set(index, Preconditions.checkNotNull(childNode));
+ }
+
+ /**
+ * Remove tbe child {@link Node} at the given index from the list of child nodes.
+ *
+ * <p>The list of child nodes must contain at least 3 nodes to perform this operation.
+ */
+ public void removeChild(int index) {
+ Preconditions.checkState(mChildren.size() > 2, "List of child nodes must"
+ + "contain at least 3 nodes in order to remove.");
+ Preconditions.checkArgumentInRange(index, /*lower=*/ 0, /*upper=*/ mChildren.size() - 1,
+ /*valueName=*/ "Index");
+ mChildren.remove(index);
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
index 0816e9e..ddca2c1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -154,6 +154,16 @@
public static final String FLAG_ENABLE_ABSTRACT_SYNTAX_TREES =
FLAG_PREFIX + "enable_abstract_syntax_trees";
+ /**
+ * Enables additional builder copy constructors for
+ * {@link androidx.appsearch.app.AppSearchSchema},
+ * {@link androidx.appsearch.app.SetSchemaRequest}, {@link androidx.appsearch.app.SearchSpec},
+ * {@link androidx.appsearch.app.JoinSpec}, {@link androidx.appsearch.app.AppSearchBatchResult},
+ * and {@link androidx.appsearch.app.GetSchemaResponse}.
+ */
+ public static final String FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS =
+ FLAG_PREFIX + "enable_additional_builder_copy_constructors";
+
// Whether the features should be enabled.
//
// In Jetpack, those should always return true.
@@ -285,4 +295,15 @@
public static boolean enableAbstractSyntaxTrees() {
return true;
}
+
+ /**
+ * Whether additional builder copy constructors for
+ * {@link androidx.appsearch.app.AppSearchSchema},
+ * {@link androidx.appsearch.app.SetSchemaRequest}, {@link androidx.appsearch.app.SearchSpec},
+ * {@link androidx.appsearch.app.JoinSpec}, {@link androidx.appsearch.app.AppSearchBatchResult},
+ * and {@link androidx.appsearch.app.GetSchemaResponse} should be enabled.
+ */
+ public static boolean enableAdditionalBuilderCopyConstructors() {
+ return true;
+ }
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index b7994e4..a849371 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -41,13 +41,24 @@
* Note that when StartupMode.COLD is used, additional work must be performed during target app
* startup to initialize tracing.
*/
- private val _perfettoSdkTracingEnable: Boolean
val perfettoSdkTracingEnable: Boolean
get() = perfettoSdkTracingEnableOverride ?: _perfettoSdkTracingEnable
- /** Allows tests to override whether full tracing is enabled */
+ private val _perfettoSdkTracingEnable: Boolean
@VisibleForTesting var perfettoSdkTracingEnableOverride: Boolean? = null
+ /**
+ * Base URL for help articles for Startup Insights.
+ *
+ * This property should only be used while the Startup Insights feature is under development. It
+ * can be overridden for testing purposes using [startupInsightsHelpUrlBaseOverride].
+ */
+ val startupInsightsHelpUrlBase: String?
+ get() = startupInsightsHelpUrlBaseOverride ?: _startupInsightsHelpUrlBase
+
+ private val _startupInsightsHelpUrlBase: String?
+ @VisibleForTesting var startupInsightsHelpUrlBaseOverride: String? = null
+
val enabledRules: Set<RuleType>
enum class RuleType {
@@ -175,6 +186,9 @@
?: arguments.getBenchmarkArgument("fullTracing.enable")?.toBoolean()
?: false
+ _startupInsightsHelpUrlBase =
+ arguments.getBenchmarkArgument("startupInsights.helpUrlBase", defaultValue = null)
+
// Transform comma-delimited list into set of suppressed errors
// E.g. "DEBUGGABLE, UNLOCKED" -> setOf("DEBUGGABLE", "UNLOCKED")
suppressedErrors =
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkConfigApi.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkConfigApi.kt
index f7a26ad..f34eb3e 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkConfigApi.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkConfigApi.kt
@@ -16,6 +16,7 @@
package androidx.benchmark
+import androidx.annotation.RestrictTo
import androidx.benchmark.perfetto.PerfettoConfig
/**
@@ -33,4 +34,16 @@
val startupInsightsConfig: StartupInsightsConfig? = null
)
-@ExperimentalBenchmarkConfigApi public class StartupInsightsConfig(val isEnabled: Boolean)
+/** Configuration for startup insights. */
+@ExperimentalBenchmarkConfigApi
+public class StartupInsightsConfig(val isEnabled: Boolean) {
+ /**
+ * Base URL for linking to more information about specific startup reasons. This URL should
+ * accept a reason ID as a direct suffix. For example, a base URL of
+ * `https://developer.android.com/[...]/slow-start-reason#` could be combined with a reason ID
+ * of `MAIN_THREAD_MONITOR_CONTENTION` to create a complete URL like:
+ * `https://developer.android.com/[...]/slow-start-reason#MAIN_THREAD_MONITOR_CONTENTION`
+ */
+ val reasonHelpUrlBase: String? = Arguments.startupInsightsHelpUrlBase
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get
+}
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 347d615..8c0b606 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -69,7 +69,7 @@
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.9.0")
- implementation("androidx.profileinstaller:profileinstaller:1.4.0")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.1")
implementation("androidx.tracing:tracing-ktx:1.1.0")
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
@@ -105,9 +105,10 @@
kotlinOptions {
// Enable using experimental APIs from within same version group
freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.ExperimentalBenchmarkConfigApi",
"-opt-in=androidx.benchmark.macro.ExperimentalMetricApi",
"-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
- "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi",
]
}
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt
index 7bca88d..d593ea6 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/InsightExtensions.kt
@@ -17,6 +17,8 @@
package androidx.benchmark.macro
import androidx.benchmark.Insight
+import androidx.benchmark.StartupInsightsConfig
+import java.net.URLEncoder
import perfetto.protos.AndroidStartupMetric.SlowStartReason
import perfetto.protos.AndroidStartupMetric.ThresholdValue.ThresholdUnit
@@ -26,7 +28,10 @@
*
* TODO(353692849): add unit tests
*/
-internal fun createInsightsIdeSummary(rawInsights: List<List<SlowStartReason>>): List<Insight> {
+internal fun createInsightsIdeSummary(
+ rawInsights: List<List<SlowStartReason>>,
+ startupInsightsConfig: StartupInsightsConfig?
+): List<Insight> {
fun createInsightString(
criterion: SlowStartReason,
observed: List<IndexedValue<SlowStartReason>>
@@ -49,7 +54,20 @@
}
val criterionString = buildString {
- append(requireNotNull(criterion.reason))
+ val reasonHelpUrlBase = startupInsightsConfig?.reasonHelpUrlBase
+ if (reasonHelpUrlBase != null) {
+ append("[")
+ append(requireNotNull(criterion.reason).replace("]", "\\]"))
+ append("]")
+ append("(")
+ append(reasonHelpUrlBase.replace(")", "\\)")) // base url
+ val reasonId = requireNotNull(criterion.reason_id).name
+ append(URLEncoder.encode(reasonId, Charsets.UTF_8.name())) // reason id as a suffix
+ append(")")
+ } else {
+ append(requireNotNull(criterion.reason))
+ }
+
val thresholdValue = requireNotNull(expectedValue.value_)
append(" (expected: ")
if (thresholdUnit == ThresholdUnit.TRUE_OR_FALSE) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 05b4710..823a9b3 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -330,7 +330,8 @@
warningMessage = warningMessage,
testName = uniqueName,
measurements = measurements,
- insights = createInsightsIdeSummary(insightsList),
+ insights =
+ createInsightsIdeSummary(insightsList, experimentalConfig?.startupInsightsConfig),
iterationTracePaths = tracePaths,
profilerResults = profilerResults,
useTreeDisplayFormat = experimentalConfig?.startupInsightsConfig?.isEnabled == true
diff --git a/biometric/biometric/src/main/res/values-kk/strings.xml b/biometric/biometric/src/main/res/values-kk/strings.xml
index 24ae7d4..42b808c2 100644
--- a/biometric/biometric/src/main/res/values-kk/strings.xml
+++ b/biometric/biometric/src/main/res/values-kk/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="fingerprint_dialog_touch_sensor" msgid="1072308044213194243">"Саусақ ізін оқу сканерін түртіңіз"</string>
+ <string name="fingerprint_dialog_touch_sensor" msgid="1072308044213194243">"Саусақ ізін оқу сканерін түртіңіз."</string>
<string name="fingerprint_not_recognized" msgid="3873359464293253009">"Танылмады"</string>
<string name="fingerprint_error_hw_not_available" msgid="8216738333501875566">"Саусақ ізі жабдығы қолжетімді емес."</string>
<string name="fingerprint_error_no_fingerprints" msgid="7520712796891883488">"Саусақ іздері тіркелмеген."</string>
diff --git a/biometric/biometric/src/main/res/values-lt/strings.xml b/biometric/biometric/src/main/res/values-lt/strings.xml
index 180b3fe..8dc2c23 100644
--- a/biometric/biometric/src/main/res/values-lt/strings.xml
+++ b/biometric/biometric/src/main/res/values-lt/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="fingerprint_dialog_touch_sensor" msgid="1072308044213194243">"Palieskite piršto antspaudo jutiklį"</string>
+ <string name="fingerprint_dialog_touch_sensor" msgid="1072308044213194243">"Palieskite piršto atspaudo jutiklį"</string>
<string name="fingerprint_not_recognized" msgid="3873359464293253009">"Neatpažinta"</string>
<string name="fingerprint_error_hw_not_available" msgid="8216738333501875566">"Piršto antspaudo aparatinė įranga nepasiekiama."</string>
<string name="fingerprint_error_no_fingerprints" msgid="7520712796891883488">"Neužregistruota jokių kontrolinių kodų."</string>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 90a6d2b..9c101e5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -173,6 +173,7 @@
project.configureLint()
project.configureKtfmt()
project.configureKotlinVersion()
+ project.configureJavaFormat()
// Avoid conflicts between full Guava and LF-only Guava.
project.configureGuavaUpgradeHandler()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt b/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt
new file mode 100644
index 0000000..5d41cfc
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
+import org.gradle.api.file.FileTree
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.IgnoreEmptyDirectories
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFiles
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.options.Option
+import org.gradle.process.ExecOperations
+
+fun Project.configureJavaFormat() {
+ val javaFormatClasspath = getJavaFormatConfiguration()
+ tasks.register("javaFormat", JavaFormatTask::class.java) { task ->
+ task.javaFormatClasspath.from(javaFormatClasspath)
+ }
+}
+
+private fun Project.getJavaFormatConfiguration(): FileCollection {
+ val config =
+ configurations.detachedConfiguration(
+ dependencies.create(getLibraryByName("googlejavaformat"))
+ )
+ return files(config)
+}
+
+@CacheableTask
+abstract class JavaFormatTask : DefaultTask() {
+ init {
+ description = "Fix Java code style deviations."
+ group = "formatting"
+ }
+
+ @get:Input
+ @set:Option(option = "fix-imports-only", description = "Only correct imports")
+ var importsOnly: Boolean = false
+
+ @get:Inject abstract val execOperations: ExecOperations
+
+ @get:Classpath abstract val javaFormatClasspath: ConfigurableFileCollection
+
+ @get:Inject abstract val objects: ObjectFactory
+
+ @[InputFiles PathSensitive(PathSensitivity.RELATIVE) SkipWhenEmpty IgnoreEmptyDirectories]
+ open fun getInputFiles(): FileTree {
+ return objects.fileTree().setDir(INPUT_DIR).apply {
+ include(INCLUDED_FILES)
+ exclude(excludedDirectoryGlobs)
+ }
+ }
+
+ // Format task rewrites inputs, so the outputs are the same as inputs.
+ @OutputFiles fun getRewrittenFiles(): FileTree = getInputFiles()
+
+ private fun getArgsList(): List<String> {
+ val arguments = mutableListOf("--aosp", "--replace")
+ if (importsOnly) arguments.add("--fix-imports-only")
+ arguments.addAll(getInputFiles().files.map { it.absolutePath })
+ return arguments
+ }
+
+ @TaskAction
+ fun runFormat() {
+ execOperations.javaexec { javaExecSpec ->
+ javaExecSpec.mainClass.set(MAIN_CLASS)
+ javaExecSpec.classpath = javaFormatClasspath
+ javaExecSpec.args = getArgsList()
+ javaExecSpec.jvmArgs(
+ "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ )
+ }
+ }
+
+ companion object {
+ private val excludedDirectories =
+ listOf(
+ "test-data",
+ "external",
+ )
+
+ private val excludedDirectoryGlobs = excludedDirectories.map { "**/$it/**/*.java" }
+ private const val MAIN_CLASS = "com.google.googlejavaformat.java.Main"
+ private const val INPUT_DIR = "src"
+ private const val INCLUDED_FILES = "**/*.java"
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index d586bd4..bff4d0d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -211,7 +211,6 @@
private fun Project.configureLint(lint: Lint, isLibrary: Boolean) {
val extension = project.androidXExtension
- val isMultiplatform = project.multiplatformExtension != null
val lintChecksProject = findLintProject(":lint-checks") ?: return
project.dependencies.add("lintChecks", lintChecksProject)
@@ -263,13 +262,6 @@
fatal.add("VisibleForTests")
}
- if (isMultiplatform) {
- // Disable classfile-based checks because lint cannot find the class files for
- // multiplatform projects and `SourceSet.java.classesDirectory` is not configurable.
- // This is not ideal, but it's better than having no lint checks at all.
- disable.add("LintError")
- }
-
// Disable a check that's only relevant for apps that ship to Play Store. (b/299278101)
disable.add("ExpiredTargetSdkVersion")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
index ba370be..ca891ea 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
@@ -115,18 +115,18 @@
testImplementation.extendsFrom(bundle)
// Relocation needed to avoid classpath conflicts with Android Studio (b/337980250)
- // Can be removed if we migrate from using kotlinx-metadata-jvm inside of lint checks
- val relocations = listOf(Relocation("kotlinx.metadata", "androidx.lint.kotlinx.metadata"))
+ // Can be removed if we migrate from using kotlin-metadata-jvm inside of lint checks
+ val relocations = listOf(Relocation("kotlin.metadata", "androidx.lint.kotlin.metadata"))
val repackage = configureRepackageTaskForType(relocations, bundle, null)
val sourceSets = extensions.getByType(SourceSetContainer::class.java)
repackage.configure { task ->
task.from(sourceSets.findByName("main")?.output)
- // kotlinx-metadata-jvm has a service descriptor that needs transformation
+ // kotlin-metadata-jvm has a service descriptor that needs transformation
task.mergeServiceFiles()
- // Exclude Kotlin metadata files from kotlinx-metadata-jvm
+ // Exclude Kotlin metadata files from kotlin-metadata-jvm
task.exclude(
- "META-INF/kotlinx-metadata-jvm.kotlin_module",
- "META-INF/kotlinx-metadata.kotlin_module",
+ "META-INF/kotlin-metadata-jvm.kotlin_module",
+ "META-INF/kotlin-metadata.kotlin_module",
"META-INF/metadata.jvm.kotlin_module",
"META-INF/metadata.kotlin_module"
)
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index af8fa23..a9bb97d 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -64,17 +64,6 @@
fi
}
-# export some variables depending on the platform
-if [[ "$(uname)" == Darwin* ]]; then
- ANDROID_HOME=../../prebuilts/fullsdk-darwin
-else
- ANDROID_HOME=../../prebuilts/fullsdk-linux
- # Remove when b/365535238 is fixed as the Linux image will contain Python3
- export PATH="$ANDROID_HOME/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/python3/bin:$PATH"
- # Remove when b/366010045 is resolved: android platform build requires either en_US.UTF-8 or C.UTF-8 to exist
- export LC_ALL=C.UTF-8
-fi
-
BUILD_STATUS=0
# enable remote build cache unless explicitly disabled
if [ "$USE_ANDROIDX_REMOTE_BUILD_CACHE" == "" ]; then
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
index 02b76ee..96895c8 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -60,7 +60,7 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import androidx.camera.testing.impl.fakes.FakeImageProxy
import androidx.camera.testing.impl.mocks.MockScreenFlash
import androidx.concurrent.futures.await
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..e2356fb 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -44,9 +44,10 @@
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
import androidx.camera.testing.fakes.FakeCameraControl;
+import androidx.camera.testing.imagecapture.CaptureResult;
import androidx.camera.testing.impl.CoreAppTestUtil;
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult;
import androidx.camera.testing.impl.fakes.FakeCameraCoordinator;
import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
@@ -126,7 +127,7 @@
fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
// Notify the cancel after the capture request has been successfully submitted
- fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
+ fakeCameraControl.completeAllCaptureRequests(CaptureResult.cancelledResult());
});
mInstrumentation.runOnMainSync(
@@ -156,7 +157,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
// Notify the failure after the capture request has been successfully submitted
- fakeCameraControl.notifyAllRequestsOnCaptureFailed();
+ fakeCameraControl.completeAllCaptureRequests(CaptureResult.failedResult());
});
mInstrumentation.runOnMainSync(
@@ -304,7 +305,7 @@
// Simulates the case that the capture request failed after running in 300 ms.
fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
CameraXExecutors.mainThreadExecutor().schedule(() -> {
- fakeCameraControl.notifyAllRequestsOnCaptureFailed();
+ fakeCameraControl.completeAllCaptureRequests(CaptureResult.failedResult());
}, 300, TimeUnit.MILLISECONDS);
});
@@ -482,7 +483,7 @@
CaptureFailedRetryEnabler retryEnabler = new CaptureFailedRetryEnabler();
// Because of retry in some devices, we may need to notify capture failures multiple times.
addExtraFailureNotificationsForRetry(fakeCameraControl, retryEnabler.getRetryCount());
- fakeCameraControl.notifyAllRequestsOnCaptureFailed();
+ fakeCameraControl.completeAllCaptureRequests(CaptureResult.failedResult());
// Assert.
verify(callback, timeout(1000).times(1)).onError(any());
@@ -494,7 +495,7 @@
if (retryCount > 0) {
cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
- cameraControl.notifyAllRequestsOnCaptureFailed();
+ cameraControl.completeAllCaptureRequests(CaptureResult.failedResult());
});
}
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java
index 966b690..c8653a1 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java
@@ -27,8 +27,8 @@
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.TagBundle;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
import androidx.camera.testing.impl.HandlerUtil;
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult;
import androidx.camera.testing.impl.fakes.FakeImageReaderProxy;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
index 260965f..065ac04 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
@@ -34,6 +34,7 @@
import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
import androidx.camera.core.imagecapture.Utils.TIMESTAMP
import androidx.camera.core.imagecapture.Utils.WIDTH
+import androidx.camera.core.imagecapture.Utils.createTakePictureRequest
import androidx.camera.core.impl.Quirks
import androidx.camera.core.impl.utils.Exif
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
@@ -144,7 +145,7 @@
private suspend fun processYuvAndVerifyOutputSize(outputFileOptions: OutputFileOptions?) {
// Arrange: create node with JPEG input and grayscale effect.
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(ImageFormat.YUV_420_888, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.YUV_420_888, listOf(ImageFormat.JPEG))
val imageIn =
createYuvFakeImageProxy(
CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
@@ -167,7 +168,7 @@
null,
InternalImageProcessor(GrayscaleImageEffect())
)
- val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, listOf(ImageFormat.JPEG))
val imageIn =
createJpegFakeImageProxy(
CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
@@ -194,11 +195,13 @@
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- CROP_RECT,
- /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ CROP_RECT,
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -220,18 +223,20 @@
) {
// Arrange: create a request with no cropping
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, listOf(ImageFormat.JPEG))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- Rect(0, 0, WIDTH, HEIGHT),
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -260,18 +265,20 @@
// Arrange: create a request with no cropping
val format = ImageFormat.JPEG_R
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(format, format)
+ val nodeIn = ProcessingNode.In.of(format, listOf(format))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- Rect(0, 0, WIDTH, HEIGHT),
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -306,18 +313,20 @@
private suspend fun inMemoryInputPacket_callbackInvoked(outputFileOptions: OutputFileOptions?) {
// Arrange.
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, listOf(ImageFormat.JPEG))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- Rect(0, 0, WIDTH, HEIGHT),
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -346,18 +355,20 @@
// Arrange.
val format = ImageFormat.JPEG_R
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(format, format)
+ val nodeIn = ProcessingNode.In.of(format, listOf(format))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- Rect(0, 0, WIDTH, HEIGHT),
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -385,7 +396,7 @@
private suspend fun saveJpegOnDisk_verifyOutput(outputFileOptions: OutputFileOptions?) {
// Arrange: create a on-disk processing request.
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, listOf(ImageFormat.JPEG))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val jpegBytes =
@@ -395,11 +406,13 @@
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- CROP_RECT,
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ CROP_RECT,
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -425,7 +438,7 @@
// Arrange: create a on-disk processing request.
val format = ImageFormat.JPEG_R
val node = ProcessingNode(mainThreadExecutor(), null)
- val nodeIn = ProcessingNode.In.of(format, format)
+ val nodeIn = ProcessingNode.In.of(format, listOf(format))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val jpegBytes =
@@ -438,11 +451,13 @@
val processingRequest =
ProcessingRequest(
{ listOf() },
- outputFileOptions,
- CROP_RECT,
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null else listOf(outputFileOptions),
+ CROP_RECT,
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -483,18 +498,20 @@
// Force inject the quirk for the A24 incorrect JPEG metadata problem
val node =
ProcessingNode(mainThreadExecutor(), Quirks(listOf(IncorrectJpegMetadataQuirk())), null)
- val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, listOf(ImageFormat.JPEG))
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
val processingRequest =
ProcessingRequest(
{ listOf() },
- null,
- Rect(0, 0, WIDTH, HEIGHT),
- 0,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ /*outputFileOptions=*/ null,
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 0, // 0 because exif does not have rotation.
+ /*jpegQuality=*/ 100
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/Utils.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/Utils.kt
index a3672fd..f59512c 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/Utils.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/Utils.kt
@@ -20,9 +20,11 @@
import android.graphics.Rect
import android.util.Size
import androidx.camera.core.ImageCapture
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import java.io.File
import java.util.UUID
+import org.mockito.Mockito.mock
object Utils {
const val WIDTH = 640
@@ -46,4 +48,35 @@
"hdrgm:Version=",
"Item:Semantic=\"GainMap\"",
)
+
+ fun createTakePictureRequest(
+ outputFileOptions: List<ImageCapture.OutputFileOptions>?,
+ cropRect: Rect,
+ sensorToBufferTransform: Matrix,
+ rotationDegrees: Int,
+ jpegQuality: Int,
+ isSimultaneousCapture: Boolean = false
+ ): TakePictureRequest {
+ var onDiskCallback: ImageCapture.OnImageSavedCallback? = null
+ var onMemoryCallback: ImageCapture.OnImageCapturedCallback? = null
+ if (outputFileOptions == null) {
+ onMemoryCallback = mock(ImageCapture.OnImageCapturedCallback::class.java)
+ } else {
+ onDiskCallback = mock(ImageCapture.OnImageSavedCallback::class.java)
+ }
+
+ return TakePictureRequest.of(
+ CameraXExecutors.mainThreadExecutor(),
+ onMemoryCallback,
+ onDiskCallback,
+ outputFileOptions,
+ cropRect,
+ sensorToBufferTransform,
+ rotationDegrees,
+ jpegQuality,
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ isSimultaneousCapture,
+ listOf()
+ )
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index bd994cb..72c23be 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -47,6 +47,7 @@
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_SECONDARY_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
@@ -319,6 +320,13 @@
public static final int OUTPUT_FORMAT_RAW = 2;
/**
+ * Captures raw images in the {@link ImageFormat#RAW_SENSOR} and {@link ImageFormat#JPEG}
+ * image formats.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final int OUTPUT_FORMAT_RAW_JPEG = 3;
+
+ /**
* Provides a static configuration with implementation-agnostic options.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -479,6 +487,9 @@
} else {
if (isOutputFormatRaw(builder.getMutableConfig())) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ } else if (isOutputFormatRawJpeg(builder.getMutableConfig())) {
+ builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ builder.getMutableConfig().insertOption(OPTION_SECONDARY_INPUT_FORMAT, JPEG);
} else if (isOutputFormatUltraHdr(builder.getMutableConfig())) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG_R);
builder.getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE,
@@ -531,6 +542,11 @@
OUTPUT_FORMAT_RAW);
}
+ private static boolean isOutputFormatRawJpeg(@NonNull MutableConfig config) {
+ return Objects.equals(config.retrieveOption(OPTION_OUTPUT_FORMAT, null),
+ OUTPUT_FORMAT_RAW_JPEG);
+ }
+
/**
* Configures flash mode to CameraControlInternal once it is ready.
*/
@@ -887,6 +903,11 @@
*
* <p>The listener is responsible for calling {@link Image#close()} on the returned image.
*
+ * <p>For simultaneous image capture with {@link #OUTPUT_FORMAT_RAW_JPEG}, the
+ * {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)} will be triggered twice, one for
+ * {@link ImageFormat#RAW_SENSOR} and the other for {@link ImageFormat#JPEG}. The order in which
+ * image format is triggered first is not guaranteed.
+ *
* @param executor The executor in which the callback methods will be run.
* @param callback Callback to be invoked for the newly captured image.
*
@@ -923,9 +944,34 @@
final @NonNull OutputFileOptions outputFileOptions,
final @NonNull Executor executor,
final @NonNull OnImageSavedCallback imageSavedCallback) {
+ takePicture(List.of(outputFileOptions), executor, imageSavedCallback);
+ }
+
+ /**
+ * Captures two still images simultaneously and saves to a file along with application
+ * specified metadata.
+ *
+ * <p>Currently only {@link #OUTPUT_FORMAT_RAW_JPEG} is supporting simultaneous image capture.
+ * It needs two {@link OutputFileOptions}, the first one is used for
+ * {@link ImageFormat#RAW_SENSOR} image and the second one is for {@link ImageFormat#JPEG}. The
+ * order in which image format is triggered first is not guaranteed.
+ *
+ * @param outputFileOptions List of options to store the newly captured images.
+ * @param executor The executor in which the callback methods will be run.
+ * @param imageSavedCallback Callback to be called for the newly captured image.
+ *
+ * @throws IllegalArgumentException If {@link ImageCapture#FLASH_MODE_SCREEN} is used without a
+ * a non-null {@code ScreenFlash} instance set.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void takePicture(
+ final @NonNull List<OutputFileOptions> outputFileOptions,
+ final @NonNull Executor executor,
+ final @NonNull OnImageSavedCallback imageSavedCallback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(
- () -> takePicture(outputFileOptions, executor, imageSavedCallback));
+ () -> takePicture(outputFileOptions,
+ executor, imageSavedCallback));
return;
}
takePictureInternal(executor, /*inMemoryCallback=*/null, imageSavedCallback,
@@ -1000,6 +1046,7 @@
if (isRawSupported()) {
formats.add(OUTPUT_FORMAT_RAW);
+ formats.add(OUTPUT_FORMAT_RAW_JPEG);
}
return formats;
@@ -1407,7 +1454,7 @@
private void takePictureInternal(@NonNull Executor executor,
@Nullable OnImageCapturedCallback inMemoryCallback,
@Nullable ImageCapture.OnImageSavedCallback onDiskCallback,
- @Nullable OutputFileOptions outputFileOptions) {
+ @Nullable List<OutputFileOptions> outputFileOptions) {
checkMainThread();
if (getFlashMode() == ImageCapture.FLASH_MODE_SCREEN
&& mScreenFlashWrapper.getBaseScreenFlash() == null) {
@@ -1429,6 +1476,7 @@
getRelativeRotation(camera),
getJpegQualityInternal(),
getCaptureMode(),
+ getCurrentConfig().getSecondaryInputFormat() != ImageFormat.UNKNOWN,
mSessionConfigBuilder.getSingleCameraCaptureCallbacks()));
}
@@ -1644,7 +1692,8 @@
*/
@OptIn(markerClass = androidx.camera.core.ExperimentalImageCaptureOutputFormat.class)
@Target({ElementType.TYPE_USE})
- @IntDef({OUTPUT_FORMAT_JPEG, OUTPUT_FORMAT_JPEG_ULTRA_HDR, OUTPUT_FORMAT_RAW})
+ @IntDef({OUTPUT_FORMAT_JPEG, OUTPUT_FORMAT_JPEG_ULTRA_HDR,
+ OUTPUT_FORMAT_RAW, OUTPUT_FORMAT_RAW_JPEG})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface OutputFormat {
@@ -2368,6 +2417,9 @@
} else {
if (isOutputFormatRaw(getMutableConfig())) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ } else if (isOutputFormatRawJpeg(getMutableConfig())) {
+ getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ getMutableConfig().insertOption(OPTION_SECONDARY_INPUT_FORMAT, JPEG);
} else if (isOutputFormatUltraHdr(getMutableConfig())) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG_R);
getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index b81133b..62245fd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -49,10 +49,11 @@
public final class ImageProcessingUtil {
private static final String TAG = "ImageProcessingUtil";
+ public static final String JNI_LIB_NAME = "image_processing_util_jni";
private static int sImageCount = 0;
static {
- System.loadLibrary("image_processing_util_jni");
+ System.loadLibrary(JNI_LIB_NAME);
}
enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
*
* <p>The {@link Bitmap} will be recycled and should not be used after the processing.
*/
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
@NonNull
@Override
@@ -79,16 +81,16 @@
* Input of {@link Bitmap2JpegBytes} processor.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
abstract Packet<Bitmap> getPacket();
abstract int getJpegQuality();
@NonNull
- static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+ public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
}
}
}
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
index c66c031..595ab76 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
@@ -16,6 +16,9 @@
package androidx.camera.core.imagecapture;
+import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.RAW_SENSOR;
+
import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
@@ -54,6 +57,8 @@
import com.google.auto.value.AutoValue;
+import java.util.List;
+
/**
* A {@link Node} that calls back when all the images for one capture are received.
*
@@ -79,6 +84,10 @@
@Nullable
SafeCloseImageReaderProxy mSafeCloseImageReaderProxy;
+ /* Additional image reader for simultaneous RAW + JPEG capture */
+ @Nullable
+ SafeCloseImageReaderProxy mSecondarySafeCloseImageReaderProxy;
+
@Nullable
SafeCloseImageReaderProxy mSafeCloseImageReaderForPostview;
@@ -88,6 +97,7 @@
private In mInputEdge;
@Nullable
private NoMetadataImageReader mNoMetadataImageReader = null;
+
@NonNull
@Override
public ProcessingNode.In transform(@NonNull In inputEdge) {
@@ -100,6 +110,7 @@
// Create and configure ImageReader.
Consumer<ProcessingRequest> requestConsumer;
ImageReaderProxy wrappedImageReader;
+ ImageReaderProxy secondaryWrappedImageReader = null;
boolean hasMetadata = !inputEdge.isVirtualCamera();
CameraCaptureCallback progressCallback = new CameraCaptureCallback() {
@Override
@@ -120,15 +131,36 @@
});
}
};
+
CameraCaptureCallback cameraCaptureCallbacks;
+ CameraCaptureCallback secondaryCameraCaptureCallback = null;
+ boolean isSimultaneousCaptureEnabled = inputEdge.getOutputFormats().size() > 1;
if (hasMetadata && inputEdge.getImageReaderProxyProvider() == null) {
- // Use MetadataImageReader if the input edge expects metadata.
- MetadataImageReader metadataImageReader = new MetadataImageReader(size.getWidth(),
- size.getHeight(), format, MAX_IMAGES);
- cameraCaptureCallbacks =
- CameraCaptureCallbacks.createComboCallback(
- progressCallback, metadataImageReader.getCameraCaptureCallback());
- wrappedImageReader = metadataImageReader;
+ if (isSimultaneousCaptureEnabled) {
+ MetadataImageReader metadataImageReader = new MetadataImageReader(size.getWidth(),
+ size.getHeight(), JPEG, MAX_IMAGES);
+ cameraCaptureCallbacks =
+ CameraCaptureCallbacks.createComboCallback(
+ progressCallback, metadataImageReader.getCameraCaptureCallback());
+ wrappedImageReader = metadataImageReader;
+
+ MetadataImageReader secondaryMetadataImageReader = new MetadataImageReader(
+ size.getWidth(), size.getHeight(), RAW_SENSOR, MAX_IMAGES);
+ secondaryCameraCaptureCallback =
+ CameraCaptureCallbacks.createComboCallback(
+ progressCallback,
+ secondaryMetadataImageReader.getCameraCaptureCallback());
+ secondaryWrappedImageReader = secondaryMetadataImageReader;
+ } else {
+ // Use MetadataImageReader if the input edge expects metadata.
+ MetadataImageReader metadataImageReader = new MetadataImageReader(size.getWidth(),
+ size.getHeight(), format, MAX_IMAGES);
+ cameraCaptureCallbacks =
+ CameraCaptureCallbacks.createComboCallback(
+ progressCallback, metadataImageReader.getCameraCaptureCallback());
+ wrappedImageReader = metadataImageReader;
+ }
+
requestConsumer = this::onRequestAvailable;
} else {
cameraCaptureCallbacks = progressCallback;
@@ -144,33 +176,14 @@
};
}
inputEdge.setCameraCaptureCallback(cameraCaptureCallbacks);
+ if (isSimultaneousCaptureEnabled && secondaryCameraCaptureCallback != null) {
+ inputEdge.setSecondaryCameraCaptureCallback(secondaryCameraCaptureCallback);
+ }
inputEdge.setSurface(requireNonNull(wrappedImageReader.getSurface()));
mSafeCloseImageReaderProxy = new SafeCloseImageReaderProxy(wrappedImageReader);
// Listen to the input edges.
- wrappedImageReader.setOnImageAvailableListener(imageReader -> {
- try {
- ImageProxy image = imageReader.acquireLatestImage();
- if (image != null) {
- onImageProxyAvailable(image);
- } else {
- if (mCurrentRequest != null) {
- sendCaptureError(
- TakePictureManager.CaptureError.of(mCurrentRequest.getRequestId(),
- new ImageCaptureException(ERROR_CAPTURE_FAILED,
- "Failed to acquire latest image", null)));
- }
- }
- } catch (IllegalStateException e) {
- if (mCurrentRequest != null) {
- sendCaptureError(
- TakePictureManager.CaptureError.of(mCurrentRequest.getRequestId(),
- new ImageCaptureException(
- ERROR_CAPTURE_FAILED, "Failed to acquire latest image",
- e)));
- }
- }
- }, mainThreadExecutor());
+ setOnImageAvailableListener(wrappedImageReader);
// Postview
if (inputEdge.getPostviewSize() != null) {
@@ -196,10 +209,20 @@
inputEdge.getPostviewSize(), inputEdge.getPostviewImageFormat());
}
+ // Simultaneous capture RAW + JPEG
+ if (isSimultaneousCaptureEnabled && secondaryWrappedImageReader != null) {
+ inputEdge.setSecondarySurface(secondaryWrappedImageReader.getSurface());
+ mSecondarySafeCloseImageReaderProxy = new SafeCloseImageReaderProxy(
+ secondaryWrappedImageReader);
+ setOnImageAvailableListener(secondaryWrappedImageReader);
+ }
+
inputEdge.getRequestEdge().setListener(requestConsumer);
inputEdge.getErrorEdge().setListener(this::sendCaptureError);
- mOutputEdge = ProcessingNode.In.of(inputEdge.getInputFormat(), inputEdge.getOutputFormat());
+ mOutputEdge = ProcessingNode.In.of(
+ inputEdge.getInputFormat(),
+ inputEdge.getOutputFormats());
return mOutputEdge;
}
@@ -215,6 +238,33 @@
}
}
+ private void setOnImageAvailableListener(@NonNull ImageReaderProxy imageReaderProxy) {
+ imageReaderProxy.setOnImageAvailableListener(imageReader -> {
+ try {
+ ImageProxy image = imageReader.acquireLatestImage();
+ if (image != null) {
+ onImageProxyAvailable(image);
+ } else {
+ if (mCurrentRequest != null) {
+ sendCaptureError(
+ TakePictureManager.CaptureError.of(
+ mCurrentRequest.getRequestId(),
+ new ImageCaptureException(ERROR_CAPTURE_FAILED,
+ "Failed to acquire latest image", null)));
+ }
+ }
+ } catch (IllegalStateException e) {
+ if (mCurrentRequest != null) {
+ sendCaptureError(
+ TakePictureManager.CaptureError.of(mCurrentRequest.getRequestId(),
+ new ImageCaptureException(
+ ERROR_CAPTURE_FAILED,
+ "Failed to acquire latest image", e)));
+ }
+ }
+ }, mainThreadExecutor());
+ }
+
@VisibleForTesting
@MainThread
void onImageProxyAvailable(@NonNull ImageProxy imageProxy) {
@@ -247,7 +297,22 @@
// The capture is complete. Let the pipeline know it can take another picture.
ProcessingRequest request = mCurrentRequest;
- mCurrentRequest = null;
+
+ // If simultaneous capture RAW + JPEG, only reset when both images are processed.
+ boolean isSimultaneousCaptureEnabled = mInputEdge != null
+ && mInputEdge.getOutputFormats().size() > 1;
+ if (isSimultaneousCaptureEnabled && mCurrentRequest != null) {
+ mCurrentRequest.getTakePictureRequest()
+ .markFormatProcessStatusInSimultaneousCapture(
+ imageProxy.getFormat(), true);
+ }
+ boolean isProcessed =
+ !isSimultaneousCaptureEnabled || (mCurrentRequest != null
+ && mCurrentRequest.getTakePictureRequest()
+ .isFormatProcessedInSimultaneousCapture());
+ if (isProcessed) {
+ mCurrentRequest = null;
+ }
request.onImageCaptured();
}
@@ -309,12 +374,14 @@
checkMainThread();
releaseInputResources(requireNonNull(mInputEdge),
requireNonNull(mSafeCloseImageReaderProxy),
+ mSecondarySafeCloseImageReaderProxy,
mSafeCloseImageReaderForPostview);
}
private void releaseInputResources(@NonNull CaptureNode.In inputEdge,
@NonNull SafeCloseImageReaderProxy imageReader,
+ @Nullable SafeCloseImageReaderProxy secondaryImageReader,
@Nullable SafeCloseImageReaderProxy imageReaderForPostview) {
inputEdge.getSurface().close();
// Wait for the termination to close the ImageReader or the Surface may be released
@@ -331,6 +398,15 @@
}
}, mainThreadExecutor());
}
+
+ if (inputEdge.getOutputFormats().size() > 1 && inputEdge.getSecondarySurface() != null) {
+ inputEdge.getSecondarySurface().close();
+ inputEdge.getSecondarySurface().getTerminationFuture().addListener(() -> {
+ if (secondaryImageReader != null) {
+ secondaryImageReader.safeClose();
+ }
+ }, mainThreadExecutor());
+ }
}
@VisibleForTesting
@@ -371,15 +447,24 @@
private CameraCaptureCallback mCameraCaptureCallback = new CameraCaptureCallback() {
};
+ /* Additional camera capture callback for simultaneous RAW + JPEG capture */
+ @Nullable
+ private CameraCaptureCallback mSecondaryCameraCaptureCallback;
+
@Nullable
private DeferrableSurface mSurface;
+ /* Additional surface for simultaneous RAW + JPEG capture */
+ @Nullable
+ private DeferrableSurface mSecondarySurface;
+
@Nullable
private DeferrableSurface mPostviewSurface = null;
/**
* Size of the {@link ImageReader} buffer.
*/
+ @NonNull
abstract Size getSize();
/**
@@ -390,10 +475,13 @@
/**
* The output format of the pipeline.
*
- * <p> For public users, only {@link ImageFormat#JPEG} and {@link ImageFormat#JPEG_R} are
- * supported. Other formats are only used by in-memory capture in tests.
+ * <p> For public users, {@link ImageFormat#JPEG}, {@link ImageFormat#JPEG_R} and
+ * {@link ImageFormat#RAW_SENSOR}} are supported. Other formats are only used by in-memory
+ * capture in tests.
*/
- abstract int getOutputFormat();
+ @SuppressWarnings("AutoValueImmutableFields")
+ @NonNull
+ abstract List<Integer> getOutputFormats();
/**
* Whether the pipeline is connected to a virtual camera.
@@ -449,6 +537,13 @@
return mPostviewSurface;
}
+ /**
+ * Edge that accepts the image frames for simultaneous RAW + JPEG capture.
+ */
+ @Nullable
+ DeferrableSurface getSecondarySurface() {
+ return mSecondarySurface;
+ }
void setSurface(@NonNull Surface surface) {
checkState(mSurface == null, "The surface is already set.");
@@ -459,6 +554,12 @@
mPostviewSurface = new ImmediateSurface(surface, size, imageFormat);
}
+ void setSecondarySurface(@NonNull Surface surface) {
+ checkState(mSecondarySurface == null, "The secondary surface is "
+ + "already set.");
+ mSecondarySurface = new ImmediateSurface(surface, getSize(), getInputFormat());
+ }
+
/**
* Edge that accepts image metadata.
*
@@ -473,19 +574,37 @@
mCameraCaptureCallback = cameraCaptureCallback;
}
+ @Nullable
+ CameraCaptureCallback getSecondaryCameraCaptureCallback() {
+ return mSecondaryCameraCaptureCallback;
+ }
+
+ void setSecondaryCameraCaptureCallback(
+ @NonNull CameraCaptureCallback cameraCaptureCallback) {
+ mSecondaryCameraCaptureCallback = cameraCaptureCallback;
+ }
+
@NonNull
- static In of(Size size, int inputFormat, int outputFormat, boolean isVirtualCamera,
+ static In of(
+ @NonNull Size size,
+ int inputFormat,
+ @NonNull List<Integer> outputFormats,
+ boolean isVirtualCamera,
@Nullable ImageReaderProxyProvider imageReaderProxyProvider) {
- return new AutoValue_CaptureNode_In(size, inputFormat, outputFormat, isVirtualCamera,
+ return new AutoValue_CaptureNode_In(size, inputFormat, outputFormats, isVirtualCamera,
imageReaderProxyProvider, null, ImageFormat.YUV_420_888,
new Edge<>(), new Edge<>());
}
@NonNull
- static In of(Size size, int inputFormat, int outputFormat, boolean isVirtualCamera,
+ static In of(
+ @NonNull Size size,
+ int inputFormat,
+ @NonNull List<Integer> outputFormats,
+ boolean isVirtualCamera,
@Nullable ImageReaderProxyProvider imageReaderProxyProvider,
@Nullable Size postviewSize, int postviewImageFormat) {
- return new AutoValue_CaptureNode_In(size, inputFormat, outputFormat, isVirtualCamera,
+ return new AutoValue_CaptureNode_In(size, inputFormat, outputFormats, isVirtualCamera,
imageReaderProxyProvider, postviewSize, postviewImageFormat,
new Edge<>(), new Edge<>());
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index 812be3d..124049a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -126,11 +126,20 @@
cameraCharacteristics,
cameraEffect != null ? new InternalImageProcessor(cameraEffect) : null);
+ // Pass down [RAW_SENSOR, JPEG] to the pipeline if simultaneous capture is enabled.
+ List<Integer> outputFormats = new ArrayList<>();
+ if (mUseCaseConfig.getSecondaryInputFormat() != ImageFormat.UNKNOWN) {
+ outputFormats.add(ImageFormat.RAW_SENSOR);
+ outputFormats.add(ImageFormat.JPEG);
+ } else {
+ outputFormats.add(getOutputFormat());
+ }
+
// Connect nodes
mPipelineIn = CaptureNode.In.of(
cameraSurfaceSize,
mUseCaseConfig.getInputFormat(),
- getOutputFormat(),
+ outputFormats,
isVirtualCamera,
mUseCaseConfig.getImageReaderProxyProvider(),
postviewSize,
@@ -147,6 +156,10 @@
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mUseCaseConfig,
resolution);
builder.addNonRepeatingSurface(mPipelineIn.getSurface());
+ if (mPipelineIn.getOutputFormats().size() > 1
+ && mPipelineIn.getSecondarySurface() != null) {
+ builder.addNonRepeatingSurface(mPipelineIn.getSecondarySurface());
+ }
// Postview surface is generated when initializing CaptureNode.
if (mPipelineIn.getPostviewSurface() != null) {
@@ -274,11 +287,7 @@
@NonNull ListenableFuture<Void> captureFuture) {
return new ProcessingRequest(
captureBundle,
- takePictureRequest.getOutputFileOptions(),
- takePictureRequest.getCropRect(),
- takePictureRequest.getRotationDegrees(),
- takePictureRequest.getJpegQuality(),
- takePictureRequest.getSensorToBufferTransform(),
+ takePictureRequest,
takePictureCallback,
captureFuture,
requestId);
@@ -310,6 +319,10 @@
builder.addAllCameraCaptureCallbacks(
takePictureRequest.getSessionConfigCameraCaptureCallbacks());
builder.addSurface(mPipelineIn.getSurface());
+ if (mPipelineIn.getOutputFormats().size() > 1
+ && mPipelineIn.getSecondarySurface() != null) {
+ builder.addSurface(mPipelineIn.getSecondarySurface());
+ }
builder.setPostviewEnabled(shouldEnablePostview());
// Sets the JPEG rotation and quality for JPEG and RAW formats. Some devices do not
@@ -332,6 +345,10 @@
builder.addTag(tagBundleKey, captureStage.getId());
builder.setId(requestId);
builder.addCameraCaptureCallback(mPipelineIn.getCameraCaptureCallback());
+ if (mPipelineIn.getOutputFormats().size() > 1
+ && mPipelineIn.getSecondaryCameraCaptureCallback() != null) {
+ builder.addCameraCaptureCallback(mPipelineIn.getSecondaryCameraCaptureCallback());
+ }
captureConfigs.add(builder.build());
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 26085ba..6e32225 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -25,6 +25,7 @@
import android.net.Uri;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser;
@@ -40,7 +41,9 @@
/**
* Saves JPEG bytes to disk.
*/
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+ Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
@NonNull
@Override
@@ -72,7 +75,8 @@
* Input packet.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
@NonNull
abstract Packet<byte[]> getPacket();
@@ -81,7 +85,7 @@
abstract ImageCapture.OutputFileOptions getOutputFileOptions();
@NonNull
- static In of(@NonNull Packet<byte[]> jpegBytes,
+ public static In of(@NonNull Packet<byte[]> jpegBytes,
@NonNull ImageCapture.OutputFileOptions outputFileOptions) {
return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index fddd9b7..5819ecd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -54,6 +54,7 @@
import com.google.auto.value.AutoValue;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -72,6 +73,10 @@
@Nullable
private final CameraCharacteristics mCameraCharacteristics;
+ @VisibleForTesting
+ @Nullable
+ DngImage2Disk mDngImage2Disk;
+
private ProcessingNode.In mInputEdge;
private Operation<InputPacket, Packet<ImageProxy>> mInput2Packet;
private Operation<Image2JpegBytes.In, Packet<byte[]>> mImage2JpegBytes;
@@ -189,12 +194,20 @@
void processInputPacket(@NonNull InputPacket inputPacket) {
ProcessingRequest request = inputPacket.getProcessingRequest();
try {
+ // If simultaneous capture RAW + JPEG, only trigger callback when both images
+ // are available and processed.
+ boolean isSimultaneousCaptureEnabled = mInputEdge.getOutputFormats().size() > 1;
if (inputPacket.getProcessingRequest().isInMemoryCapture()) {
ImageProxy result = processInMemoryCapture(inputPacket);
mainThreadExecutor().execute(() -> request.onFinalResult(result));
} else {
ImageCapture.OutputFileResults result = processOnDiskCapture(inputPacket);
- mainThreadExecutor().execute(() -> request.onFinalResult(result));
+ boolean isProcessed =
+ !isSimultaneousCaptureEnabled || request.getTakePictureRequest()
+ .isFormatProcessedInSimultaneousCapture();
+ if (isProcessed) {
+ mainThreadExecutor().execute(() -> request.onFinalResult(result));
+ }
}
} catch (ImageCaptureException e) {
sendError(request, e);
@@ -209,7 +222,10 @@
@WorkerThread
void processPostviewInputPacket(@NonNull InputPacket inputPacket) {
- int format = mInputEdge.getOutputFormat();
+ List<Integer> outputFormats = mInputEdge.getOutputFormats();
+ checkArgument(!outputFormats.isEmpty());
+
+ int format = outputFormats.get(0);
checkArgument(format == YUV_420_888 || format == JPEG,
String.format("Postview only support YUV and JPEG output formats. "
+ "Output format: %s", format));
@@ -228,37 +244,97 @@
@WorkerThread
ImageCapture.OutputFileResults processOnDiskCapture(@NonNull InputPacket inputPacket)
throws ImageCaptureException {
- int format = mInputEdge.getOutputFormat();
+ List<Integer> outputFormats = mInputEdge.getOutputFormats();
+ checkArgument(!outputFormats.isEmpty());
+ int format = outputFormats.get(0);
checkArgument(isJpegFormats(format)
|| isRawFormats(format),
String.format("On-disk capture only support JPEG and"
+ " JPEG/R and RAW output formats. Output format: %s", format));
ProcessingRequest request = inputPacket.getProcessingRequest();
+ checkArgument(request.getOutputFileOptions() != null
+ && !request.getOutputFileOptions().isEmpty(),
+ "OutputFileOptions cannot be empty");
Packet<ImageProxy> originalImage = mInput2Packet.apply(inputPacket);
-
- switch (format) {
- case RAW_SENSOR:
- DngImage2Disk dngImage2Disk = new DngImage2Disk(
- requireNonNull(mCameraCharacteristics),
- originalImage.getCameraCaptureResult().getCaptureResult());
- return dngImage2Disk.apply(DngImage2Disk.In.of(
- originalImage.getData(),
- originalImage.getRotationDegrees(),
- requireNonNull(request.getOutputFileOptions())));
- case JPEG:
- default:
- Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
- Image2JpegBytes.In.of(originalImage, request.getJpegQuality()));
- if (jpegBytes.hasCropping() || mBitmapEffect != null) {
- jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
- }
- return mJpegBytes2Disk.apply(
- JpegBytes2Disk.In.of(jpegBytes,
- requireNonNull(request.getOutputFileOptions())));
+ boolean isSimultaneousCaptureEnabled = outputFormats.size() > 1;
+ if (isSimultaneousCaptureEnabled) {
+ // If simultaneous capture RAW + JPEG, use the first output file options for JPEG and
+ // the second for RAW.
+ checkArgument(request.getOutputFileOptions() != null
+ && request.getOutputFileOptions().size() > 1,
+ "The number of OutputFileOptions for simultaneous capture "
+ + "should be at least two");
+ ImageCapture.OutputFileResults outputFileResults = null;
+ switch (originalImage.getFormat()) {
+ case RAW_SENSOR:
+ outputFileResults = saveRawToDisk(originalImage,
+ requireNonNull(request.getOutputFileOptions()).get(0));
+ request.getTakePictureRequest()
+ .markFormatProcessStatusInSimultaneousCapture(RAW_SENSOR, true);
+ return outputFileResults;
+ case JPEG:
+ default:
+ outputFileResults = saveJpegToDisk(originalImage,
+ requireNonNull(request.getOutputFileOptions()).get(1),
+ request.getJpegQuality());
+ request.getTakePictureRequest()
+ .markFormatProcessStatusInSimultaneousCapture(JPEG, true);
+ return outputFileResults;
+ }
+ } else {
+ switch (format) {
+ case RAW_SENSOR:
+ return saveRawToDisk(originalImage,
+ requireNonNull(request.getOutputFileOptions()).get(0));
+ case JPEG:
+ default:
+ return saveJpegToDisk(originalImage,
+ requireNonNull(request.getOutputFileOptions()).get(0),
+ request.getJpegQuality());
+ }
}
}
@NonNull
+ private ImageCapture.OutputFileResults saveRawToDisk(
+ @NonNull Packet<ImageProxy> originalImage,
+ @NonNull ImageCapture.OutputFileOptions outputFileOptions)
+ throws ImageCaptureException {
+
+ if (mDngImage2Disk == null) {
+ if (mCameraCharacteristics == null) {
+ throw new ImageCaptureException(ERROR_UNKNOWN,
+ "CameraCharacteristics is null, DngCreator cannot be created", null);
+ }
+ if (originalImage.getCameraCaptureResult().getCaptureResult() == null) {
+ throw new ImageCaptureException(ERROR_UNKNOWN,
+ "CameraCaptureResult is null, DngCreator cannot be created", null);
+ }
+ mDngImage2Disk = new DngImage2Disk(
+ requireNonNull(mCameraCharacteristics),
+ requireNonNull(originalImage.getCameraCaptureResult().getCaptureResult()));
+ }
+ return mDngImage2Disk.apply(DngImage2Disk.In.of(
+ originalImage.getData(),
+ originalImage.getRotationDegrees(),
+ requireNonNull(outputFileOptions)));
+ }
+
+ @NonNull
+ private ImageCapture.OutputFileResults saveJpegToDisk(
+ @NonNull Packet<ImageProxy> originalImage,
+ @NonNull ImageCapture.OutputFileOptions outputFileOptions,
+ int jpegQuality) throws ImageCaptureException {
+ Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
+ Image2JpegBytes.In.of(originalImage, jpegQuality));
+ if (jpegBytes.hasCropping() || mBitmapEffect != null) {
+ jpegBytes = cropAndMaybeApplyEffect(jpegBytes, jpegQuality);
+ }
+ return mJpegBytes2Disk.apply(
+ JpegBytes2Disk.In.of(jpegBytes, requireNonNull(outputFileOptions)));
+ }
+
+ @NonNull
@WorkerThread
ImageProxy processInMemoryCapture(@NonNull InputPacket inputPacket)
throws ImageCaptureException {
@@ -266,9 +342,12 @@
Packet<ImageProxy> image = mInput2Packet.apply(inputPacket);
// TODO(b/322311893): Update to handle JPEG/R as output format in the if-statement when YUV
// to JPEG/R and effect with JPEG/R are supported.
+ List<Integer> outputFormats = mInputEdge.getOutputFormats();
+ checkArgument(!outputFormats.isEmpty());
+ int format = outputFormats.get(0);
+
if ((image.getFormat() == YUV_420_888 || mBitmapEffect != null
- || mHasIncorrectJpegMetadataQuirk)
- && mInputEdge.getOutputFormat() == JPEG) {
+ || mHasIncorrectJpegMetadataQuirk) && (format == JPEG)) {
Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
Image2JpegBytes.In.of(image, request.getJpegQuality()));
if (mBitmapEffect != null) {
@@ -276,7 +355,13 @@
}
image = mJpegBytes2Image.apply(jpegBytes);
}
- return mJpegImage2Result.apply(image);
+ ImageProxy imageProxy = mJpegImage2Result.apply(image);
+ boolean isSimultaneousCaptureEnabled = outputFormats.size() > 1;
+ if (isSimultaneousCaptureEnabled) {
+ request.getTakePictureRequest()
+ .markFormatProcessStatusInSimultaneousCapture(imageProxy.getFormat(), true);
+ }
+ return imageProxy;
}
/**
@@ -297,9 +382,11 @@
/**
* Sends {@link ImageCaptureException} to {@link TakePictureManager}.
*/
- private static void sendError(@NonNull ProcessingRequest request,
+ private void sendError(@NonNull ProcessingRequest request,
@NonNull ImageCaptureException e) {
- mainThreadExecutor().execute(() -> request.onProcessFailure(e));
+ mainThreadExecutor().execute(() -> {
+ request.onProcessFailure(e);
+ });
}
/**
@@ -329,12 +416,14 @@
/**
* Get the main input edge that contains a {@link InputPacket} flow.
*/
+ @NonNull
abstract Edge<InputPacket> getEdge();
/**
* Get the postview input edge.
*/
+ @NonNull
abstract Edge<InputPacket> getPostviewEdge();
/**
@@ -345,14 +434,18 @@
/**
* The output format of the pipeline.
*
- * <p> For public users, only {@link ImageFormat#JPEG} and {@link ImageFormat#JPEG_R} are
- * supported. Other formats are only used by in-memory capture in tests.
+ * <p> For public users, {@link ImageFormat#JPEG}, {@link ImageFormat#JPEG_R} and
+ * {@link ImageFormat#RAW_SENSOR}} are supported. Other formats are only used by in-memory
+ * capture in tests.
*/
- abstract int getOutputFormat();
+ @SuppressWarnings("AutoValueImmutableFields")
+ @NonNull
+ abstract List<Integer> getOutputFormats();
- static In of(int inputFormat, int outputFormat) {
+ static In of(int inputFormat,
+ @NonNull List<Integer> outputFormats) {
return new AutoValue_ProcessingNode_In(new Edge<>(), new Edge<>(),
- inputFormat, outputFormat);
+ inputFormat, outputFormats);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingRequest.java
index 35a7fc0..bf70e64 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingRequest.java
@@ -41,8 +41,9 @@
*/
class ProcessingRequest {
private final int mRequestId;
+ @NonNull TakePictureRequest mTakePictureRequest;
@Nullable
- private final ImageCapture.OutputFileOptions mOutputFileOptions;
+ private final List<ImageCapture.OutputFileOptions> mOutputFileOptions;
@NonNull
private final Rect mCropRect;
private final int mRotationDegrees;
@@ -62,32 +63,24 @@
ProcessingRequest(
@NonNull CaptureBundle captureBundle,
- @Nullable ImageCapture.OutputFileOptions outputFileOptions,
- @NonNull Rect cropRect,
- int rotationDegrees,
- int jpegQuality,
- @NonNull Matrix sensorToBufferTransform,
+ @NonNull TakePictureRequest takePictureRequest,
@NonNull TakePictureCallback callback,
@NonNull ListenableFuture<Void> captureFuture) {
- this(captureBundle, outputFileOptions, cropRect, rotationDegrees, jpegQuality,
- sensorToBufferTransform, callback, captureFuture, 0);
+ this(captureBundle, takePictureRequest, callback, captureFuture, 0);
}
ProcessingRequest(
@NonNull CaptureBundle captureBundle,
- @Nullable ImageCapture.OutputFileOptions outputFileOptions,
- @NonNull Rect cropRect,
- int rotationDegrees,
- int jpegQuality,
- @NonNull Matrix sensorToBufferTransform,
+ @NonNull TakePictureRequest takePictureRequest,
@NonNull TakePictureCallback callback,
@NonNull ListenableFuture<Void> captureFuture,
int requestId) {
mRequestId = requestId;
- mOutputFileOptions = outputFileOptions;
- mJpegQuality = jpegQuality;
- mRotationDegrees = rotationDegrees;
- mCropRect = cropRect;
- mSensorToBufferTransform = sensorToBufferTransform;
+ mTakePictureRequest = takePictureRequest;
+ mOutputFileOptions = takePictureRequest.getOutputFileOptions();
+ mJpegQuality = takePictureRequest.getJpegQuality();
+ mRotationDegrees = takePictureRequest.getRotationDegrees();
+ mCropRect = takePictureRequest.getCropRect();
+ mSensorToBufferTransform = takePictureRequest.getSensorToBufferTransform();
mCallback = callback;
mTagBundleKey = String.valueOf(captureBundle.hashCode());
mStageIds = new ArrayList<>();
@@ -111,8 +104,13 @@
return mRequestId;
}
+ @NonNull
+ TakePictureRequest getTakePictureRequest() {
+ return mTakePictureRequest;
+ }
+
@Nullable
- ImageCapture.OutputFileOptions getOutputFileOptions() {
+ List<ImageCapture.OutputFileOptions> getOutputFileOptions() {
return mOutputFileOptions;
}
@@ -135,7 +133,7 @@
}
boolean isInMemoryCapture() {
- return getOutputFileOptions() == null;
+ return getOutputFileOptions() == null || getOutputFileOptions().isEmpty();
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
index 61dc018..a0faca3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
@@ -49,6 +49,7 @@
private final ListenableFuture<Void> mCompleteFuture;
private CallbackToFutureAdapter.Completer<Void> mCaptureCompleter;
private CallbackToFutureAdapter.Completer<Void> mCompleteCompleter;
+
// Flag tracks if the request has been aborted by the UseCase. Once aborted, this class stops
// propagating callbacks to the app.
private boolean mIsAborted = false;
@@ -282,7 +283,13 @@
}
private void markComplete() {
- checkState(!mCompleteFuture.isDone(), "The callback can only complete once.");
+ if (mTakePictureRequest.isSimultaneousCapture()
+ && !mTakePictureRequest.isFormatProcessedInSimultaneousCapture()) {
+ return;
+ }
+ if (!mTakePictureRequest.isSimultaneousCapture()) {
+ checkState(!mCompleteFuture.isDone(), "The callback can only complete once.");
+ }
mCompleteCompleter.set(null);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..120b23c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -16,6 +16,9 @@
package androidx.camera.core.imagecapture;
+import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.RAW_SENSOR;
+
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.core.util.Preconditions.checkArgument;
@@ -33,6 +36,7 @@
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.ImageOutputConfig;
@@ -41,7 +45,9 @@
import com.google.auto.value.AutoValue;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -53,6 +59,8 @@
@AutoValue
public abstract class TakePictureRequest {
+ private static final String TAG = "TakePictureRequest";
+
/**
* By default, ImageCapture does not retry requests. For some problematic devices, the
* capture request can become success after retrying. The allowed retry count will be
@@ -61,6 +69,17 @@
private int mRemainingRetires = new CaptureFailedRetryEnabler().getRetryCount();
/**
+ * Map to track image capture status for simultaneous capture.
+ *
+ * <p>For RAW + JPEG, we will only have two formats
+ * {@link android.graphics.ImageFormat#RAW_SENSOR} and {@link android.graphics.ImageFormat#JPEG}
+ * but it could extend to other formats in the future.
+ *
+ * It is not thread-safe.
+ */
+ private final Map<Integer, Boolean> mFormatCaptureStatus = new HashMap<>();
+
+ /**
* Gets the callback {@link Executor} provided by the app.
*/
@NonNull
@@ -82,14 +101,14 @@
* Gets the app provided options for on-disk capture.
*/
@Nullable
- abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+ public abstract List<ImageCapture.OutputFileOptions> getOutputFileOptions();
/**
* A snapshot of {@link ImageCapture#getViewPortCropRect()} when
* {@link ImageCapture#takePicture} is called.
*/
@NonNull
- abstract Rect getCropRect();
+ public abstract Rect getCropRect();
/**
* A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +121,14 @@
* A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
*/
@ImageOutputConfig.RotationValue
- abstract int getRotationDegrees();
+ public abstract int getRotationDegrees();
/**
* A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
* {@link ImageCapture#takePicture} is called.
*/
@IntRange(from = 1, to = 100)
- abstract int getJpegQuality();
+ public abstract int getJpegQuality();
/**
* Gets the capture mode of the request.
@@ -122,6 +141,11 @@
abstract int getCaptureMode();
/**
+ * Checks if the request is for simultaneous capture. Currently only RAW + JPEG are supported.
+ */
+ abstract boolean isSimultaneousCapture();
+
+ /**
* Gets the {@link CameraCaptureCallback}s set on the {@link SessionConfig}.
*
* <p>This is for calling back to Camera2InterOp. See: aosp/947197.
@@ -166,6 +190,36 @@
return mRemainingRetires;
}
+ void initFormatProcessStatusInSimultaneousCapture() {
+ mFormatCaptureStatus.put(RAW_SENSOR, false);
+ mFormatCaptureStatus.put(JPEG, false);
+ }
+
+ /**
+ * Marks the format as processed in simultaneous capture.
+ */
+ void markFormatProcessStatusInSimultaneousCapture(int format, boolean isProcessed) {
+ if (!mFormatCaptureStatus.containsKey(format)) {
+ Logger.e(TAG, "The format is not supported in simultaneous capture");
+ return;
+ }
+ mFormatCaptureStatus.put(format, isProcessed);
+ }
+
+ /**
+ * Checks if all the formats are processed in simultaneous capture.
+ */
+ boolean isFormatProcessedInSimultaneousCapture() {
+ boolean isProcessed = true;
+ for (Map.Entry<Integer, Boolean> entry : mFormatCaptureStatus.entrySet()) {
+ if (!entry.getValue()) {
+ isProcessed = false;
+ break;
+ }
+ }
+ return isProcessed;
+ }
+
/**
* Delivers {@link ImageCaptureException} to the app.
*/
@@ -229,20 +283,27 @@
public static TakePictureRequest of(@NonNull Executor appExecutor,
@Nullable ImageCapture.OnImageCapturedCallback inMemoryCallback,
@Nullable ImageCapture.OnImageSavedCallback onDiskCallback,
- @Nullable ImageCapture.OutputFileOptions outputFileOptions,
+ @Nullable List<ImageCapture.OutputFileOptions> outputFileOptions,
@NonNull Rect cropRect,
@NonNull Matrix sensorToBufferTransform,
int rotationDegrees,
int jpegQuality,
@ImageCapture.CaptureMode int captureMode,
+ boolean isSimultaneousCapture,
@NonNull List<CameraCaptureCallback> sessionConfigCameraCaptureCallbacks) {
checkArgument((onDiskCallback == null) == (outputFileOptions == null),
"onDiskCallback and outputFileOptions should be both null or both non-null.");
checkArgument((onDiskCallback == null) ^ (inMemoryCallback == null),
"One and only one on-disk or in-memory callback should be present.");
- return new AutoValue_TakePictureRequest(appExecutor, inMemoryCallback,
- onDiskCallback, outputFileOptions, cropRect, sensorToBufferTransform,
- rotationDegrees, jpegQuality, captureMode, sessionConfigCameraCaptureCallbacks);
+ TakePictureRequest request = new AutoValue_TakePictureRequest(appExecutor, inMemoryCallback,
+ onDiskCallback, outputFileOptions, cropRect,
+ sensorToBufferTransform, rotationDegrees, jpegQuality, captureMode,
+ isSimultaneousCapture,
+ sessionConfigCameraCaptureCallbacks);
+ if (isSimultaneousCapture) {
+ request.initFormatProcessStatusInSimultaneousCapture();
+ }
+ return request;
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageInputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageInputConfig.java
index f0c7c00..7e5ca65 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageInputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageInputConfig.java
@@ -16,6 +16,8 @@
package androidx.camera.core.impl;
+import android.graphics.ImageFormat;
+
import androidx.annotation.NonNull;
import androidx.camera.core.Camera;
import androidx.camera.core.DynamicRange;
@@ -25,6 +27,8 @@
public interface ImageInputConfig extends ReadableConfig {
Config.Option<Integer> OPTION_INPUT_FORMAT =
Config.Option.create("camerax.core.imageInput.inputFormat", int.class);
+ Config.Option<Integer> OPTION_SECONDARY_INPUT_FORMAT =
+ Config.Option.create("camerax.core.imageInput.secondaryInputFormat", int.class);
Config.Option<DynamicRange> OPTION_INPUT_DYNAMIC_RANGE =
Config.Option.create("camerax.core.imageInput.inputDynamicRange",
DynamicRange.class);
@@ -48,6 +52,19 @@
}
/**
+ * Retrieve the secondary input image format.
+ *
+ * <p>This is the format that is required for simultaneous capture. Currently only RAW + JPEG
+ * are supported and the input format must be set to RAW and secondary input format must be set
+ * to JPEG.
+ *
+ * <p>If the secondary input format is not set, {@link ImageFormat#UNKNOWN} will be returned.
+ */
+ default int getSecondaryInputFormat() {
+ return retrieveOption(OPTION_SECONDARY_INPUT_FORMAT, ImageFormat.UNKNOWN);
+ }
+
+ /**
* Retrieve the required input {@link DynamicRange}.
*
* <p>This is the dynamic range that is required as input and it must be
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
index 816367e..80fac69 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.core.imagecapture
import android.graphics.ImageFormat.JPEG
+import android.graphics.ImageFormat.RAW_SENSOR
import android.graphics.ImageFormat.YUV_420_888
import android.os.Build
import android.os.Looper.getMainLooper
@@ -57,7 +58,7 @@
@Before
fun setUp() {
- captureNodeIn = CaptureNode.In.of(Size(10, 10), JPEG, JPEG, false, null)
+ captureNodeIn = CaptureNode.In.of(Size(10, 10), JPEG, listOf(JPEG), false, null)
captureNodeOut = captureNode.transform(captureNodeIn)
captureNodeOut.edge.setListener { imagePropagated.add(it.imageProxy) }
}
@@ -68,11 +69,47 @@
}
@Test
+ fun isNotSimultaneousCapture_createOneImageReaders() {
+ // Arrange: enable isSimultaneousCaptureEnabled in CaptureNode.In
+ val input = CaptureNode.In.of(Size(10, 10), RAW_SENSOR, listOf(RAW_SENSOR), false, null)
+
+ // Act: transform.
+ val node = CaptureNode()
+ val output = node.transform(input)
+
+ // Assert
+ assertThat(output.outputFormats.size).isEqualTo(1)
+ assertThat(output.outputFormats[0]).isEqualTo(RAW_SENSOR)
+ assertThat(input.surface).isNotNull()
+ assertThat(input.cameraCaptureCallback).isNotNull()
+ assertThat(input.secondarySurface).isNull()
+ assertThat(input.secondaryCameraCaptureCallback).isNull()
+ }
+
+ @Test
+ fun isSimultaneousCapture_createTwoImageReaders() {
+ // Arrange: enable isSimultaneousCaptureEnabled in CaptureNode.In
+ val input =
+ CaptureNode.In.of(Size(10, 10), RAW_SENSOR, listOf(RAW_SENSOR, JPEG), false, null)
+
+ // Act: transform.
+ val node = CaptureNode()
+ val output = node.transform(input)
+
+ // Assert
+ assertThat(output.outputFormats.size).isEqualTo(2)
+ assertThat(input.surface).isNotNull()
+ assertThat(input.cameraCaptureCallback).isNotNull()
+ assertThat(input.secondarySurface).isNotNull()
+ assertThat(input.secondaryCameraCaptureCallback).isNotNull()
+ }
+
+ @Test
fun hasImageReaderProxyProvider_useTheProvidedImageReader() {
// Arrange: create a fake ImageReaderProxyProvider.
val imageReader = FakeImageReaderProxy(CaptureNode.MAX_IMAGES)
val imageReaderProvider = ImageReaderProxyProvider { _, _, _, _, _ -> imageReader }
- val input = CaptureNode.In.of(Size(10, 10), JPEG, JPEG, false, imageReaderProvider)
+ val input = CaptureNode.In.of(Size(10, 10), JPEG, listOf(JPEG), false, imageReaderProvider)
// Act: transform.
val node = CaptureNode()
node.transform(input)
@@ -114,7 +151,7 @@
CaptureNode.In.of(
Size(10, 10),
JPEG,
- JPEG,
+ listOf(JPEG),
/* isVirtualCamera */ true,
{ _, _, _, _, _ -> imageReaderProxy }
)
@@ -175,7 +212,7 @@
CaptureNode.In.of(
Size(10, 10),
JPEG,
- JPEG,
+ listOf(JPEG),
/* isVirtualCamera */ true,
{ _, _, _, _, _ -> imageReaderProxy }
)
@@ -206,7 +243,7 @@
CaptureNode.In.of(
Size(10, 10),
JPEG,
- JPEG,
+ listOf(JPEG),
/* isVirtualCamera */ true,
{ _, _, _, _, _ -> imageReaderProxy }
)
@@ -241,7 +278,7 @@
CaptureNode.In.of(
Size(10, 10),
JPEG,
- JPEG,
+ listOf(JPEG),
/* isVirtualCamera */ true,
{ _, _, _, _, _ -> imageReaderProxy }
)
@@ -280,7 +317,15 @@
val postviewSize = Size(640, 480)
val input =
- CaptureNode.In.of(Size(10, 10), JPEG, JPEG, false, null, postviewSize, YUV_420_888)
+ CaptureNode.In.of(
+ Size(10, 10),
+ JPEG,
+ listOf(JPEG),
+ false,
+ null,
+ postviewSize,
+ YUV_420_888
+ )
// Act: transform.
val node = CaptureNode()
@@ -298,7 +343,8 @@
// Arrange: set the postviewSize to the CaptureNode.In
val postviewSize = Size(640, 480)
- val input = CaptureNode.In.of(Size(10, 10), JPEG, JPEG, false, null, postviewSize, JPEG)
+ val input =
+ CaptureNode.In.of(Size(10, 10), JPEG, listOf(JPEG), false, null, postviewSize, JPEG)
// Act: transform.
val node = CaptureNode()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeProcessingRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeProcessingRequest.kt
index 658a6bb..7a40e02 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeProcessingRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeProcessingRequest.kt
@@ -20,6 +20,7 @@
import android.graphics.Rect
import androidx.camera.core.ImageCapture
import androidx.camera.core.imagecapture.Utils.CROP_RECT
+import androidx.camera.core.imagecapture.Utils.createTakePictureRequest
import androidx.camera.core.impl.CaptureBundle
import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
@@ -27,6 +28,7 @@
/** Fake [ProcessingRequest]. */
internal class FakeProcessingRequest(
outputFileOptions: ImageCapture.OutputFileOptions?,
+ secondaryOutputFileOptions: ImageCapture.OutputFileOptions?,
captureBundle: CaptureBundle,
cropRect: Rect,
rotationDegrees: Int,
@@ -37,11 +39,15 @@
) :
ProcessingRequest(
captureBundle,
- outputFileOptions,
- cropRect,
- rotationDegrees,
- jpegQuality,
- sensorToBufferTransform,
+ createTakePictureRequest(
+ if (outputFileOptions == null) null
+ else if (secondaryOutputFileOptions == null) listOf(outputFileOptions)
+ else listOf(outputFileOptions, secondaryOutputFileOptions),
+ cropRect,
+ sensorToBufferTransform,
+ rotationDegrees,
+ jpegQuality
+ ),
callback,
captureFuture
) {
@@ -49,5 +55,5 @@
captureBundle: CaptureBundle,
callback: TakePictureCallback,
captureFuture: ListenableFuture<Void> = CallbackToFutureAdapter.getFuture { "test" }
- ) : this(null, captureBundle, CROP_RECT, 0, 100, Matrix(), callback, captureFuture)
+ ) : this(null, null, captureBundle, CROP_RECT, 0, 100, Matrix(), callback, captureFuture)
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..7bf9c75 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -35,6 +35,7 @@
private var imageCapturedCallback: OnImageCapturedCallback? = null
private var imageSavedCallback: OnImageSavedCallback? = null
private var fileOptions: ImageCapture.OutputFileOptions? = null
+ private var secondaryFileOptions: ImageCapture.OutputFileOptions? = null
var exceptionReceived: ImageCaptureException? = null
var imageReceived: ImageProxy? = null
var fileReceived: ImageCapture.OutputFileResults? = null
@@ -108,11 +109,11 @@
imageSavedCallback = onDiskCallback
}
- override fun getOutputFileOptions(): ImageCapture.OutputFileOptions? {
- return fileOptions
+ override fun getOutputFileOptions(): List<ImageCapture.OutputFileOptions> {
+ return listOfNotNull(fileOptions, secondaryFileOptions)
}
- internal override fun getCropRect(): Rect {
+ override fun getCropRect(): Rect {
return Rect(0, 0, 640, 480)
}
@@ -120,14 +121,18 @@
return Matrix()
}
- internal override fun getRotationDegrees(): Int {
+ override fun getRotationDegrees(): Int {
return ROTATION_DEGREES
}
- internal override fun getJpegQuality(): Int {
+ override fun getJpegQuality(): Int {
return JPEG_QUALITY
}
+ override fun isSimultaneousCapture(): Boolean {
+ return false
+ }
+
internal override fun getCaptureMode(): Int {
return ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index 8ede6e5..f6a7c82 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -42,6 +42,7 @@
import androidx.camera.core.imagecapture.Utils.JPEG_QUALITY
import androidx.camera.core.imagecapture.Utils.OUTPUT_FILE_OPTIONS
import androidx.camera.core.imagecapture.Utils.ROTATION_DEGREES
+import androidx.camera.core.imagecapture.Utils.SECONDARY_OUTPUT_FILE_OPTIONS
import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
import androidx.camera.core.imagecapture.Utils.SIZE
import androidx.camera.core.imagecapture.Utils.WIDTH
@@ -66,6 +67,7 @@
import androidx.camera.testing.impl.fakes.FakeImageReaderProxy
import androidx.camera.testing.impl.fakes.GrayscaleImageEffect
import androidx.core.util.Pair
+import com.google.common.collect.ImmutableList
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -198,7 +200,7 @@
}
@Test
- fun createRequests_verifyCameraRequest_whenFormatIsRAw() {
+ fun createRequests_verifyCameraRequest_whenFormatIsRaw() {
// Arrange.
imageCaptureConfig = createImageCaptureConfig(inputFormat = ImageFormat.RAW_SENSOR)
imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
@@ -213,6 +215,25 @@
}
@Test
+ fun createRequests_verifyCameraRequest_whenFormatIsRawAndSimultaneousCaptureEnabled() {
+ // Arrange.
+ imageCaptureConfig =
+ createImageCaptureConfig(
+ inputFormat = ImageFormat.RAW_SENSOR,
+ secondaryInputFormat = ImageFormat.JPEG
+ )
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
+ val captureInput = imagePipeline.captureNode.inputEdge
+
+ // Act: create requests
+ val result =
+ imagePipeline.createRequests(IN_MEMORY_REQUEST, CALLBACK, Futures.immediateFuture(null))
+
+ // Assert: CameraRequest is constructed correctly.
+ verifyCaptureRequest(captureInput, result, true)
+ }
+
+ @Test
fun createCameraRequestWithRotationQuirk_rotationNotInCaptureConfig() {
// Arrange.
injectRotationOptionQuirk()
@@ -352,12 +373,13 @@
override fun onError(exception: ImageCaptureException) {}
},
- OUTPUT_FILE_OPTIONS,
+ ImmutableList.of(OUTPUT_FILE_OPTIONS, SECONDARY_OUTPUT_FILE_OPTIONS),
cropRect,
SENSOR_TO_BUFFER,
ROTATION_DEGREES,
JPEG_QUALITY,
captureMode,
+ false,
listOf()
)
@@ -439,6 +461,22 @@
assertThat(CALLBACK.inMemoryResult!!.planes).isEqualTo(image.planes)
}
+ @Test
+ fun sendInMemoryRequest_receivesImageProxy_whenSimultaneousCaptureEnabled() {
+ // Arrange & act.
+ imageCaptureConfig =
+ createImageCaptureConfig(
+ inputFormat = ImageFormat.RAW_SENSOR,
+ secondaryInputFormat = ImageFormat.JPEG
+ )
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
+ val image = sendInMemoryRequest(imagePipeline)
+
+ // Assert: the image is received by TakePictureCallback.
+ assertThat(image.format).isEqualTo(ImageFormat.JPEG)
+ assertThat(CALLBACK.inMemoryResult).isNotNull()
+ }
+
/** Creates a ImageProxy and sends it to the pipeline. */
private fun sendInMemoryRequest(
pipeline: ImagePipeline,
@@ -525,6 +563,7 @@
private fun createImageCaptureConfig(
templateType: Int = TEMPLATE_TYPE,
inputFormat: Int = ImageFormat.JPEG,
+ secondaryInputFormat: Int? = null,
): ImageCaptureConfig {
val builder =
ImageCapture.Builder().setCaptureOptionUnpacker { _, builder ->
@@ -532,6 +571,12 @@
}
builder.mutableConfig.insertOption(OPTION_IO_EXECUTOR, mainThreadExecutor())
builder.mutableConfig.insertOption(ImageInputConfig.OPTION_INPUT_FORMAT, inputFormat)
+ if (secondaryInputFormat != null) {
+ builder.mutableConfig.insertOption(
+ ImageInputConfig.OPTION_SECONDARY_INPUT_FORMAT,
+ secondaryInputFormat
+ )
+ }
if (Build.VERSION.SDK_INT >= 34 && inputFormat == ImageFormat.JPEG_R) {
builder.mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, HLG_10_BIT)
}
@@ -541,13 +586,23 @@
private fun verifyCaptureRequest(
captureInput: CaptureNode.In,
- result: Pair<CameraRequest, ProcessingRequest>
+ result: Pair<CameraRequest, ProcessingRequest>,
+ isSimultaneousCaptureEnabled: Boolean = false
) {
val cameraRequest = result.first!!
val captureConfig = cameraRequest.captureConfigs.single()
- assertThat(captureConfig.cameraCaptureCallbacks)
- .containsExactly(captureInput.cameraCaptureCallback)
- assertThat(captureConfig.surfaces).containsExactly(captureInput.surface)
+ if (isSimultaneousCaptureEnabled) {
+ assertThat(captureConfig.cameraCaptureCallbacks)
+ .contains(captureInput.cameraCaptureCallback)
+ assertThat(captureConfig.cameraCaptureCallbacks)
+ .contains(captureInput.secondaryCameraCaptureCallback)
+ assertThat(captureConfig.surfaces).contains(captureInput.surface)
+ assertThat(captureConfig.surfaces).contains(captureInput.secondarySurface)
+ } else {
+ assertThat(captureConfig.cameraCaptureCallbacks)
+ .containsExactly(captureInput.cameraCaptureCallback)
+ assertThat(captureConfig.surfaces).containsExactly(captureInput.surface)
+ }
assertThat(captureConfig.templateType).isEqualTo(TEMPLATE_TYPE)
val jpegQuality = captureConfig.implementationOptions.retrieveOption(OPTION_JPEG_QUALITY)
assertThat(jpegQuality).isEqualTo(JPEG_QUALITY)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingInput2PacketTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingInput2PacketTest.kt
index 88695c5..d059a5a 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingInput2PacketTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingInput2PacketTest.kt
@@ -30,6 +30,7 @@
import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
import androidx.camera.core.imagecapture.Utils.WIDTH
import androidx.camera.core.imagecapture.Utils.createProcessingRequest
+import androidx.camera.core.imagecapture.Utils.createTakePictureRequest
import androidx.camera.core.imagecapture.Utils.injectRotationOptionQuirk
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.core.internal.CameraCaptureResultImageInfo
@@ -137,11 +138,13 @@
val processingRequest =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- CROP_RECT,
- 90,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ CROP_RECT,
+ SENSOR_TO_BUFFER,
+ /*rotationDegrees=*/ 90,
+ /*jpegQuality=*/ 100
+ ),
FakeTakePictureCallback(),
Futures.immediateFuture(null)
)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
index b6ab8c8..7653d47 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
@@ -16,19 +16,23 @@
package androidx.camera.core.imagecapture
-import android.graphics.ImageFormat
+import android.graphics.ImageFormat.JPEG
+import android.graphics.ImageFormat.RAW_SENSOR
import android.graphics.Rect
import android.hardware.camera2.CameraCharacteristics
import android.os.Build
import android.os.Looper.getMainLooper
+import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.imagecapture.Utils.CAMERA_CAPTURE_RESULT
import androidx.camera.core.imagecapture.Utils.HEIGHT
import androidx.camera.core.imagecapture.Utils.OUTPUT_FILE_OPTIONS
import androidx.camera.core.imagecapture.Utils.ROTATION_DEGREES
+import androidx.camera.core.imagecapture.Utils.SECONDARY_OUTPUT_FILE_OPTIONS
import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
import androidx.camera.core.imagecapture.Utils.WIDTH
import androidx.camera.core.imagecapture.Utils.createProcessingRequest
+import androidx.camera.core.imagecapture.Utils.createTakePictureRequest
import androidx.camera.core.impl.utils.executor.CameraXExecutors.isSequentialExecutor
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.impl.utils.futures.Futures
@@ -38,13 +42,16 @@
import androidx.camera.testing.impl.TestImageUtil.createJpegFakeImageProxy
import androidx.camera.testing.impl.TestImageUtil.createJpegrBytes
import androidx.camera.testing.impl.TestImageUtil.createJpegrFakeImageProxy
+import androidx.camera.testing.impl.TestImageUtil.createRawFakeImageProxy
import androidx.camera.testing.impl.fakes.FakeImageInfo
import androidx.camera.testing.impl.fakes.FakeImageProxy
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.any
import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@@ -65,7 +72,7 @@
@Before
fun setUp() {
- processingNodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ processingNodeIn = ProcessingNode.In.of(JPEG, listOf(JPEG))
node.transform(processingNodeIn)
}
@@ -76,11 +83,13 @@
val request =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- Rect(0, 0, WIDTH, HEIGHT),
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
callback,
Futures.immediateFuture(null)
)
@@ -103,11 +112,13 @@
val request =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- Rect(0, 0, WIDTH, HEIGHT),
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
callback,
Futures.immediateFuture(null)
)
@@ -123,6 +134,94 @@
}
@Test
+ fun processRequest_hasDiskResult_whenFormatIsRaw() {
+ // Arrange: create a request with callback.
+ processingNodeIn = ProcessingNode.In.of(RAW_SENSOR, listOf(RAW_SENSOR))
+ node.transform(processingNodeIn)
+
+ val callback = FakeTakePictureCallback()
+ val request =
+ ProcessingRequest(
+ { listOf() },
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
+ callback,
+ Futures.immediateFuture(null)
+ )
+
+ // Act: process the request.
+ val rawImage =
+ createRawFakeImageProxy(
+ CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
+ WIDTH,
+ HEIGHT
+ )
+ val dngImage2Disk = mock(DngImage2Disk::class.java)
+ node.mDngImage2Disk = dngImage2Disk
+ `when`(dngImage2Disk.apply(any(DngImage2Disk.In::class.java)))
+ .thenReturn(mock(ImageCapture.OutputFileResults::class.java))
+ processingNodeIn.edge.accept(ProcessingNode.InputPacket.of(request, rawImage))
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the image is saved.
+ assertThat(callback.onDiskResult).isNotNull()
+ }
+
+ @Test
+ fun processRequest_hasDiskResult_whenSimultaneousCaptureEnabled() {
+ // Arrange: create a request with callback.
+ processingNodeIn = ProcessingNode.In.of(RAW_SENSOR, listOf(RAW_SENSOR, JPEG))
+ node.transform(processingNodeIn)
+
+ val callback = FakeTakePictureCallback()
+ val request =
+ ProcessingRequest(
+ { listOf() },
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS, SECONDARY_OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100,
+ isSimultaneousCapture = true
+ ),
+ callback,
+ Futures.immediateFuture(null)
+ )
+
+ // Act: process the request.
+ val jpegBytes = createJpegBytes(WIDTH, HEIGHT)
+ val jpegImage = createJpegFakeImageProxy(jpegBytes)
+ processingNodeIn.edge.accept(ProcessingNode.InputPacket.of(request, jpegImage))
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the image is saved.
+ assertThat(callback.onDiskResult).isNull()
+
+ // Act: process the request.
+ val rawImage =
+ createRawFakeImageProxy(
+ CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
+ WIDTH,
+ HEIGHT
+ )
+ val dngImage2Disk = mock(DngImage2Disk::class.java)
+ node.mDngImage2Disk = dngImage2Disk
+ `when`(dngImage2Disk.apply(any(DngImage2Disk.In::class.java)))
+ .thenReturn(mock(ImageCapture.OutputFileResults::class.java))
+ processingNodeIn.edge.accept(ProcessingNode.InputPacket.of(request, rawImage))
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the image is saved.
+ assertThat(callback.onDiskResult).isNotNull()
+ }
+
+ @Test
fun processAbortedRequest_noOps() {
// Arrange: create a request with aborted callback.
val callback = FakeTakePictureCallback()
@@ -130,11 +229,13 @@
val request =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- Rect(0, 0, WIDTH, HEIGHT),
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
callback,
Futures.immediateFuture(null)
)
@@ -156,11 +257,13 @@
val request =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- Rect(0, 0, WIDTH, HEIGHT),
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
callback,
Futures.immediateFuture(null)
)
@@ -183,11 +286,13 @@
val request =
ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
- Rect(0, 0, WIDTH, HEIGHT),
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ Rect(0, 0, WIDTH, HEIGHT),
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ /*jpegQuality=*/ 100
+ ),
callback,
Futures.immediateFuture(null)
)
@@ -241,7 +346,7 @@
// Creates the ProcessingNode after updating the device name to load the correct quirks
node = ProcessingNode(mainThreadExecutor(), cameraCharacteristics)
- processingNodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
+ processingNodeIn = ProcessingNode.In.of(JPEG, listOf(JPEG))
node.transform(processingNodeIn)
// Arrange: create an invalid ImageProxy.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/Utils.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/Utils.kt
index 2a8bdd1..3305a82 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/Utils.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/Utils.kt
@@ -30,13 +30,15 @@
import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageInputConfig
import androidx.camera.core.impl.TagBundle
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import androidx.camera.testing.impl.fakes.FakeCaptureStage
import androidx.camera.testing.impl.fakes.FakeImageProxy
import java.io.File
import java.util.UUID
+import org.mockito.Mockito.mock
import org.robolectric.util.ReflectionHelpers.setStaticField
/** Utility methods for testing image capture. */
@@ -59,6 +61,8 @@
it.deleteOnExit()
}
internal val OUTPUT_FILE_OPTIONS = ImageCapture.OutputFileOptions.Builder(TEMP_FILE).build()
+ internal val SECONDARY_OUTPUT_FILE_OPTIONS =
+ ImageCapture.OutputFileOptions.Builder(TEMP_FILE).build()
internal val CAMERA_CAPTURE_RESULT = FakeCameraCaptureResult().also { it.timestamp = TIMESTAMP }
internal fun createProcessingRequest(
@@ -67,11 +71,13 @@
): ProcessingRequest {
return ProcessingRequest(
captureBundle,
- OUTPUT_FILE_OPTIONS,
- CROP_RECT,
- ROTATION_DEGREES,
- /*jpegQuality=*/ 100,
- SENSOR_TO_BUFFER,
+ createTakePictureRequest(
+ listOf(OUTPUT_FILE_OPTIONS),
+ CROP_RECT,
+ SENSOR_TO_BUFFER,
+ ROTATION_DEGREES,
+ JPEG_QUALITY
+ ),
takePictureCallback,
Futures.immediateFuture(null)
)
@@ -101,8 +107,39 @@
fun createCameraCaptureResultImageInfo(tagBundleKey: String, stageId: Int): ImageInfo {
return CameraCaptureResultImageInfo(
FakeCameraCaptureResult().also {
- it.setTag(TagBundle.create(Pair(tagBundleKey, stageId)))
+ it.setTagBundle(TagBundle.create(Pair(tagBundleKey, stageId)))
}
)
}
+
+ fun createTakePictureRequest(
+ outputFileOptions: List<ImageCapture.OutputFileOptions>?,
+ cropRect: Rect,
+ sensorToBufferTransform: Matrix,
+ rotationDegrees: Int,
+ jpegQuality: Int,
+ isSimultaneousCapture: Boolean = false
+ ): TakePictureRequest {
+ var onDiskCallback: ImageCapture.OnImageSavedCallback? = null
+ var onMemoryCallback: ImageCapture.OnImageCapturedCallback? = null
+ if (outputFileOptions == null) {
+ onMemoryCallback = mock(ImageCapture.OnImageCapturedCallback::class.java)
+ } else {
+ onDiskCallback = mock(ImageCapture.OnImageSavedCallback::class.java)
+ }
+
+ return TakePictureRequest.of(
+ CameraXExecutors.mainThreadExecutor(),
+ onMemoryCallback,
+ onDiskCallback,
+ outputFileOptions,
+ cropRect,
+ sensorToBufferTransform,
+ rotationDegrees,
+ jpegQuality,
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ isSimultaneousCapture,
+ listOf()
+ )
+ }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraCaptureResultImageInfoTest.java b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraCaptureResultImageInfoTest.java
index df3a7de..49518c6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraCaptureResultImageInfoTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraCaptureResultImageInfoTest.java
@@ -21,7 +21,7 @@
import android.os.Build;
import androidx.camera.core.ImageInfo;
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult;
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/PacketTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/PacketTest.kt
index dc83aae..af65491 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/PacketTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/PacketTest.kt
@@ -22,9 +22,9 @@
import android.graphics.Rect
import android.os.Build
import android.util.Size
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import androidx.camera.testing.impl.ExifUtil.createExif
import androidx.camera.testing.impl.TestImageUtil.createJpegBytes
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index f6211aa..f490f6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -66,8 +66,8 @@
import androidx.camera.core.processing.DefaultSurfaceProcessor
import androidx.camera.core.processing.SurfaceProcessorWithExecutor
import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import androidx.camera.testing.fakes.FakeCameraInfoInternal
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
import androidx.camera.testing.impl.fakes.FakeSurfaceEffect
import androidx.camera.testing.impl.fakes.FakeSurfaceProcessorInternal
import androidx.camera.testing.impl.fakes.FakeUseCase
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResultTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResultTest.kt
index 0eb4320..1b17534 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResultTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResultTest.kt
@@ -20,7 +20,7 @@
import android.util.Pair
import androidx.camera.core.impl.CameraCaptureMetaData
import androidx.camera.core.impl.TagBundle
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..a55b2f7 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -37,6 +37,40 @@
method public void setHasTransform(boolean);
}
+ public final class FakeCameraCaptureResult {
+ ctor public FakeCameraCaptureResult();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AeMode getAeMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AeState getAeState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AfMode getAfMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AfState getAfState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AwbMode getAwbMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AwbState getAwbState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.FlashState getFlashState();
+ method public androidx.camera.core.impl.TagBundle getTagBundle();
+ method public long getTimestamp();
+ method public void setAeMode(androidx.camera.core.impl.CameraCaptureMetaData.AeMode);
+ method public void setAeState(androidx.camera.core.impl.CameraCaptureMetaData.AeState);
+ method public void setAfMode(androidx.camera.core.impl.CameraCaptureMetaData.AfMode);
+ method public void setAfState(androidx.camera.core.impl.CameraCaptureMetaData.AfState);
+ method public void setAwbMode(androidx.camera.core.impl.CameraCaptureMetaData.AwbMode);
+ method public void setAwbState(androidx.camera.core.impl.CameraCaptureMetaData.AwbState);
+ method public void setFlashState(androidx.camera.core.impl.CameraCaptureMetaData.FlashState);
+ method public void setTagBundle(androidx.camera.core.impl.TagBundle);
+ method public void setTimestamp(long);
+ }
+
+ public static final class FakeCameraCaptureResult.Builder {
+ ctor public FakeCameraCaptureResult.Builder();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult build();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAeState(androidx.camera.core.impl.CameraCaptureMetaData.AeState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAfMode(androidx.camera.core.impl.CameraCaptureMetaData.AfMode?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAfState(androidx.camera.core.impl.CameraCaptureMetaData.AfState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAwbState(androidx.camera.core.impl.CameraCaptureMetaData.AwbState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setFlashState(androidx.camera.core.impl.CameraCaptureMetaData.FlashState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setTagBundle(androidx.camera.core.impl.TagBundle);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setTimestamp(long);
+ }
+
public final class FakeCameraControl implements androidx.camera.core.CameraControl {
ctor public FakeCameraControl();
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
@@ -46,6 +80,7 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
method public void clearNewCaptureRequestListener();
+ method public void completeAllCaptureRequests(androidx.camera.testing.imagecapture.CaptureResult);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -59,9 +94,9 @@
method public float getZoomRatio();
method public boolean isZslConfigAdded();
method public boolean isZslDisabledByByUserCaseConfig();
- method public void notifyAllRequestsOnCaptureCancelled();
- method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
- method public void notifyAllRequestsOnCaptureFailed();
+ method @Deprecated public void notifyAllRequestsOnCaptureCancelled();
+ method @Deprecated public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method @Deprecated public void notifyAllRequestsOnCaptureFailed();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
@@ -71,6 +106,7 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ method public void submitCaptureResult(androidx.camera.testing.imagecapture.CaptureResult);
method public com.google.common.util.concurrent.ListenableFuture<java.util.List<java.lang.Void!>!> submitStillCaptureRequests(java.util.List<androidx.camera.core.impl.CaptureConfig!>, int, int);
field public static final androidx.camera.core.impl.CameraControlInternal DEFAULT_EMPTY_INSTANCE;
}
@@ -122,3 +158,29 @@
}
+package androidx.camera.testing.imagecapture {
+
+ public final class CaptureResult {
+ method public static androidx.camera.testing.imagecapture.CaptureResult cancelledResult();
+ method public static androidx.camera.testing.imagecapture.CaptureResult failedResult();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult? getCameraCaptureResult();
+ method public int getCaptureStatus();
+ method public static androidx.camera.testing.imagecapture.CaptureResult successfulResult();
+ method public static androidx.camera.testing.imagecapture.CaptureResult successfulResult(optional androidx.camera.testing.fakes.FakeCameraCaptureResult fakeCameraCaptureResult);
+ property public final androidx.camera.testing.fakes.FakeCameraCaptureResult? cameraCaptureResult;
+ property public final int captureStatus;
+ field public static final int CAPTURE_STATUS_CANCELLED = 2; // 0x2
+ field public static final int CAPTURE_STATUS_FAILED = 1; // 0x1
+ field public static final int CAPTURE_STATUS_SUCCESSFUL = 0; // 0x0
+ field public static final androidx.camera.testing.imagecapture.CaptureResult.Companion Companion;
+ }
+
+ public static final class CaptureResult.Companion {
+ method public androidx.camera.testing.imagecapture.CaptureResult cancelledResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult failedResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult successfulResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult successfulResult(optional androidx.camera.testing.fakes.FakeCameraCaptureResult fakeCameraCaptureResult);
+ }
+
+}
+
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..a55b2f7 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -37,6 +37,40 @@
method public void setHasTransform(boolean);
}
+ public final class FakeCameraCaptureResult {
+ ctor public FakeCameraCaptureResult();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AeMode getAeMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AeState getAeState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AfMode getAfMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AfState getAfState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AwbMode getAwbMode();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.AwbState getAwbState();
+ method public androidx.camera.core.impl.CameraCaptureMetaData.FlashState getFlashState();
+ method public androidx.camera.core.impl.TagBundle getTagBundle();
+ method public long getTimestamp();
+ method public void setAeMode(androidx.camera.core.impl.CameraCaptureMetaData.AeMode);
+ method public void setAeState(androidx.camera.core.impl.CameraCaptureMetaData.AeState);
+ method public void setAfMode(androidx.camera.core.impl.CameraCaptureMetaData.AfMode);
+ method public void setAfState(androidx.camera.core.impl.CameraCaptureMetaData.AfState);
+ method public void setAwbMode(androidx.camera.core.impl.CameraCaptureMetaData.AwbMode);
+ method public void setAwbState(androidx.camera.core.impl.CameraCaptureMetaData.AwbState);
+ method public void setFlashState(androidx.camera.core.impl.CameraCaptureMetaData.FlashState);
+ method public void setTagBundle(androidx.camera.core.impl.TagBundle);
+ method public void setTimestamp(long);
+ }
+
+ public static final class FakeCameraCaptureResult.Builder {
+ ctor public FakeCameraCaptureResult.Builder();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult build();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAeState(androidx.camera.core.impl.CameraCaptureMetaData.AeState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAfMode(androidx.camera.core.impl.CameraCaptureMetaData.AfMode?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAfState(androidx.camera.core.impl.CameraCaptureMetaData.AfState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setAwbState(androidx.camera.core.impl.CameraCaptureMetaData.AwbState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setFlashState(androidx.camera.core.impl.CameraCaptureMetaData.FlashState?);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setTagBundle(androidx.camera.core.impl.TagBundle);
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult.Builder setTimestamp(long);
+ }
+
public final class FakeCameraControl implements androidx.camera.core.CameraControl {
ctor public FakeCameraControl();
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
@@ -46,6 +80,7 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
method public void clearNewCaptureRequestListener();
+ method public void completeAllCaptureRequests(androidx.camera.testing.imagecapture.CaptureResult);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -59,9 +94,9 @@
method public float getZoomRatio();
method public boolean isZslConfigAdded();
method public boolean isZslDisabledByByUserCaseConfig();
- method public void notifyAllRequestsOnCaptureCancelled();
- method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
- method public void notifyAllRequestsOnCaptureFailed();
+ method @Deprecated public void notifyAllRequestsOnCaptureCancelled();
+ method @Deprecated public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method @Deprecated public void notifyAllRequestsOnCaptureFailed();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
@@ -71,6 +106,7 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ method public void submitCaptureResult(androidx.camera.testing.imagecapture.CaptureResult);
method public com.google.common.util.concurrent.ListenableFuture<java.util.List<java.lang.Void!>!> submitStillCaptureRequests(java.util.List<androidx.camera.core.impl.CaptureConfig!>, int, int);
field public static final androidx.camera.core.impl.CameraControlInternal DEFAULT_EMPTY_INSTANCE;
}
@@ -122,3 +158,29 @@
}
+package androidx.camera.testing.imagecapture {
+
+ public final class CaptureResult {
+ method public static androidx.camera.testing.imagecapture.CaptureResult cancelledResult();
+ method public static androidx.camera.testing.imagecapture.CaptureResult failedResult();
+ method public androidx.camera.testing.fakes.FakeCameraCaptureResult? getCameraCaptureResult();
+ method public int getCaptureStatus();
+ method public static androidx.camera.testing.imagecapture.CaptureResult successfulResult();
+ method public static androidx.camera.testing.imagecapture.CaptureResult successfulResult(optional androidx.camera.testing.fakes.FakeCameraCaptureResult fakeCameraCaptureResult);
+ property public final androidx.camera.testing.fakes.FakeCameraCaptureResult? cameraCaptureResult;
+ property public final int captureStatus;
+ field public static final int CAPTURE_STATUS_CANCELLED = 2; // 0x2
+ field public static final int CAPTURE_STATUS_FAILED = 1; // 0x1
+ field public static final int CAPTURE_STATUS_SUCCESSFUL = 0; // 0x0
+ field public static final androidx.camera.testing.imagecapture.CaptureResult.Companion Companion;
+ }
+
+ public static final class CaptureResult.Companion {
+ method public androidx.camera.testing.imagecapture.CaptureResult cancelledResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult failedResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult successfulResult();
+ method public androidx.camera.testing.imagecapture.CaptureResult successfulResult(optional androidx.camera.testing.fakes.FakeCameraCaptureResult fakeCameraCaptureResult);
+ }
+
+}
+
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index 3b5a46e..41b7885 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -39,21 +39,24 @@
implementation("androidx.test:rules:$testRulesVersion")
implementation("androidx.test:runner:$testRunnerVersion")
- implementation(libs.testUiautomator)
api("androidx.annotation:annotation:1.8.1")
- implementation(libs.guavaListenableFuture)
- implementation("androidx.appcompat:appcompat:1.1.0")
api(project(":camera:camera-core"))
api(project(":camera:camera-lifecycle"))
api(project(":camera:camera-video"))
+
+ implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.core:core:1.1.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.test.espresso:espresso-core:3.5.0")
implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
+
+ implementation(libs.atomicFu)
+ implementation(libs.guavaListenableFuture)
implementation(libs.junit)
implementation(libs.truth)
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesAndroid)
+ implementation(libs.testUiautomator)
testImplementation("androidx.test:core:$testCoreVersion")
testImplementation("androidx.test:runner:$testRunnerVersion")
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 41958c4..8135b3f 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
@@ -29,6 +29,9 @@
import androidx.camera.testing.impl.fakes.FakeCameraFactory;
import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Convenience class for generating a fake {@link CameraXConfig}.
*
@@ -59,20 +62,10 @@
*/
@NonNull
public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
final CameraFactory.Provider cameraFactoryProvider =
- (ignored1, ignored2, ignored3, ignore4) -> {
- final FakeCameraFactory cameraFactory = new FakeCameraFactory(
- availableCamerasSelector);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
- DEFAULT_BACK_CAMERA_ID,
- FakeAppConfig::getBackCamera);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
- DEFAULT_FRONT_CAMERA_ID,
- FakeAppConfig::getFrontCamera);
- final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
- cameraFactory.setCameraCoordinator(cameraCoordinator);
- return cameraFactory;
- };
+ (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
(ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
@@ -80,7 +73,14 @@
final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
.setCameraFactoryProvider(cameraFactoryProvider)
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
- .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+ .setUseCaseConfigFactoryProvider(ignored -> {
+ List<FakeCamera> fakeCameras = new ArrayList<>();
+ for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+ fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+ }
+
+ return new FakeUseCaseConfigFactory(fakeCameras);
+ });
if (availableCamerasSelector != null) {
appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +89,21 @@
return appConfigBuilder.build();
}
+ private static FakeCameraFactory createCameraFactory(
+ @Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+ cameraFactory.insertCamera(
+ CameraSelector.LENS_FACING_BACK,
+ DEFAULT_BACK_CAMERA_ID,
+ FakeAppConfig::getBackCamera);
+ cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+ DEFAULT_FRONT_CAMERA_ID,
+ FakeAppConfig::getFrontCamera);
+ final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+ cameraFactory.setCameraCoordinator(cameraCoordinator);
+ return cameraFactory;
+ }
+
/**
* Returns the default fake back camera that is used internally by CameraX.
*/
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraCaptureResult.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java
similarity index 86%
rename from camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraCaptureResult.java
rename to camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java
index 8be8b5d5..11884ff 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraCaptureResult.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,21 +14,17 @@
* limitations under the License.
*/
-package androidx.camera.testing.impl.fakes;
+package androidx.camera.testing.fakes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.impl.CameraCaptureMetaData;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.TagBundle;
/**
* A fake implementation of {@link CameraCaptureResult} where the values are settable.
- *
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
public final class FakeCameraCaptureResult implements CameraCaptureResult {
private CameraCaptureMetaData.AfMode mAfMode = CameraCaptureMetaData.AfMode.UNKNOWN;
private CameraCaptureMetaData.AfState mAfState = CameraCaptureMetaData.AfState.UNKNOWN;
@@ -72,7 +68,7 @@
mTimestamp = timestamp;
}
- public void setTag(@NonNull TagBundle tag) {
+ public void setTagBundle(@NonNull TagBundle tag) {
mTag = tag;
}
@@ -134,8 +130,7 @@
*
*/
@SuppressWarnings("unused")
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static class Builder {
+ public static final class Builder {
private CameraCaptureMetaData.AfMode mAfMode = CameraCaptureMetaData.AfMode.UNKNOWN;
private CameraCaptureMetaData.AfState mAfState = CameraCaptureMetaData.AfState.UNKNOWN;
private CameraCaptureMetaData.AeState mAeState = CameraCaptureMetaData.AeState.UNKNOWN;
@@ -145,7 +140,7 @@
private long mTimestamp = -1L;
private TagBundle mTag = TagBundle.emptyBundle();
- /** Construct and return a new instance of a {@link FakeCameraCaptureResult}. */
+ /** Constructs and returns a new instance of a {@link FakeCameraCaptureResult}. */
@NonNull public FakeCameraCaptureResult build() {
FakeCameraCaptureResult fakeCameraCaptureResult = new FakeCameraCaptureResult();
fakeCameraCaptureResult.setAfMode(mAfMode);
@@ -154,56 +149,56 @@
fakeCameraCaptureResult.setAwbState(mAwbState);
fakeCameraCaptureResult.setFlashState(mFlashState);
fakeCameraCaptureResult.setTimestamp(mTimestamp);
- fakeCameraCaptureResult.setTag(mTag);
+ fakeCameraCaptureResult.setTagBundle(mTag);
return fakeCameraCaptureResult;
}
- /** Set the {@link CameraCaptureMetaData.AfMode} */
+ /** Sets the {@link CameraCaptureMetaData.AfMode} */
@NonNull
public Builder setAfMode(@Nullable CameraCaptureMetaData.AfMode mode) {
mAfMode = mode;
return this;
}
- /** Set the {@link CameraCaptureMetaData.AfState} */
+ /** Sets the {@link CameraCaptureMetaData.AfState} */
@NonNull
public Builder setAfState(@Nullable CameraCaptureMetaData.AfState state) {
mAfState = state;
return this;
}
- /** Set the {@link CameraCaptureMetaData.AeState} */
+ /** Sets the {@link CameraCaptureMetaData.AeState} */
@NonNull
public Builder setAeState(@Nullable CameraCaptureMetaData.AeState state) {
mAeState = state;
return this;
}
- /** Set the {@link CameraCaptureMetaData.AwbState} */
+ /** Sets the {@link CameraCaptureMetaData.AwbState} */
@NonNull
public Builder setAwbState(@Nullable CameraCaptureMetaData.AwbState state) {
mAwbState = state;
return this;
}
- /** Set the {@link CameraCaptureMetaData.FlashState} */
+ /** Sets the {@link CameraCaptureMetaData.FlashState} */
@NonNull
public Builder setFlashState(@Nullable CameraCaptureMetaData.FlashState state) {
mFlashState = state;
return this;
}
- /** Set the timestamp. */
+ /** Sets the timestamp. */
@NonNull
public Builder setTimestamp(long timestamp) {
mTimestamp = timestamp;
return this;
}
- /** Set the tag. */
+ /** Sets the {@link TagBundle}. */
@NonNull
- public Builder setTag(@NonNull TagBundle tag) {
+ public Builder setTagBundle(@NonNull TagBundle tag) {
mTag = tag;
return this;
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index fb34d5d..656ff62 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -17,10 +17,16 @@
package androidx.camera.testing.fakes;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
+import static androidx.camera.testing.imagecapture.CaptureResult.CAPTURE_STATUS_CANCELLED;
+import static androidx.camera.testing.imagecapture.CaptureResult.CAPTURE_STATUS_FAILED;
+import static androidx.camera.testing.imagecapture.CaptureResult.CAPTURE_STATUS_SUCCESSFUL;
import static androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager.MAX_OUTPUT_SIZE;
+import static java.util.Objects.requireNonNull;
+
import android.graphics.Rect;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -41,6 +47,7 @@
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.testing.imagecapture.CaptureResult;
import androidx.camera.testing.impl.FakeCameraCapturePipeline;
import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -48,10 +55,13 @@
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
/**
* A fake implementation for the {@link CameraControlInternal} interface which is capable of
@@ -73,22 +83,33 @@
}
};
+ private final Object mLock = new Object();
+
/**
* The executor used to invoke any callback/listener which doesn't have a dedicated executor
* for it.
* <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
- * via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
+ * via {@link #FakeCameraControl(Executor, ControlUpdateCallback)}.
*/
- @NonNull private final Executor mExecutor;
+ @NonNull
+ private final Executor mExecutor;
private final ControlUpdateCallback mControlUpdateCallback;
private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
@ImageCapture.FlashMode
private int mFlashMode = FLASH_MODE_OFF;
- private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
- private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
- new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private final ArrayDeque<CaptureConfig> mSubmittedCaptureRequests = new ArrayDeque<>();
+ @GuardedBy("mLock")
+ private final ArrayDeque<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
+ new ArrayDeque<>();
+ @GuardedBy("mLock")
+ private final ArrayDeque<CaptureResult> mCaptureResults = new ArrayDeque<>();
+
+ private final List<CaptureSuccessListener> mCaptureSuccessListeners =
+ new CopyOnWriteArrayList<>();
private boolean mIsZslDisabledByUseCaseConfig = false;
private boolean mIsZslConfigAdded = false;
@@ -108,8 +129,8 @@
* Constructs an instance of {@link FakeCameraControl} with a no-op
* {@link ControlUpdateCallback}.
*
- * @see #FakeCameraControl(CameraControlInternal.ControlUpdateCallback)
- * @see #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)
+ * @see #FakeCameraControl(ControlUpdateCallback)
+ * @see #FakeCameraControl(Executor, ControlUpdateCallback)
*/
public FakeCameraControl() {
this(NO_OP_CALLBACK);
@@ -121,7 +142,7 @@
*
* <p> Note that callbacks will be executed on the calling thread directly via
* {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
- * {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
+ * {@link #FakeCameraControl(Executor, ControlUpdateCallback)}.
*
* @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
*/
@@ -133,7 +154,8 @@
* Constructs an instance of {@link FakeCameraControl} with the
* provided {@link ControlUpdateCallback}.
*
- * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+ * @param executor {@link Executor} used to invoke the {@code
+ * controlUpdateCallback}.
* @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
*/
public FakeCameraControl(@NonNull Executor executor,
@@ -145,44 +167,31 @@
/**
* Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCancelled},
* which is invoked in the thread denoted by {@link #mExecutor}.
+ *
+ * @deprecated Use {@link #completeAllCaptureRequests(CaptureResult)} instead.
*/
+ @Deprecated // TODO: b/366136115 - Remove all usages
public void notifyAllRequestsOnCaptureCancelled() {
- for (CaptureConfig captureConfig : mSubmittedCaptureRequests) {
- for (CameraCaptureCallback cameraCaptureCallback :
- captureConfig.getCameraCaptureCallbacks()) {
- mExecutor.execute(() -> {
- cameraCaptureCallback.onCaptureCancelled(captureConfig.getId());
- });
+ while (true) {
+ if (!completeFirstPendingCaptureRequest(CAPTURE_STATUS_CANCELLED, null)) {
+ break;
}
}
- for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
- completer.setException(
- new ImageCaptureException(ImageCapture.ERROR_CAMERA_CLOSED, "Simulate "
- + "capture cancelled", null));
- }
- mSubmittedCompleterList.clear();
- mSubmittedCaptureRequests.clear();
}
/**
* Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureFailed},
* which is invoked in the thread denoted by {@link #mExecutor}.
+ *
+ * @deprecated Use {@link #completeAllCaptureRequests(CaptureResult)} instead.
*/
+ @Deprecated // TODO: b/366136115 - Remove all usages
public void notifyAllRequestsOnCaptureFailed() {
- for (CaptureConfig captureConfig : mSubmittedCaptureRequests) {
- for (CameraCaptureCallback cameraCaptureCallback :
- captureConfig.getCameraCaptureCallbacks()) {
- mExecutor.execute(() -> cameraCaptureCallback.onCaptureFailed(
- captureConfig.getId(),
- new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR)));
+ while (true) {
+ if (!completeFirstPendingCaptureRequest(CAPTURE_STATUS_FAILED, null)) {
+ break;
}
}
- for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
- completer.setException(new ImageCaptureException(ImageCapture.ERROR_CAPTURE_FAILED,
- "Simulate capture fail", null));
- }
- mSubmittedCompleterList.clear();
- mSubmittedCaptureRequests.clear();
}
/**
@@ -190,20 +199,97 @@
* which is invoked in the thread denoted by {@link #mExecutor}.
*
* @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+ * @deprecated Use {@link #completeAllCaptureRequests(CaptureResult)} instead.
*/
+ @Deprecated // TODO: b/366136115 - Remove all usages
public void notifyAllRequestsOnCaptureCompleted(@NonNull CameraCaptureResult result) {
- for (CaptureConfig captureConfig : mSubmittedCaptureRequests) {
- for (CameraCaptureCallback cameraCaptureCallback :
- captureConfig.getCameraCaptureCallbacks()) {
- mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
- captureConfig.getId(), result));
+ while (true) {
+ if (!completeFirstPendingCaptureRequest(CAPTURE_STATUS_SUCCESSFUL, result)) {
+ break;
}
}
- for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
- completer.set(null);
+ }
+
+ /**
+ * Completes the first submitted but incomplete capture request using one of the
+ * {@link CameraCaptureCallback} methods, which is invoked in the thread denoted by
+ * {@link #mExecutor}.
+ *
+ * @param captureStatus Represents how a capture request should be completed.
+ * @param captureResult The {@link CameraCaptureResult} which is notified to all the
+ * callbacks. Must not be null if captureStatus parameter is
+ * {@link CaptureResult#CAPTURE_STATUS_SUCCESSFUL}.
+ * @return True if a capture request was completed, false otherwise.
+ */
+ // TODO: b/365519650 - Take FakeCameraCaptureResult as parameter to contain extra user-provided
+ // data like bitmap/image proxy and use that to complete capture.
+ @SuppressWarnings("ObjectToString") // Required for captureConfig hashcode log
+ private boolean completeFirstPendingCaptureRequest(
+ @CaptureResult.CaptureStatus int captureStatus,
+ @Nullable CameraCaptureResult captureResult) {
+ CaptureConfig captureConfig;
+ CallbackToFutureAdapter.Completer<Void> completer;
+
+ synchronized (mLock) {
+ if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+ Logger.d(TAG,
+ "completeFirstPendingCaptureRequest: returning early since either "
+ + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+ + "mSubmittedCaptureRequests = "
+ + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+ + mSubmittedCompleterList);
+ return false;
+ }
+
+ captureConfig = mSubmittedCaptureRequests.removeFirst();
+ completer = mSubmittedCompleterList.removeFirst();
}
- mSubmittedCompleterList.clear();
- mSubmittedCaptureRequests.clear();
+ Logger.d(TAG, "completeFirstPendingCaptureRequest: captureConfig = " + captureConfig);
+
+ if (captureStatus == CAPTURE_STATUS_SUCCESSFUL) {
+ notifyCaptureSuccess(requireNonNull(captureResult));
+ }
+
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ mExecutor.execute(() -> {
+ switch (captureStatus) {
+ case CAPTURE_STATUS_SUCCESSFUL:
+ cameraCaptureCallback.onCaptureCompleted(captureConfig.getId(),
+ Objects.requireNonNull(captureResult));
+ break;
+ case CAPTURE_STATUS_FAILED:
+ cameraCaptureCallback.onCaptureFailed(captureConfig.getId(),
+ new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR));
+ break;
+ case CAPTURE_STATUS_CANCELLED:
+ cameraCaptureCallback.onCaptureCancelled(captureConfig.getId());
+ break;
+ default:
+ Logger.e(TAG, "completeFirstPendingCaptureRequest: unknown capture status: "
+ + captureStatus);
+ }
+ });
+ }
+
+ switch (captureStatus) {
+ case CAPTURE_STATUS_SUCCESSFUL:
+ completer.set(null);
+ break;
+ case CAPTURE_STATUS_FAILED:
+ completer.setException(new ImageCaptureException(ImageCapture.ERROR_CAPTURE_FAILED,
+ "Simulate capture fail", null));
+ break;
+ case CAPTURE_STATUS_CANCELLED:
+ completer.setException(new ImageCaptureException(ImageCapture.ERROR_CAMERA_CLOSED,
+ "Simulate capture cancelled", null));
+ break;
+ default:
+ Logger.e(TAG, "completeFirstPendingCaptureRequest: unknown capture status: "
+ + captureStatus);
+ }
+
+ return true;
}
@ImageCapture.FlashMode
@@ -294,24 +380,40 @@
public ListenableFuture<List<Void>> submitStillCaptureRequests(
@NonNull List<CaptureConfig> captureConfigs,
int captureMode, int flashType) {
- mSubmittedCaptureRequests.addAll(captureConfigs);
- mExecutor.execute(
- () -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
+ Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
+
List<ListenableFuture<Void>> fakeFutures = new ArrayList<>();
- for (int i = 0; i < captureConfigs.size(); i++) {
- fakeFutures.add(CallbackToFutureAdapter.getFuture(completer -> {
- mSubmittedCompleterList.add(completer);
- return "fakeFuture";
- }));
+
+ synchronized (mLock) {
+ mSubmittedCaptureRequests.addAll(captureConfigs);
+ for (int i = 0; i < captureConfigs.size(); i++) {
+ AtomicReference<CallbackToFutureAdapter.Completer<Void>> completerRef =
+ new AtomicReference<>();
+ fakeFutures.add(CallbackToFutureAdapter.getFuture(completer -> {
+ // mSubmittedCaptureRequests and mSubmittedCompleterList must be updated under
+ // the same lock to avoid rare out-of-state bugs. So, completer can't be added
+ // to mSubmittedCompleterList here directly even though this line is guaranteed
+ // to be called immediately.
+ completerRef.set(completer);
+ return "fakeFuture";
+ }));
+ mSubmittedCompleterList.add(Objects.requireNonNull(completerRef.get()));
+ }
}
+ mExecutor.execute(
+ () -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
+
if (mOnNewCaptureRequestListener != null) {
- Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
+ Executor executor = requireNonNull(mOnNewCaptureRequestListener.first);
OnNewCaptureRequestListener listener =
- Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+ requireNonNull(mOnNewCaptureRequestListener.second);
executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
}
+
+ mExecutor.execute(this::applyCaptureResults);
+
return Futures.allAsList(fakeFutures);
}
@@ -369,7 +471,7 @@
* {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
*
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
*/
public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
@@ -380,7 +482,7 @@
*
* @param executor {@link Executor} used to notify the {@code listener}.
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
*/
public void setOnNewCaptureRequestListener(@NonNull Executor executor,
@NonNull OnNewCaptureRequestListener listener) {
@@ -444,9 +546,108 @@
return MutableOptionsBundle.from(mInteropConfig);
}
+ /**
+ * Submits a {@link CaptureResult} to be used for the first pending capture request.
+ *
+ * <p> If there are no pending capture requests, the `CaptureResult` is kept in a queue to be
+ * used in future capture requests.
+ *
+ * <p> This method will complete a corresponding capture request according to the provided
+ * capture result.
+ *
+ * <p> For applying a capture result to all already submitted capture requests, use the
+ * {@link #completeAllCaptureRequests} method instead.
+ */
+ public void submitCaptureResult(@NonNull CaptureResult captureResult) {
+ synchronized (mLock) {
+ mCaptureResults.add(captureResult);
+ }
+ applyCaptureResults();
+ }
+
+ /**
+ * Completes all the incomplete capture requests with the provided {@link CaptureResult}.
+ *
+ * <p> Note that {@link ImageCapture#takePicture} methods send requests to camera asynchronously
+ * and thus a capture request from {@link ImageCapture} may not be available immediately.
+ * Consider using {@link #setOnNewCaptureRequestListener} to know when a capture request has
+ * been submitted before using this method right after {@code ImageCapture#takePicture}.
+ * Furthermore, {@code ImageCapture} queues capture requests before submitting to camera when
+ * multiple captures are requested. So it is recommended to use {@link #submitCaptureResult}
+ * whenever possible to avoid confusing and complicated scenario in integration tests.
+ */
+ public void completeAllCaptureRequests(@NonNull CaptureResult captureResult) {
+ synchronized (mLock) {
+ // Add CaptureResult instances for all pending requests first.
+ for (int i = 0; i < mSubmittedCaptureRequests.size(); i++) {
+ mCaptureResults.add(captureResult);
+ }
+ }
+
+ applyCaptureResults();
+ }
+
+ private void applyCaptureResults() {
+ synchronized (mLock) {
+ while (!mCaptureResults.isEmpty()) {
+ CaptureResult captureResult = mCaptureResults.getFirst();
+
+ if (completeFirstPendingCaptureRequest(captureResult.getCaptureStatus(),
+ captureResult.getCameraCaptureResult())) {
+ mCaptureResults.removeFirst();
+ } else {
+ Logger.d(TAG, "applyCaptureResults: failed to notify");
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a listener to be notified when there are completed capture requests.
+ *
+ * @param listener {@link CaptureSuccessListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when capture requests are completed.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public void addCaptureSuccessListener(@NonNull CaptureSuccessListener listener) {
+ mCaptureSuccessListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link CaptureSuccessListener} if it exist.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public void removeCaptureSuccessListener(@NonNull CaptureSuccessListener listener) {
+ mCaptureSuccessListeners.remove(listener);
+ }
+
+ private void notifyCaptureSuccess(@NonNull CameraCaptureResult result) {
+ Logger.d(TAG, "notifyCaptureComplete: mCaptureCompleteListeners = "
+ + mCaptureSuccessListeners);
+ for (CaptureSuccessListener listener : mCaptureSuccessListeners) {
+ listener.onCompleted(result);
+ }
+ }
+
/** A listener which is used to notify when there are new submitted capture requests */
public interface OnNewCaptureRequestListener {
/** Called when there are new submitted capture request */
void onNewCaptureRequests(@NonNull List<CaptureConfig> captureConfigs);
}
+
+ /**
+ * A listener which is used to notify when submitted capture requests are completed
+ * successfully.
+ *
+ * <p> The reason we need to listen to success case specifically is because of how CameraX image
+ * capture flow works internally. In case of success, a real android.media.Image instance is
+ * also expected from ImageReader which makes this kind of listener necessary for the proper
+ * implementation of a fake TakePictureManager.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public interface CaptureSuccessListener {
+ /** Called when a submitted capture request has been completed successfully. */
+ void onCompleted(@NonNull CameraCaptureResult result);
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/imagecapture/CaptureResult.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/imagecapture/CaptureResult.kt
new file mode 100644
index 0000000..8184d01d
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/imagecapture/CaptureResult.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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.testing.imagecapture
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.CAPTURE_STATUS_CANCELLED
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.CAPTURE_STATUS_FAILED
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.CAPTURE_STATUS_SUCCESSFUL
+
+/**
+ * The capture result info used for a fake image capture completion.
+ *
+ * If [captureStatus] is [CAPTURE_STATUS_SUCCESSFUL], [cameraCaptureResult] is guaranteed to be
+ * non-null (`FakeCameraCaptureResult()` is used by default if user didn't provide any).
+ *
+ * If [captureStatus] is [CAPTURE_STATUS_FAILED] or [CAPTURE_STATUS_CANCELLED],
+ * [cameraCaptureResult] is usually null.
+ *
+ * @see FakeCameraControl.submitCaptureResult
+ */
+public class CaptureResult
+private constructor(
+ public val captureStatus: @CaptureStatus Int,
+ public val cameraCaptureResult: FakeCameraCaptureResult? = null
+) {
+ /**
+ * The capture result status used in fake image capture completion.
+ *
+ * @see CaptureResult
+ */
+ @Target(AnnotationTarget.TYPE)
+ @IntDef(CAPTURE_STATUS_SUCCESSFUL, CAPTURE_STATUS_FAILED, CAPTURE_STATUS_CANCELLED)
+ @Retention(AnnotationRetention.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public annotation class CaptureStatus
+
+ public companion object {
+ /** Represents a successful [CaptureStatus]. */
+ public const val CAPTURE_STATUS_SUCCESSFUL: Int = 0
+
+ /** Represents a failed [CaptureStatus]. */
+ public const val CAPTURE_STATUS_FAILED: Int = 1
+
+ /** Represents a canceled [CaptureStatus]. */
+ public const val CAPTURE_STATUS_CANCELLED: Int = 2
+
+ /** Represents a successful [CaptureResult]. */
+ @JvmStatic
+ @JvmOverloads
+ public fun successfulResult(
+ fakeCameraCaptureResult: FakeCameraCaptureResult = FakeCameraCaptureResult()
+ ): CaptureResult =
+ CaptureResult(
+ captureStatus = CAPTURE_STATUS_SUCCESSFUL,
+ cameraCaptureResult = fakeCameraCaptureResult
+ )
+
+ /** Represents a failed [CaptureResult]. */
+ @JvmStatic
+ public fun failedResult(): CaptureResult =
+ CaptureResult(captureStatus = CAPTURE_STATUS_FAILED)
+
+ /** Represents a cancelled [CaptureResult]. */
+ @JvmStatic
+ public fun cancelledResult(): CaptureResult =
+ CaptureResult(
+ captureStatus = CAPTURE_STATUS_CANCELLED,
+ )
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
package androidx.camera.testing.impl
+import android.graphics.Bitmap
import android.graphics.Rect
+import android.util.Size
import android.view.Surface
+import androidx.camera.core.Logger
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
private const val TAG = "CaptureSimulation"
/** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
it.simulateCaptureFrame()
}
@@ -45,7 +48,7 @@
*
* @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
*/
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
val deferred = CompletableDeferred<Unit>()
Futures.addCallback(
@@ -53,6 +56,7 @@
object : FutureCallback<Surface?> {
override fun onSuccess(surface: Surface?) {
if (surface == null) {
+ Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
deferred.completeExceptionally(
IllegalStateException(
"Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
)
return
}
- val canvas =
- surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
// TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
- surface.unlockCanvasAndPost(canvas)
+ surface.simulateCaptureFrame(prescribedSize)
+
deferred.complete(Unit)
}
@@ -77,6 +80,20 @@
deferred.await()
}
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+ val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+ }
+ unlockCanvasAndPost(canvas)
+}
+
// The following methods are adapters for Java invocations.
/**
@@ -88,7 +105,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/SurfaceTextureProvider.java
index cd21770..8366412 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/SurfaceTextureProvider.java
@@ -41,6 +41,7 @@
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class creates implementations of PreviewSurfaceProvider that provide Surfaces that have been
@@ -203,6 +204,8 @@
@Nullable Runnable onClosed) {
return CallbackToFutureAdapter.getFuture((completer) -> {
glExecutor.execute(() -> {
+ Object lock = new Object();
+ AtomicBoolean surfaceTextureReleased = new AtomicBoolean(false);
EGLContextParams contextParams = createDummyEGLContext();
EGL14.eglMakeCurrent(contextParams.display, contextParams.outputSurface,
contextParams.outputSurface, contextParams.context);
@@ -212,14 +215,22 @@
surfaceTexture.setDefaultBufferSize(width, height);
surfaceTexture.setOnFrameAvailableListener(it ->
glExecutor.execute(() -> {
- it.updateTexImage();
- if (frameAvailableListener != null) {
- frameAvailableListener.onFrameAvailable(surfaceTexture);
+ synchronized (lock) {
+ if (surfaceTextureReleased.get()) {
+ return;
+ }
+ it.updateTexImage();
+ if (frameAvailableListener != null) {
+ frameAvailableListener.onFrameAvailable(surfaceTexture);
+ }
}
}));
completer.set(
new SurfaceTextureHolder(surfaceTexture, () -> glExecutor.execute(() -> {
+ synchronized (lock) {
+ surfaceTextureReleased.set(true);
+ }
surfaceTexture.release();
GLES20.glDeleteTextures(1, textureIds, 0);
terminateEGLContext(contextParams);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
index 1914e96..c5ac789 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
@@ -40,7 +40,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageInfo;
import androidx.camera.core.internal.CameraCaptureResultImageInfo;
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult;
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
import androidx.camera.testing.impl.fakes.FakeImageProxy;
import androidx.camera.testing.impl.fakes.FakeJpegPlaneProxy;
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
package androidx.camera.testing.impl.fakes;
+import android.graphics.Bitmap;
import android.graphics.Rect;
import android.media.Image;
@@ -47,6 +48,8 @@
@NonNull
private ImageInfo mImageInfo;
private Image mImage;
+ @Nullable
+ private Bitmap mBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mReleaseLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
mImageInfo = imageInfo;
}
+ public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+ mImageInfo = imageInfo;
+ mBitmap = bitmap;
+ }
+
@Override
public void close() {
synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
return mReleaseFuture;
}
}
+
+ @NonNull
+ @Override
+ public Bitmap toBitmap() {
+ if (mBitmap != null) {
+ return mBitmap;
+ }
+ return ImageProxy.super.toBitmap();
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
index 135f64c..f5157da 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
@@ -22,6 +22,7 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
import androidx.camera.core.impl.utils.Exif
import com.google.common.truth.Truth
import java.io.ByteArrayInputStream
@@ -66,6 +67,7 @@
public val errors: MutableList<ImageCaptureException> = mutableListOf()
override fun onCaptureSuccess(image: ImageProxy) {
+ Logger.d(TAG, "onCaptureSuccess: image = $image")
results.add(
CapturedImage(
image = image,
@@ -86,6 +88,7 @@
}
override fun onError(exception: ImageCaptureException) {
+ Logger.d(TAG, "onError", exception)
errors.add(exception)
latch.countDown()
}
@@ -105,6 +108,11 @@
Truth.assertThat(withTimeoutOrNull(timeout) { latch.await() }).isNotNull()
}
+ /** Asserts that capture hasn't been completed within the provided `duration`. */
+ public suspend fun assertNoCapture(timeout: Duration = CAPTURE_TIMEOUT) {
+ Truth.assertThat(withTimeoutOrNull(timeout) { latch.await() }).isNull()
+ }
+
public suspend fun awaitCapturesAndAssert(
timeout: Duration = CAPTURE_TIMEOUT,
capturedImagesCount: Int = 0,
@@ -137,6 +145,7 @@
}
public companion object {
+ private const val TAG = "FakeOnImageCaptureCallback"
private val CAPTURE_TIMEOUT = 15.seconds
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt
deleted file mode 100644
index e1b2f99..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2024 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.testing.impl.fakes
-
-import androidx.annotation.VisibleForTesting
-import androidx.camera.core.imagecapture.ImagePipeline
-import androidx.camera.core.imagecapture.RequestWithCallback
-import androidx.camera.core.imagecapture.TakePictureManager
-import androidx.camera.core.imagecapture.TakePictureRequest
-
-private const val TAG = "FakeTakePictureManager"
-
-internal class FakeTakePictureManagerImpl : TakePictureManager {
- override fun setImagePipeline(imagePipeline: ImagePipeline) {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- override fun offerRequest(takePictureRequest: TakePictureRequest) {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- override fun pause() {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- override fun resume() {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- override fun abortRequests() {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- @VisibleForTesting
- override fun hasCapturingRequest(): Boolean {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- @VisibleForTesting
- override fun getCapturingRequest(): RequestWithCallback? {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- @VisibleForTesting
- override fun getIncompleteRequests(): List<RequestWithCallback> {
- throw UnsupportedOperationException("Not implemented yet")
- }
-
- @VisibleForTesting
- override fun getImagePipeline(): ImagePipeline {
- throw UnsupportedOperationException("Not implemented yet")
- }
-}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..54dac3a 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,44 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* A fake implementation of {@link UseCaseConfigFactory}.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
@Nullable
private CaptureType mLastRequestedCaptureType;
+ @NonNull
+ private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance.
+ */
+ public FakeUseCaseConfigFactory() {
+ }
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+ * instances.
+ */
+ public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+ mFakeCameras.addAll(fakeCameras);
+ }
+
/**
* Returns the configuration for the given capture type, or <code>null</code> if the
* configuration cannot be produced.
@@ -66,6 +90,18 @@
mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
new FakeSessionConfigOptionUnpacker());
+ if (captureType == CaptureType.IMAGE_CAPTURE) {
+ mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ return new TakePictureManagerWrapper(imageCaptureControl, mFakeCameras);
+ }
+ });
+ }
+
return OptionsBundle.from(mutableConfig);
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..a035008
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2024 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.testing.impl.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.fakes.FakeCameraControl.CaptureSuccessListener
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.TestImageUtil.createBitmap
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+import kotlinx.atomicfu.atomic
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+ imageCaptureControl: ImageCaptureControl,
+ private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+ // Try to keep the fake as close to real as possible
+ private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+ private val bitmap2JpegBytes = Bitmap2JpegBytes()
+ private val jpegBytes2Disk = JpegBytes2Disk()
+ private val jpegBytes2Image = JpegBytes2Image()
+
+ private val imageProxyQueue = ArrayDeque<ImageProxy>()
+ private val outputFileResultsQueue = ArrayDeque<OutputFileResults>()
+
+ private val pendingRequestCount = atomic(0)
+
+ private val captureCompleteListener = CaptureSuccessListener { _ ->
+ completeCapturingRequest()
+
+ // TODO - handle CameraControl receiving more than one capture per takePictureRequest
+ if (pendingRequestCount.decrementAndGet() == 0) {
+ removeCaptureCompleteListener()
+ }
+ }
+
+ private fun addCaptureCompleteListener() {
+ Logger.d(TAG, "addCaptureCompleteListener: fakeCameras = $fakeCameras")
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl).addCaptureSuccessListener(
+ captureCompleteListener
+ )
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+ }
+
+ private fun removeCaptureCompleteListener() {
+ Logger.d(TAG, "removeCaptureCompleteListener: fakeCameras = $fakeCameras")
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl).removeCaptureSuccessListener(
+ captureCompleteListener
+ )
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+ }
+
+ override fun setImagePipeline(imagePipeline: ImagePipeline) {
+ managerDelegate.imagePipeline = imagePipeline
+ }
+
+ override fun offerRequest(takePictureRequest: TakePictureRequest) {
+ if (pendingRequestCount.getAndIncrement() == 0) {
+ addCaptureCompleteListener()
+ }
+
+ managerDelegate.offerRequest(takePictureRequest)
+ }
+
+ override fun pause() {
+ managerDelegate.pause()
+ }
+
+ override fun resume() {
+ managerDelegate.resume()
+ }
+
+ override fun abortRequests() {
+ managerDelegate.abortRequests()
+ }
+
+ @VisibleForTesting
+ override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+ @VisibleForTesting
+ override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+ @VisibleForTesting
+ override fun getIncompleteRequests(): List<RequestWithCallback> =
+ managerDelegate.incompleteRequests
+
+ @VisibleForTesting
+ override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+ @VisibleForTesting
+ private fun completeCapturingRequest() {
+ Log.d(
+ TAG,
+ "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+ )
+ managerDelegate.capturingRequest?.apply {
+ runOnMainThread { // onCaptureStarted, onImageCaptured etc. are @MainThread annotated
+ Logger.d(TAG, "completeCapturingRequest: runOnMainThread")
+ onCaptureStarted()
+ onImageCaptured()
+
+ // TODO: b/365519650 - Take FakeCameraCaptureResult as parameter to contain extra
+ // user-provided data like bitmap/image proxy and use that to complete capture.
+ val bitmap = takePictureRequest.createBitmap()
+
+ val outputFileOptions =
+ takePictureRequest.outputFileOptions // enables smartcast for null check
+
+ if (takePictureRequest.onDiskCallback != null && outputFileOptions != null) {
+ if (outputFileResultsQueue.isEmpty()) {
+ if (outputFileOptions.size > 1) {
+ Logger.w(
+ TAG,
+ "Simultaneous capture not supported, outputFileOptions = $outputFileOptions"
+ )
+ }
+ onFinalResult(
+ createOutputFileResults(
+ takePictureRequest,
+ outputFileOptions[0],
+ bitmap
+ )
+ )
+ } else {
+ onFinalResult(outputFileResultsQueue.removeFirst())
+ }
+ } else {
+ if (imageProxyQueue.isEmpty()) {
+ onFinalResult(createImageProxy(takePictureRequest, bitmap))
+ } else {
+ onFinalResult(imageProxyQueue.removeFirst())
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues an [ImageProxy] to be used as result for the next image capture with
+ * [ImageCapture.OnImageCapturedCallback].
+ *
+ * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+ * for following captures. If no result is available during a capture, CameraX will create a
+ * fake image by itself and provide result based on that.
+ */
+ public fun enqueueImageProxy(imageProxy: ImageProxy) {
+ imageProxyQueue.add(imageProxy)
+ }
+
+ /**
+ * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+ * [ImageCapture.OnImageSavedCallback].
+ *
+ * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+ * available for following captures. If no result is available during a capture, CameraX will
+ * create a fake image by itself and provide result based on that.
+ */
+ public fun enqueueOutputFileResults(outputFileResults: OutputFileResults) {
+ outputFileResultsQueue.add(outputFileResults)
+ }
+
+ private fun createOutputFileResults(
+ takePictureRequest: TakePictureRequest,
+ outputFileOptions: OutputFileOptions,
+ bitmap: Bitmap,
+ ): OutputFileResults {
+ // TODO - Take a bitmap as input and use that directly
+ val bytesPacket = takePictureRequest.convertBitmapToBytes(bitmap)
+ return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+ }
+
+ private fun createImageProxy(
+ takePictureRequest: TakePictureRequest,
+ bitmap: Bitmap,
+ ): ImageProxy {
+ if (canLoadImageProcessingUtilJniLib()) {
+ try {
+ val bytesPacket = takePictureRequest.convertBitmapToBytes(bitmap)
+ return jpegBytes2Image.apply(bytesPacket).data
+ } catch (e: Exception) {
+ // We have observed this kind of issue in Pixel 2 API 26 emulator, however this is
+ // added as a general workaround as this may happen in any emulator/device and
+ // similar to how not all resolutions are supported due to device capabilities even
+ // in production code.
+ Logger.e(
+ TAG,
+ "createImageProxy: failed for cropRect = ${takePictureRequest.cropRect}" +
+ " which may happen due to a high resolution not being supported" +
+ ", trying again with 640x480",
+ e
+ )
+
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ createBitmap(640, 480),
+ Rect(0, 0, 640, 480)
+ )
+ Logger.d(TAG, "createImageProxy: bytesPacket size = ${bytesPacket.size}")
+
+ return jpegBytes2Image.apply(bytesPacket).data
+ }
+ } else {
+ return bitmap.toFakeImageProxy()
+ }
+ }
+
+ private fun Bitmap.toFakeImageProxy(): ImageProxy {
+ return FakeImageProxy(FakeImageInfo(), this)
+ }
+
+ private fun TakePictureRequest.createBitmap() =
+ TestImageUtil.createBitmap(cropRect.width(), cropRect.height())
+
+ private fun TakePictureRequest.convertBitmapToBytes(
+ bitmap: Bitmap,
+ cropRect: Rect = this.cropRect
+ ): Packet<ByteArray> {
+ val inputPacket =
+ Packet.of(
+ bitmap,
+ ExifUtil.createExif(
+ TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+ ),
+ cropRect,
+ rotationDegrees,
+ Matrix(),
+ FakeCameraCaptureResult()
+ )
+
+ return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+ }
+
+ private fun canLoadImageProcessingUtilJniLib(): Boolean {
+ try {
+ System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+ return true
+ } catch (e: UnsatisfiedLinkError) {
+ Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+ return false
+ }
+ }
+
+ private fun runOnMainThread(block: () -> Any) {
+ CameraXExecutors.mainThreadExecutor().submit(block)
+ }
+
+ private companion object {
+ private const val TAG = "TakePictureManagerWrap"
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..0d91091 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -37,7 +37,7 @@
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult;
+import androidx.camera.testing.imagecapture.CaptureResult;
import androidx.camera.testing.impl.mocks.MockScreenFlash;
import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +71,8 @@
}
@Test
+ // TODO: b/366136115 - Remove test for deprecated API when the API is fully removed.
+ @SuppressWarnings("deprecation")
public void notifiesAllRequestOnCaptureCancelled() {
CountDownLatch latch = new CountDownLatch(3);
CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
@@ -99,6 +101,36 @@
}
@Test
+ public void completeAllCaptureRequests_notifiesCaptureCancelledToAllRequests() {
+ CountDownLatch latch = new CountDownLatch(3);
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCancelled(int captureConfigId) {
+ latch.countDown();
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCancelled(int captureConfigId) {
+ latch.countDown();
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCancelled(int captureConfigId) {
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.completeAllCaptureRequests(CaptureResult.cancelledResult());
+
+ awaitLatch(latch);
+ }
+
+ @Test
+ // TODO: b/366136115 - Remove test for deprecated API when the API is fully removed.
+ @SuppressWarnings("deprecation")
public void notifiesAllRequestOnCaptureFailed() {
CountDownLatch latch = new CountDownLatch(3);
List<CameraCaptureFailure> failureList = new ArrayList<>();
@@ -134,6 +166,39 @@
}
@Test
+ public void completeAllCaptureRequests_notifiesCaptureFailedToAllRequests() {
+ CountDownLatch latch = new CountDownLatch(3);
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureFailed(int captureConfigId,
+ @NonNull CameraCaptureFailure failure) {
+ latch.countDown();
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureFailed(int captureConfigId,
+ @NonNull CameraCaptureFailure failure) {
+ latch.countDown();
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureFailed(int captureConfigId,
+ @NonNull CameraCaptureFailure failure) {
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.completeAllCaptureRequests(CaptureResult.failedResult());
+
+ awaitLatch(latch);
+ }
+
+ @Test
+ // TODO: b/366136115 - Remove test for deprecated API when the API is fully removed.
+ @SuppressWarnings("deprecation")
public void notifiesAllRequestOnCaptureCompleted() {
CameraCaptureResult captureResult = new FakeCameraCaptureResult();
@@ -173,6 +238,82 @@
}
@Test
+ public void completeAllCaptureRequests_notifiesCaptureCompletedToAllRequests() {
+ FakeCameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+ CountDownLatch latch = new CountDownLatch(3);
+ List<CameraCaptureResult> resultList = new ArrayList<>();
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.completeAllCaptureRequests(CaptureResult.successfulResult(captureResult));
+
+ awaitLatch(latch);
+ assertThat(resultList).containsExactlyElementsIn(Arrays.asList(captureResult, captureResult,
+ captureResult));
+ }
+
+ @Test
+ public void submitCaptureResult_notifiesCaptureCompletedToAllCallbacksOfFirstRequest() {
+ FakeCameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+ CountDownLatch latch = new CountDownLatch(2);
+ List<CameraCaptureResult> resultList = new ArrayList<>();
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.submitCaptureResult(CaptureResult.successfulResult(captureResult));
+
+ awaitLatch(latch);
+ assertThat(resultList).containsExactlyElementsIn(List.of(captureResult, captureResult));
+ }
+
+ @Test
public void canUpdateFlashModeToOff() {
mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -264,7 +405,7 @@
@Test
public void futureCompletes_whenStillCaptureRequestsSubmittedAndSuccessNotified() {
ListenableFuture<?> future = submitStillCaptureRequests();
- mCameraControl.notifyAllRequestsOnCaptureCompleted(new FakeCameraCaptureResult());
+ mCameraControl.completeAllCaptureRequests(CaptureResult.successfulResult());
try {
future.get(3, TimeUnit.SECONDS);
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
index 3b491b0..f8a913b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -17,11 +17,6 @@
import android.Manifest
import android.content.Context
-import android.graphics.SurfaceTexture
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Log
-import android.view.Surface
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.Camera
@@ -35,12 +30,11 @@
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
-import androidx.camera.testing.impl.GLUtil
+import androidx.camera.testing.impl.SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.testing.impl.video.AudioChecker
@@ -497,47 +491,9 @@
class PreviewMonitor {
private var countDown: CountDownLatch? = null
- private val surfaceProvider =
- Preview.SurfaceProvider { request ->
- val lock = Any()
- var surfaceTextureReleased = false
- val surfaceTexture = SurfaceTexture(0)
- surfaceTexture.setDefaultBufferSize(
- request.resolution.width,
- request.resolution.height
- )
- surfaceTexture.detachFromGLContext()
- surfaceTexture.attachToGLContext(GLUtil.getTexIdFromGLContext())
- val frameUpdateThread = HandlerThread("frameUpdateThread").apply { start() }
-
- surfaceTexture.setOnFrameAvailableListener(
- {
- InstrumentationRegistry.getInstrumentation().runOnMainSync {
- synchronized(lock) {
- if (!surfaceTextureReleased) {
- try {
- surfaceTexture.updateTexImage()
- } catch (e: IllegalStateException) {
- Log.e(TAG, "updateTexImage failed!")
- }
- }
- }
- }
- countDown?.countDown()
- },
- Handler(frameUpdateThread.getLooper())
- )
-
- val surface = Surface(surfaceTexture)
- request.provideSurface(surface, CameraXExecutors.directExecutor()) {
- synchronized(lock) {
- surfaceTextureReleased = true
- surface.release()
- surfaceTexture.release()
- frameUpdateThread.quitSafely()
- }
- }
- }
+ private val surfaceProvider = createAutoDrainingSurfaceTextureProvider {
+ countDown?.countDown()
+ }
fun getSurfaceProvider(): Preview.SurfaceProvider = surfaceProvider
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..3bd6e9b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.core.fakecamera
+import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.os.Build
@@ -27,10 +28,13 @@
import androidx.camera.testing.fakes.FakeAppConfig
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.successfulResult
+import androidx.camera.testing.impl.IgnoreProblematicDeviceRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.testing.impl.fakes.FakeOnImageCapturedCallback
import androidx.camera.testing.impl.fakes.FakeOnImageSavedCallback
import androidx.test.core.app.ApplicationProvider
+import androidx.test.rule.GrantPermissionRule
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import java.text.SimpleDateFormat
@@ -41,8 +45,8 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.After
+import org.junit.Assume.assumeFalse
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -63,6 +67,14 @@
val temporaryFolder =
TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+ // Required for MediaStore tests on some emulators
+ @get:Rule
+ val storagePermissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var camera: FakeCamera
@@ -96,18 +108,25 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
- val callback = FakeOnImageCapturedCallback()
+ assumeFalse(
+ "This emulator fails to create a bitmap from an android.media.Image instance" +
+ ", the emulator is known to have various issues and generally ignored in our tests",
+ IgnoreProblematicDeviceRule.isPixel2Api26Emulator
+ )
+
+ val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
+
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
+ cameraControl.submitCaptureResult(successfulResult())
+
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- callback.results.first().image.toBitmap()
+ assertThat(callback.results.first().image.toBitmap()).isNotNull()
}
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val saveLocation = temporaryFolder.newFile()
@@ -119,6 +138,7 @@
CameraXExecutors.directExecutor(),
callback
)
+ cameraControl.submitCaptureResult(successfulResult())
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
assertThat(saveLocation.length()).isGreaterThan(previousLength)
@@ -126,16 +146,18 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val initialCount = getMediaStoreCameraXImageCount()
val callback = FakeOnImageSavedCallback()
+
imageCapture.takePicture(
createMediaStoreOutputOptions(),
CameraXExecutors.directExecutor(),
callback
)
+ cameraControl.submitCaptureResult(successfulResult())
+
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
assertThat(getMediaStoreCameraXImageCount()).isEqualTo(initialCount + 1)
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index aec2319..b8cdb7e 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -30,6 +30,7 @@
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG;
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR;
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW;
+import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW_JPEG;
import static androidx.camera.core.ImageCapture.getImageCaptureCapabilities;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.integration.core.CameraXViewModel.getConfiguredCameraXCameraImplementation;
@@ -975,6 +976,7 @@
}
}
+ @SuppressLint("RestrictedApiAndroidX")
private void setUpTakePictureButton() {
mTakePicture.setOnClickListener(
new View.OnClickListener() {
@@ -985,47 +987,64 @@
mImageSavedIdlingResource.increment();
mStartCaptureTime = SystemClock.elapsedRealtime();
- ImageCapture.OutputFileOptions outputFileOptions =
- createOutputFileOptions(mImageOutputFormat);
- getImageCapture().takePicture(outputFileOptions,
- mImageCaptureExecutorService,
- new ImageCapture.OnImageSavedCallback() {
- @Override
- public void onImageSaved(
- @NonNull ImageCapture.OutputFileResults
- outputFileResults) {
- Log.d(TAG, "Saved image to "
- + outputFileResults.getSavedUri());
- try {
- mImageSavedIdlingResource.decrement();
- } catch (IllegalStateException e) {
- Log.e(TAG, "Error: unexpected onImageSaved "
- + "callback received. Continuing.");
- }
+ ImageCapture.OnImageSavedCallback callback = new ImageCapture
+ .OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(
+ @NonNull ImageCapture.OutputFileResults
+ outputFileResults) {
+ Log.d(TAG, "Saved image to "
+ + outputFileResults.getSavedUri());
+ try {
+ mImageSavedIdlingResource.decrement();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Error: unexpected onImageSaved "
+ + "callback received. Continuing.");
+ }
- long duration =
- SystemClock.elapsedRealtime() - mStartCaptureTime;
- runOnUiThread(() -> Toast.makeText(CameraXActivity.this,
- "Image captured in " + duration + " ms",
- Toast.LENGTH_SHORT).show());
- if (mSessionImagesUriSet != null) {
- mSessionImagesUriSet.add(
- requireNonNull(
- outputFileResults.getSavedUri()));
- }
- }
+ long duration =
+ SystemClock.elapsedRealtime()
+ - mStartCaptureTime;
+ runOnUiThread(() -> Toast.makeText(CameraXActivity.this,
+ "Image captured in " + duration + " ms",
+ Toast.LENGTH_SHORT).show());
+ if (mSessionImagesUriSet != null) {
+ mSessionImagesUriSet.add(
+ requireNonNull(
+ outputFileResults.getSavedUri()));
+ }
+ }
- @Override
- public void onError(@NonNull ImageCaptureException exception) {
- Log.e(TAG, "Failed to save image.", exception);
+ @Override
+ public void onError(
+ @NonNull ImageCaptureException exception) {
+ Log.e(TAG, "Failed to save image.", exception);
- mLastTakePictureErrorMessage =
- getImageCaptureErrorMessage(exception);
- if (!mImageSavedIdlingResource.isIdleNow()) {
- mImageSavedIdlingResource.decrement();
- }
- }
- });
+ mLastTakePictureErrorMessage =
+ getImageCaptureErrorMessage(exception);
+ if (!mImageSavedIdlingResource.isIdleNow()) {
+ mImageSavedIdlingResource.decrement();
+ }
+ }
+ };
+
+ if (mImageOutputFormat == OUTPUT_FORMAT_RAW_JPEG) {
+ ImageCapture.OutputFileOptions rawOutputFileOptions =
+ createOutputFileOptions(OUTPUT_FORMAT_RAW);
+ ImageCapture.OutputFileOptions jpegOutputFileOptions =
+ createOutputFileOptions(OUTPUT_FORMAT_JPEG);
+ getImageCapture().takePicture(
+ List.of(rawOutputFileOptions, jpegOutputFileOptions),
+ mImageCaptureExecutorService,
+ callback);
+ } else {
+ ImageCapture.OutputFileOptions outputFileOptions =
+ createOutputFileOptions(mImageOutputFormat);
+ getImageCapture().takePicture(
+ outputFileOptions,
+ mImageCaptureExecutorService,
+ callback);
+ }
}
});
}
@@ -1058,9 +1077,9 @@
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimetype);
return new ImageCapture.OutputFileOptions.Builder(
- getContentResolver(),
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues).build();
+ getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues).build();
}
@@ -2654,6 +2673,8 @@
return "Ultra HDR";
} else if (format == OUTPUT_FORMAT_RAW) {
return "Raw";
+ } else if (format == OUTPUT_FORMAT_RAW_JPEG) {
+ return "Raw + Jpeg";
}
return "?";
}
@@ -2667,6 +2688,8 @@
return "Ultra HDR";
} else if (format == OUTPUT_FORMAT_RAW) {
return "Raw";
+ } else if (format == OUTPUT_FORMAT_RAW_JPEG) {
+ return "Raw + Jpeg";
}
return "Unknown format";
}
@@ -2679,6 +2702,8 @@
return 1;
} else if (format == OUTPUT_FORMAT_RAW) {
return 2;
+ } else if (format == OUTPUT_FORMAT_RAW_JPEG) {
+ return 3;
} else {
throw new IllegalArgumentException("Undefined output format: " + format);
}
@@ -2694,6 +2719,8 @@
return OUTPUT_FORMAT_JPEG_ULTRA_HDR;
case 2:
return OUTPUT_FORMAT_RAW;
+ case 3:
+ return OUTPUT_FORMAT_RAW_JPEG;
default:
throw new IllegalArgumentException("Undefined item id: " + itemId);
}
@@ -2728,7 +2755,7 @@
@OptIn(markerClass = ExperimentalCamera2Interop.class)
private static int getCamera2LensFacing(@NonNull CameraInfo cameraInfo) {
Integer lensFacing = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
- CameraCharacteristics.LENS_FACING);
+ CameraCharacteristics.LENS_FACING);
return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
}
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 37609c1..b12b8d8 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -19,11 +19,15 @@
import android.content.ContentValues
import android.content.Context
import android.os.Build
+import android.os.Looper
import android.provider.MediaStore
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.cancelledResult
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.failedResult
+import androidx.camera.testing.imagecapture.CaptureResult.Companion.successfulResult
import androidx.camera.testing.impl.fakes.FakeOnImageCapturedCallback
import androidx.camera.testing.impl.fakes.FakeOnImageSavedCallback
import androidx.camera.testing.rules.FakeCameraTestRule
@@ -33,15 +37,17 @@
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@@ -75,20 +81,111 @@
assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue()
}
- // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
- // need to be reflected there too
- @Ignore("b/318314454")
@Test
- fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
+ fun completesAllCaptures_whenMultiplePicsTakenConsecutivelyBeforeSubmittingResults(): Unit =
+ runBlocking {
+ val callback1 = FakeOnImageCapturedCallback()
+ val callback2 = FakeOnImageCapturedCallback()
+
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback1)
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback2)
+ cameraControl.submitCaptureResult(successfulResult())
+ cameraControl.submitCaptureResult(successfulResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback1.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ callback2.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ }
+
+ @Test
+ fun completesAllCaptures_whenMultiplePicsTakenConsecutivelyAfterSubmittingResults(): Unit =
+ runBlocking {
+ val callback1 = FakeOnImageCapturedCallback()
+ val callback2 = FakeOnImageCapturedCallback()
+
+ cameraControl.submitCaptureResult(successfulResult())
+ cameraControl.submitCaptureResult(successfulResult())
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback1)
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback2)
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback1.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ callback2.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ }
+
+ @Test
+ fun completesFirstCapture_whenMultiplePicsTakenConsecutivelyBeforeSubmittingResult(): Unit =
+ runBlocking {
+ val callback1 = FakeOnImageCapturedCallback()
+ val callback2 = FakeOnImageCapturedCallback()
+
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback1)
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback2)
+ cameraControl.submitCaptureResult(successfulResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback1.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ callback2.assertNoCapture(timeout = 100.milliseconds)
+ }
+
+ @Test
+ fun completesCaptureWithError_whenCaptureRequestsCancelled(): Unit = runBlocking {
val callback = FakeOnImageCapturedCallback()
+
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- callback.results.first().image.toBitmap()
+ cameraControl.submitCaptureResult(cancelledResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback.awaitCapturesAndAssert(
+ timeout = 1.seconds,
+ errorsCount = 1,
+ capturedImagesCount = 0
+ )
+ }
+
+ @Test
+ fun completesCaptureWithError_whenCaptureRequestsFailed(): Unit = runBlocking {
+ val callback = FakeOnImageCapturedCallback()
+
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
+ cameraControl.submitCaptureResult(failedResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback.awaitCapturesAndAssert(
+ timeout = 1.seconds,
+ errorsCount = 1,
+ capturedImagesCount = 0
+ )
}
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
+ @Test
+ fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
+ val callback = FakeOnImageCapturedCallback()
+
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
+ cameraControl.submitCaptureResult(successfulResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
+ assertThat(callback.results.first().image.toBitmap()).isNotNull()
+ }
+
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
val saveLocation = temporaryFolder.newFile()
@@ -100,23 +197,32 @@
CameraXExecutors.directExecutor(),
callback
)
+ cameraControl.submitCaptureResult(successfulResult())
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
assertThat(saveLocation.length()).isGreaterThan(previousLength)
}
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val callback = FakeOnImageSavedCallback()
+
imageCapture.takePicture(
createMediaStoreOutputOptions(),
CameraXExecutors.directExecutor(),
callback
)
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ cameraControl.submitCaptureResult(successfulResult())
+
+ // TODO: b/365571231 - Can we remove CameraX main thread dependencies using more fakes?
+ shadowOf(Looper.getMainLooper()).idle()
+
+ callback.awaitCapturesAndAssert(timeout = 1.seconds, capturedImagesCount = 1)
assertThat(callback.results.first().savedUri).isNotNull()
}
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index d7df27b..40eca5a 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -148,6 +148,8 @@
}
public final class DoubleListKt {
+ method public static inline androidx.collection.DoubleList buildDoubleList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableDoubleList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.DoubleList buildDoubleList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableDoubleList,kotlin.Unit> builderAction);
method public static androidx.collection.DoubleList doubleListOf();
method public static androidx.collection.DoubleList doubleListOf(double element1);
method public static androidx.collection.DoubleList doubleListOf(double element1, double element2);
@@ -165,7 +167,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(float value);
method public final int count();
@@ -198,6 +200,8 @@
}
public final class FloatFloatMapKt {
+ method public static inline androidx.collection.FloatFloatMap buildFloatFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatFloatMap buildFloatFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
method public static androidx.collection.FloatFloatMap floatFloatMapOf();
method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1);
@@ -228,7 +232,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(int value);
method public final int count();
@@ -261,6 +265,8 @@
}
public final class FloatIntMapKt {
+ method public static inline androidx.collection.FloatIntMap buildFloatIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatIntMap buildFloatIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatIntMap emptyFloatIntMap();
method public static androidx.collection.FloatIntMap floatIntMapOf();
method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1);
@@ -330,6 +336,8 @@
}
public final class FloatListKt {
+ method public static inline androidx.collection.FloatList buildFloatList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatList buildFloatList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatList,kotlin.Unit> builderAction);
method public static androidx.collection.FloatList emptyFloatList();
method public static androidx.collection.FloatList floatListOf();
method public static androidx.collection.FloatList floatListOf(float element1);
@@ -347,7 +355,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(long value);
method public final int count();
@@ -380,6 +388,8 @@
}
public final class FloatLongMapKt {
+ method public static inline androidx.collection.FloatLongMap buildFloatLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatLongMap buildFloatLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatLongMap emptyFloatLongMap();
method public static androidx.collection.FloatLongMap floatLongMapOf();
method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1);
@@ -399,7 +409,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(V value);
method public final int count();
@@ -432,6 +442,8 @@
}
public final class FloatObjectMapKt {
+ method public static inline <V> androidx.collection.FloatObjectMap<V> buildFloatObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.FloatObjectMap<V> buildFloatObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1);
@@ -479,6 +491,8 @@
}
public final class FloatSetKt {
+ method public static inline androidx.collection.FloatSet buildFloatSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatSet buildFloatSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatSet,kotlin.Unit> builderAction);
method public static androidx.collection.FloatSet emptyFloatSet();
method public static androidx.collection.FloatSet floatSetOf();
method public static androidx.collection.FloatSet floatSetOf(float element1);
@@ -496,7 +510,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(float value);
method public final int count();
@@ -529,6 +543,8 @@
}
public final class IntFloatMapKt {
+ method public static inline androidx.collection.IntFloatMap buildIntFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntFloatMap buildIntFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntFloatMap emptyIntFloatMap();
method public static androidx.collection.IntFloatMap intFloatMapOf();
method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1);
@@ -548,7 +564,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(int value);
method public final int count();
@@ -581,6 +597,8 @@
}
public final class IntIntMapKt {
+ method public static inline androidx.collection.IntIntMap buildIntIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntIntMap buildIntIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntIntMap emptyIntIntMap();
method public static androidx.collection.IntIntMap intIntMapOf();
method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1);
@@ -661,6 +679,8 @@
}
public final class IntListKt {
+ method public static inline androidx.collection.IntList buildIntList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntList buildIntList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntList,kotlin.Unit> builderAction);
method public static androidx.collection.IntList emptyIntList();
method public static androidx.collection.IntList intListOf();
method public static androidx.collection.IntList intListOf(int element1);
@@ -678,7 +698,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(long value);
method public final int count();
@@ -711,6 +731,8 @@
}
public final class IntLongMapKt {
+ method public static inline androidx.collection.IntLongMap buildIntLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntLongMap buildIntLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntLongMap emptyIntLongMap();
method public static androidx.collection.IntLongMap intLongMapOf();
method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1);
@@ -730,7 +752,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(V value);
method public final int count();
@@ -763,6 +785,8 @@
}
public final class IntObjectMapKt {
+ method public static inline <V> androidx.collection.IntObjectMap<V> buildIntObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.IntObjectMap<V> buildIntObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1);
@@ -810,6 +834,8 @@
}
public final class IntSetKt {
+ method public static inline androidx.collection.IntSet buildIntSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntSet buildIntSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntSet,kotlin.Unit> builderAction);
method public static androidx.collection.IntSet emptyIntSet();
method public static androidx.collection.IntSet intSetOf();
method public static androidx.collection.IntSet intSetOf(int element1);
@@ -827,7 +853,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(float value);
method public final int count();
@@ -860,6 +886,8 @@
}
public final class LongFloatMapKt {
+ method public static inline androidx.collection.LongFloatMap buildLongFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongFloatMap buildLongFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongFloatMap emptyLongFloatMap();
method public static androidx.collection.LongFloatMap longFloatMapOf();
method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1);
@@ -879,7 +907,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(int value);
method public final int count();
@@ -912,6 +940,8 @@
}
public final class LongIntMapKt {
+ method public static inline androidx.collection.LongIntMap buildLongIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongIntMap buildLongIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongIntMap emptyLongIntMap();
method public static androidx.collection.LongIntMap longIntMapOf();
method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1);
@@ -981,6 +1011,8 @@
}
public final class LongListKt {
+ method public static inline androidx.collection.LongList buildLongList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongList buildLongList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongList,kotlin.Unit> builderAction);
method public static androidx.collection.LongList emptyLongList();
method public static androidx.collection.LongList longListOf();
method public static androidx.collection.LongList longListOf(long element1);
@@ -998,7 +1030,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(long value);
method public final int count();
@@ -1031,6 +1063,8 @@
}
public final class LongLongMapKt {
+ method public static inline androidx.collection.LongLongMap buildLongLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongLongMap buildLongLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongLongMap emptyLongLongMap();
method public static androidx.collection.LongLongMap longLongMapOf();
method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1);
@@ -1060,7 +1094,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(V value);
method public final int count();
@@ -1093,6 +1127,8 @@
}
public final class LongObjectMapKt {
+ method public static inline <V> androidx.collection.LongObjectMap<V> buildLongObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.LongObjectMap<V> buildLongObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1);
@@ -1140,6 +1176,8 @@
}
public final class LongSetKt {
+ method public static inline androidx.collection.LongSet buildLongSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongSet buildLongSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongSet,kotlin.Unit> builderAction);
method public static androidx.collection.LongSet emptyLongSet();
method public static androidx.collection.LongSet longSetOf();
method public static androidx.collection.LongSet longSetOf(long element1);
@@ -1877,7 +1915,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(float value);
method public final int count();
@@ -1910,6 +1948,8 @@
}
public final class ObjectFloatMapKt {
+ method public static inline <K> androidx.collection.ObjectFloatMap<K> buildObjectFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectFloatMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectFloatMap<K> buildObjectFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectFloatMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1);
@@ -1929,7 +1969,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(int value);
method public final int count();
@@ -1962,6 +2002,8 @@
}
public final class ObjectIntMapKt {
+ method public static inline <K> androidx.collection.ObjectIntMap<K> buildObjectIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectIntMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectIntMap<K> buildObjectIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectIntMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1);
@@ -2048,7 +2090,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(long value);
method public final int count();
@@ -2081,6 +2123,8 @@
}
public final class ObjectLongMapKt {
+ method public static inline <K> androidx.collection.ObjectLongMap<K> buildObjectLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectLongMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectLongMap<K> buildObjectLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectLongMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1);
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index 8b56e9b..b740615 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -150,6 +150,8 @@
}
public final class DoubleListKt {
+ method public static inline androidx.collection.DoubleList buildDoubleList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableDoubleList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.DoubleList buildDoubleList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableDoubleList,kotlin.Unit> builderAction);
method public static androidx.collection.DoubleList doubleListOf();
method public static androidx.collection.DoubleList doubleListOf(double element1);
method public static androidx.collection.DoubleList doubleListOf(double element1, double element2);
@@ -167,7 +169,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(float value);
method public final int count();
@@ -205,6 +207,8 @@
}
public final class FloatFloatMapKt {
+ method public static inline androidx.collection.FloatFloatMap buildFloatFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatFloatMap buildFloatFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
method public static androidx.collection.FloatFloatMap floatFloatMapOf();
method public static androidx.collection.FloatFloatMap floatFloatMapOf(float key1, float value1);
@@ -235,7 +239,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(int value);
method public final int count();
@@ -273,6 +277,8 @@
}
public final class FloatIntMapKt {
+ method public static inline androidx.collection.FloatIntMap buildFloatIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatIntMap buildFloatIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatIntMap emptyFloatIntMap();
method public static androidx.collection.FloatIntMap floatIntMapOf();
method public static androidx.collection.FloatIntMap floatIntMapOf(float key1, int value1);
@@ -344,6 +350,8 @@
}
public final class FloatListKt {
+ method public static inline androidx.collection.FloatList buildFloatList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatList buildFloatList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatList,kotlin.Unit> builderAction);
method public static androidx.collection.FloatList emptyFloatList();
method public static androidx.collection.FloatList floatListOf();
method public static androidx.collection.FloatList floatListOf(float element1);
@@ -361,7 +369,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(long value);
method public final int count();
@@ -399,6 +407,8 @@
}
public final class FloatLongMapKt {
+ method public static inline androidx.collection.FloatLongMap buildFloatLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatLongMap buildFloatLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.FloatLongMap emptyFloatLongMap();
method public static androidx.collection.FloatLongMap floatLongMapOf();
method public static androidx.collection.FloatLongMap floatLongMapOf(float key1, long value1);
@@ -418,7 +428,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(float key);
+ method public final inline operator boolean contains(float key);
method public final boolean containsKey(float key);
method public final boolean containsValue(V value);
method public final int count();
@@ -455,6 +465,8 @@
}
public final class FloatObjectMapKt {
+ method public static inline <V> androidx.collection.FloatObjectMap<V> buildFloatObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.FloatObjectMap<V> buildFloatObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(float key1, V value1);
@@ -505,6 +517,8 @@
}
public final class FloatSetKt {
+ method public static inline androidx.collection.FloatSet buildFloatSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.FloatSet buildFloatSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableFloatSet,kotlin.Unit> builderAction);
method public static androidx.collection.FloatSet emptyFloatSet();
method public static androidx.collection.FloatSet floatSetOf();
method public static androidx.collection.FloatSet floatSetOf(float element1);
@@ -522,7 +536,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(float value);
method public final int count();
@@ -560,6 +574,8 @@
}
public final class IntFloatMapKt {
+ method public static inline androidx.collection.IntFloatMap buildIntFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntFloatMap buildIntFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntFloatMap emptyIntFloatMap();
method public static androidx.collection.IntFloatMap intFloatMapOf();
method public static androidx.collection.IntFloatMap intFloatMapOf(int key1, float value1);
@@ -579,7 +595,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(int value);
method public final int count();
@@ -617,6 +633,8 @@
}
public final class IntIntMapKt {
+ method public static inline androidx.collection.IntIntMap buildIntIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntIntMap buildIntIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntIntMap emptyIntIntMap();
method public static androidx.collection.IntIntMap intIntMapOf();
method public static androidx.collection.IntIntMap intIntMapOf(int key1, int value1);
@@ -699,6 +717,8 @@
}
public final class IntListKt {
+ method public static inline androidx.collection.IntList buildIntList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntList buildIntList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntList,kotlin.Unit> builderAction);
method public static androidx.collection.IntList emptyIntList();
method public static androidx.collection.IntList intListOf();
method public static androidx.collection.IntList intListOf(int element1);
@@ -716,7 +736,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(long value);
method public final int count();
@@ -754,6 +774,8 @@
}
public final class IntLongMapKt {
+ method public static inline androidx.collection.IntLongMap buildIntLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntLongMap buildIntLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.IntLongMap emptyIntLongMap();
method public static androidx.collection.IntLongMap intLongMapOf();
method public static androidx.collection.IntLongMap intLongMapOf(int key1, long value1);
@@ -773,7 +795,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(int key);
+ method public final inline operator boolean contains(int key);
method public final boolean containsKey(int key);
method public final boolean containsValue(V value);
method public final int count();
@@ -810,6 +832,8 @@
}
public final class IntObjectMapKt {
+ method public static inline <V> androidx.collection.IntObjectMap<V> buildIntObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.IntObjectMap<V> buildIntObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(int key1, V value1);
@@ -860,6 +884,8 @@
}
public final class IntSetKt {
+ method public static inline androidx.collection.IntSet buildIntSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.IntSet buildIntSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableIntSet,kotlin.Unit> builderAction);
method public static androidx.collection.IntSet emptyIntSet();
method public static androidx.collection.IntSet intSetOf();
method public static androidx.collection.IntSet intSetOf(int element1);
@@ -877,7 +903,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(float value);
method public final int count();
@@ -915,6 +941,8 @@
}
public final class LongFloatMapKt {
+ method public static inline androidx.collection.LongFloatMap buildLongFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongFloatMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongFloatMap buildLongFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongFloatMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongFloatMap emptyLongFloatMap();
method public static androidx.collection.LongFloatMap longFloatMapOf();
method public static androidx.collection.LongFloatMap longFloatMapOf(long key1, float value1);
@@ -934,7 +962,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(int value);
method public final int count();
@@ -972,6 +1000,8 @@
}
public final class LongIntMapKt {
+ method public static inline androidx.collection.LongIntMap buildLongIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongIntMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongIntMap buildLongIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongIntMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongIntMap emptyLongIntMap();
method public static androidx.collection.LongIntMap longIntMapOf();
method public static androidx.collection.LongIntMap longIntMapOf(long key1, int value1);
@@ -1043,6 +1073,8 @@
}
public final class LongListKt {
+ method public static inline androidx.collection.LongList buildLongList(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongList,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongList buildLongList(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongList,kotlin.Unit> builderAction);
method public static androidx.collection.LongList emptyLongList();
method public static androidx.collection.LongList longListOf();
method public static androidx.collection.LongList longListOf(long element1);
@@ -1060,7 +1092,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(long value);
method public final int count();
@@ -1098,6 +1130,8 @@
}
public final class LongLongMapKt {
+ method public static inline androidx.collection.LongLongMap buildLongLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongLongMap,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongLongMap buildLongLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongLongMap,kotlin.Unit> builderAction);
method public static androidx.collection.LongLongMap emptyLongLongMap();
method public static androidx.collection.LongLongMap longLongMapOf();
method public static androidx.collection.LongLongMap longLongMapOf(long key1, long value1);
@@ -1127,7 +1161,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
- method public final operator boolean contains(long key);
+ method public final inline operator boolean contains(long key);
method public final boolean containsKey(long key);
method public final boolean containsValue(V value);
method public final int count();
@@ -1164,6 +1198,8 @@
}
public final class LongObjectMapKt {
+ method public static inline <V> androidx.collection.LongObjectMap<V> buildLongObjectMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongObjectMap<V>,kotlin.Unit> builderAction);
+ method public static inline <V> androidx.collection.LongObjectMap<V> buildLongObjectMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongObjectMap<V>,kotlin.Unit> builderAction);
method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(long key1, V value1);
@@ -1214,6 +1250,8 @@
}
public final class LongSetKt {
+ method public static inline androidx.collection.LongSet buildLongSet(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongSet,kotlin.Unit> builderAction);
+ method public static inline androidx.collection.LongSet buildLongSet(kotlin.jvm.functions.Function1<? super androidx.collection.MutableLongSet,kotlin.Unit> builderAction);
method public static androidx.collection.LongSet emptyLongSet();
method public static androidx.collection.LongSet longSetOf();
method public static androidx.collection.LongSet longSetOf(long element1);
@@ -1970,7 +2008,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(float value);
method public final int count();
@@ -2008,6 +2046,8 @@
}
public final class ObjectFloatMapKt {
+ method public static inline <K> androidx.collection.ObjectFloatMap<K> buildObjectFloatMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectFloatMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectFloatMap<K> buildObjectFloatMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectFloatMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(K key1, float value1);
@@ -2027,7 +2067,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(int value);
method public final int count();
@@ -2065,6 +2105,8 @@
}
public final class ObjectIntMapKt {
+ method public static inline <K> androidx.collection.ObjectIntMap<K> buildObjectIntMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectIntMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectIntMap<K> buildObjectIntMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectIntMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(K key1, int value1);
@@ -2153,7 +2195,7 @@
method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
method public final boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
- method public final operator boolean contains(K key);
+ method public final inline operator boolean contains(K key);
method public final boolean containsKey(K key);
method public final boolean containsValue(long value);
method public final int count();
@@ -2191,6 +2233,8 @@
}
public final class ObjectLongMapKt {
+ method public static inline <K> androidx.collection.ObjectLongMap<K> buildObjectLongMap(int initialCapacity, kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectLongMap<K>,kotlin.Unit> builderAction);
+ method public static inline <K> androidx.collection.ObjectLongMap<K> buildObjectLongMap(kotlin.jvm.functions.Function1<? super androidx.collection.MutableObjectLongMap<K>,kotlin.Unit> builderAction);
method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(K key1, long value1);
diff --git a/collection/collection/bcv/native/current.txt b/collection/collection/bcv/native/current.txt
index 38ae0962..012c700f 100644
--- a/collection/collection/bcv/native/current.txt
+++ b/collection/collection/bcv/native/current.txt
@@ -1042,7 +1042,6 @@
final fun <set-values>(kotlin/Array<kotlin/Any?>) // androidx.collection/FloatObjectMap.values.<set-values>|<set-values>(kotlin.Array<kotlin.Any?>){}[0]
final fun any(): kotlin/Boolean // androidx.collection/FloatObjectMap.any|any(){}[0]
- final fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatObjectMap.contains|contains(kotlin.Float){}[0]
final fun containsKey(kotlin/Float): kotlin/Boolean // androidx.collection/FloatObjectMap.containsKey|containsKey(kotlin.Float){}[0]
final fun containsValue(#A): kotlin/Boolean // androidx.collection/FloatObjectMap.containsValue|containsValue(1:0){}[0]
final fun count(): kotlin/Int // androidx.collection/FloatObjectMap.count|count(){}[0]
@@ -1054,6 +1053,7 @@
final fun none(): kotlin/Boolean // androidx.collection/FloatObjectMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Float, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatObjectMap.all|all(kotlin.Function2<kotlin.Float,1:0,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Float, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatObjectMap.any|any(kotlin.Function2<kotlin.Float,1:0,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatObjectMap.contains|contains(kotlin.Float){}[0]
final inline fun count(kotlin/Function2<kotlin/Float, #A, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatObjectMap.count|count(kotlin.Function2<kotlin.Float,1:0,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Float, #A, kotlin/Unit>) // androidx.collection/FloatObjectMap.forEach|forEach(kotlin.Function2<kotlin.Float,1:0,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/FloatObjectMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1085,7 +1085,6 @@
final fun <set-values>(kotlin/Array<kotlin/Any?>) // androidx.collection/IntObjectMap.values.<set-values>|<set-values>(kotlin.Array<kotlin.Any?>){}[0]
final fun any(): kotlin/Boolean // androidx.collection/IntObjectMap.any|any(){}[0]
- final fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntObjectMap.contains|contains(kotlin.Int){}[0]
final fun containsKey(kotlin/Int): kotlin/Boolean // androidx.collection/IntObjectMap.containsKey|containsKey(kotlin.Int){}[0]
final fun containsValue(#A): kotlin/Boolean // androidx.collection/IntObjectMap.containsValue|containsValue(1:0){}[0]
final fun count(): kotlin/Int // androidx.collection/IntObjectMap.count|count(){}[0]
@@ -1097,6 +1096,7 @@
final fun none(): kotlin/Boolean // androidx.collection/IntObjectMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Int, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntObjectMap.all|all(kotlin.Function2<kotlin.Int,1:0,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Int, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntObjectMap.any|any(kotlin.Function2<kotlin.Int,1:0,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntObjectMap.contains|contains(kotlin.Int){}[0]
final inline fun count(kotlin/Function2<kotlin/Int, #A, kotlin/Boolean>): kotlin/Int // androidx.collection/IntObjectMap.count|count(kotlin.Function2<kotlin.Int,1:0,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Int, #A, kotlin/Unit>) // androidx.collection/IntObjectMap.forEach|forEach(kotlin.Function2<kotlin.Int,1:0,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/IntObjectMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1128,7 +1128,6 @@
final fun <set-values>(kotlin/Array<kotlin/Any?>) // androidx.collection/LongObjectMap.values.<set-values>|<set-values>(kotlin.Array<kotlin.Any?>){}[0]
final fun any(): kotlin/Boolean // androidx.collection/LongObjectMap.any|any(){}[0]
- final fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongObjectMap.contains|contains(kotlin.Long){}[0]
final fun containsKey(kotlin/Long): kotlin/Boolean // androidx.collection/LongObjectMap.containsKey|containsKey(kotlin.Long){}[0]
final fun containsValue(#A): kotlin/Boolean // androidx.collection/LongObjectMap.containsValue|containsValue(1:0){}[0]
final fun count(): kotlin/Int // androidx.collection/LongObjectMap.count|count(){}[0]
@@ -1140,6 +1139,7 @@
final fun none(): kotlin/Boolean // androidx.collection/LongObjectMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Long, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongObjectMap.all|all(kotlin.Function2<kotlin.Long,1:0,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Long, #A, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongObjectMap.any|any(kotlin.Function2<kotlin.Long,1:0,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongObjectMap.contains|contains(kotlin.Long){}[0]
final inline fun count(kotlin/Function2<kotlin/Long, #A, kotlin/Boolean>): kotlin/Int // androidx.collection/LongObjectMap.count|count(kotlin.Function2<kotlin.Long,1:0,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Long, #A, kotlin/Unit>) // androidx.collection/LongObjectMap.forEach|forEach(kotlin.Function2<kotlin.Long,1:0,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/LongObjectMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1171,7 +1171,6 @@
final fun <set-values>(kotlin/FloatArray) // androidx.collection/ObjectFloatMap.values.<set-values>|<set-values>(kotlin.FloatArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/ObjectFloatMap.any|any(){}[0]
- final fun contains(#A): kotlin/Boolean // androidx.collection/ObjectFloatMap.contains|contains(1:0){}[0]
final fun containsKey(#A): kotlin/Boolean // androidx.collection/ObjectFloatMap.containsKey|containsKey(1:0){}[0]
final fun containsValue(kotlin/Float): kotlin/Boolean // androidx.collection/ObjectFloatMap.containsValue|containsValue(kotlin.Float){}[0]
final fun count(): kotlin/Int // androidx.collection/ObjectFloatMap.count|count(){}[0]
@@ -1184,6 +1183,7 @@
final fun none(): kotlin/Boolean // androidx.collection/ObjectFloatMap.none|none(){}[0]
final inline fun all(kotlin/Function2<#A, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectFloatMap.all|all(kotlin.Function2<1:0,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<#A, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectFloatMap.any|any(kotlin.Function2<1:0,kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun contains(#A): kotlin/Boolean // androidx.collection/ObjectFloatMap.contains|contains(1:0){}[0]
final inline fun count(kotlin/Function2<#A, kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/ObjectFloatMap.count|count(kotlin.Function2<1:0,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<#A, kotlin/Float, kotlin/Unit>) // androidx.collection/ObjectFloatMap.forEach|forEach(kotlin.Function2<1:0,kotlin.Float,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/ObjectFloatMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1215,7 +1215,6 @@
final fun <set-values>(kotlin/IntArray) // androidx.collection/ObjectIntMap.values.<set-values>|<set-values>(kotlin.IntArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/ObjectIntMap.any|any(){}[0]
- final fun contains(#A): kotlin/Boolean // androidx.collection/ObjectIntMap.contains|contains(1:0){}[0]
final fun containsKey(#A): kotlin/Boolean // androidx.collection/ObjectIntMap.containsKey|containsKey(1:0){}[0]
final fun containsValue(kotlin/Int): kotlin/Boolean // androidx.collection/ObjectIntMap.containsValue|containsValue(kotlin.Int){}[0]
final fun count(): kotlin/Int // androidx.collection/ObjectIntMap.count|count(){}[0]
@@ -1228,6 +1227,7 @@
final fun none(): kotlin/Boolean // androidx.collection/ObjectIntMap.none|none(){}[0]
final inline fun all(kotlin/Function2<#A, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectIntMap.all|all(kotlin.Function2<1:0,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<#A, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectIntMap.any|any(kotlin.Function2<1:0,kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun contains(#A): kotlin/Boolean // androidx.collection/ObjectIntMap.contains|contains(1:0){}[0]
final inline fun count(kotlin/Function2<#A, kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/ObjectIntMap.count|count(kotlin.Function2<1:0,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<#A, kotlin/Int, kotlin/Unit>) // androidx.collection/ObjectIntMap.forEach|forEach(kotlin.Function2<1:0,kotlin.Int,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/ObjectIntMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1319,7 +1319,6 @@
final fun <set-values>(kotlin/LongArray) // androidx.collection/ObjectLongMap.values.<set-values>|<set-values>(kotlin.LongArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/ObjectLongMap.any|any(){}[0]
- final fun contains(#A): kotlin/Boolean // androidx.collection/ObjectLongMap.contains|contains(1:0){}[0]
final fun containsKey(#A): kotlin/Boolean // androidx.collection/ObjectLongMap.containsKey|containsKey(1:0){}[0]
final fun containsValue(kotlin/Long): kotlin/Boolean // androidx.collection/ObjectLongMap.containsValue|containsValue(kotlin.Long){}[0]
final fun count(): kotlin/Int // androidx.collection/ObjectLongMap.count|count(){}[0]
@@ -1332,6 +1331,7 @@
final fun none(): kotlin/Boolean // androidx.collection/ObjectLongMap.none|none(){}[0]
final inline fun all(kotlin/Function2<#A, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectLongMap.all|all(kotlin.Function2<1:0,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<#A, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/ObjectLongMap.any|any(kotlin.Function2<1:0,kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun contains(#A): kotlin/Boolean // androidx.collection/ObjectLongMap.contains|contains(1:0){}[0]
final inline fun count(kotlin/Function2<#A, kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/ObjectLongMap.count|count(kotlin.Function2<1:0,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<#A, kotlin/Long, kotlin/Unit>) // androidx.collection/ObjectLongMap.forEach|forEach(kotlin.Function2<1:0,kotlin.Long,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/ObjectLongMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1504,7 +1504,6 @@
final fun <set-values>(kotlin/FloatArray) // androidx.collection/FloatFloatMap.values.<set-values>|<set-values>(kotlin.FloatArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/FloatFloatMap.any|any(){}[0]
- final fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatFloatMap.contains|contains(kotlin.Float){}[0]
final fun containsKey(kotlin/Float): kotlin/Boolean // androidx.collection/FloatFloatMap.containsKey|containsKey(kotlin.Float){}[0]
final fun containsValue(kotlin/Float): kotlin/Boolean // androidx.collection/FloatFloatMap.containsValue|containsValue(kotlin.Float){}[0]
final fun count(): kotlin/Int // androidx.collection/FloatFloatMap.count|count(){}[0]
@@ -1517,6 +1516,7 @@
final fun none(): kotlin/Boolean // androidx.collection/FloatFloatMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Float, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatFloatMap.all|all(kotlin.Function2<kotlin.Float,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Float, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatFloatMap.any|any(kotlin.Function2<kotlin.Float,kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatFloatMap.contains|contains(kotlin.Float){}[0]
final inline fun count(kotlin/Function2<kotlin/Float, kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatFloatMap.count|count(kotlin.Function2<kotlin.Float,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Float, kotlin/Float, kotlin/Unit>) // androidx.collection/FloatFloatMap.forEach|forEach(kotlin.Function2<kotlin.Float,kotlin.Float,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/FloatFloatMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1548,7 +1548,6 @@
final fun <set-values>(kotlin/IntArray) // androidx.collection/FloatIntMap.values.<set-values>|<set-values>(kotlin.IntArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/FloatIntMap.any|any(){}[0]
- final fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatIntMap.contains|contains(kotlin.Float){}[0]
final fun containsKey(kotlin/Float): kotlin/Boolean // androidx.collection/FloatIntMap.containsKey|containsKey(kotlin.Float){}[0]
final fun containsValue(kotlin/Int): kotlin/Boolean // androidx.collection/FloatIntMap.containsValue|containsValue(kotlin.Int){}[0]
final fun count(): kotlin/Int // androidx.collection/FloatIntMap.count|count(){}[0]
@@ -1561,6 +1560,7 @@
final fun none(): kotlin/Boolean // androidx.collection/FloatIntMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Float, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatIntMap.all|all(kotlin.Function2<kotlin.Float,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Float, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatIntMap.any|any(kotlin.Function2<kotlin.Float,kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatIntMap.contains|contains(kotlin.Float){}[0]
final inline fun count(kotlin/Function2<kotlin/Float, kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatIntMap.count|count(kotlin.Function2<kotlin.Float,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Float, kotlin/Int, kotlin/Unit>) // androidx.collection/FloatIntMap.forEach|forEach(kotlin.Function2<kotlin.Float,kotlin.Int,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/FloatIntMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1646,7 +1646,6 @@
final fun <set-values>(kotlin/LongArray) // androidx.collection/FloatLongMap.values.<set-values>|<set-values>(kotlin.LongArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/FloatLongMap.any|any(){}[0]
- final fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatLongMap.contains|contains(kotlin.Float){}[0]
final fun containsKey(kotlin/Float): kotlin/Boolean // androidx.collection/FloatLongMap.containsKey|containsKey(kotlin.Float){}[0]
final fun containsValue(kotlin/Long): kotlin/Boolean // androidx.collection/FloatLongMap.containsValue|containsValue(kotlin.Long){}[0]
final fun count(): kotlin/Int // androidx.collection/FloatLongMap.count|count(){}[0]
@@ -1659,6 +1658,7 @@
final fun none(): kotlin/Boolean // androidx.collection/FloatLongMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Float, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatLongMap.all|all(kotlin.Function2<kotlin.Float,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Float, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatLongMap.any|any(kotlin.Function2<kotlin.Float,kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatLongMap.contains|contains(kotlin.Float){}[0]
final inline fun count(kotlin/Function2<kotlin/Float, kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatLongMap.count|count(kotlin.Function2<kotlin.Float,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Float, kotlin/Long, kotlin/Unit>) // androidx.collection/FloatLongMap.forEach|forEach(kotlin.Function2<kotlin.Float,kotlin.Long,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/FloatLongMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1725,7 +1725,6 @@
final fun <set-values>(kotlin/FloatArray) // androidx.collection/IntFloatMap.values.<set-values>|<set-values>(kotlin.FloatArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/IntFloatMap.any|any(){}[0]
- final fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntFloatMap.contains|contains(kotlin.Int){}[0]
final fun containsKey(kotlin/Int): kotlin/Boolean // androidx.collection/IntFloatMap.containsKey|containsKey(kotlin.Int){}[0]
final fun containsValue(kotlin/Float): kotlin/Boolean // androidx.collection/IntFloatMap.containsValue|containsValue(kotlin.Float){}[0]
final fun count(): kotlin/Int // androidx.collection/IntFloatMap.count|count(){}[0]
@@ -1738,6 +1737,7 @@
final fun none(): kotlin/Boolean // androidx.collection/IntFloatMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Int, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntFloatMap.all|all(kotlin.Function2<kotlin.Int,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Int, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntFloatMap.any|any(kotlin.Function2<kotlin.Int,kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntFloatMap.contains|contains(kotlin.Int){}[0]
final inline fun count(kotlin/Function2<kotlin/Int, kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/IntFloatMap.count|count(kotlin.Function2<kotlin.Int,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Int, kotlin/Float, kotlin/Unit>) // androidx.collection/IntFloatMap.forEach|forEach(kotlin.Function2<kotlin.Int,kotlin.Float,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/IntFloatMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1769,7 +1769,6 @@
final fun <set-values>(kotlin/IntArray) // androidx.collection/IntIntMap.values.<set-values>|<set-values>(kotlin.IntArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/IntIntMap.any|any(){}[0]
- final fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntIntMap.contains|contains(kotlin.Int){}[0]
final fun containsKey(kotlin/Int): kotlin/Boolean // androidx.collection/IntIntMap.containsKey|containsKey(kotlin.Int){}[0]
final fun containsValue(kotlin/Int): kotlin/Boolean // androidx.collection/IntIntMap.containsValue|containsValue(kotlin.Int){}[0]
final fun count(): kotlin/Int // androidx.collection/IntIntMap.count|count(){}[0]
@@ -1782,6 +1781,7 @@
final fun none(): kotlin/Boolean // androidx.collection/IntIntMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntIntMap.all|all(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntIntMap.any|any(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntIntMap.contains|contains(kotlin.Int){}[0]
final inline fun count(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntIntMap.count|count(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // androidx.collection/IntIntMap.forEach|forEach(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/IntIntMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1867,7 +1867,6 @@
final fun <set-values>(kotlin/LongArray) // androidx.collection/IntLongMap.values.<set-values>|<set-values>(kotlin.LongArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/IntLongMap.any|any(){}[0]
- final fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntLongMap.contains|contains(kotlin.Int){}[0]
final fun containsKey(kotlin/Int): kotlin/Boolean // androidx.collection/IntLongMap.containsKey|containsKey(kotlin.Int){}[0]
final fun containsValue(kotlin/Long): kotlin/Boolean // androidx.collection/IntLongMap.containsValue|containsValue(kotlin.Long){}[0]
final fun count(): kotlin/Int // androidx.collection/IntLongMap.count|count(){}[0]
@@ -1880,6 +1879,7 @@
final fun none(): kotlin/Boolean // androidx.collection/IntLongMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Int, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntLongMap.all|all(kotlin.Function2<kotlin.Int,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Int, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntLongMap.any|any(kotlin.Function2<kotlin.Int,kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntLongMap.contains|contains(kotlin.Int){}[0]
final inline fun count(kotlin/Function2<kotlin/Int, kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/IntLongMap.count|count(kotlin.Function2<kotlin.Int,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Int, kotlin/Long, kotlin/Unit>) // androidx.collection/IntLongMap.forEach|forEach(kotlin.Function2<kotlin.Int,kotlin.Long,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/IntLongMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1946,7 +1946,6 @@
final fun <set-values>(kotlin/FloatArray) // androidx.collection/LongFloatMap.values.<set-values>|<set-values>(kotlin.FloatArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/LongFloatMap.any|any(){}[0]
- final fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongFloatMap.contains|contains(kotlin.Long){}[0]
final fun containsKey(kotlin/Long): kotlin/Boolean // androidx.collection/LongFloatMap.containsKey|containsKey(kotlin.Long){}[0]
final fun containsValue(kotlin/Float): kotlin/Boolean // androidx.collection/LongFloatMap.containsValue|containsValue(kotlin.Float){}[0]
final fun count(): kotlin/Int // androidx.collection/LongFloatMap.count|count(){}[0]
@@ -1959,6 +1958,7 @@
final fun none(): kotlin/Boolean // androidx.collection/LongFloatMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Long, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongFloatMap.all|all(kotlin.Function2<kotlin.Long,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Long, kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongFloatMap.any|any(kotlin.Function2<kotlin.Long,kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongFloatMap.contains|contains(kotlin.Long){}[0]
final inline fun count(kotlin/Function2<kotlin/Long, kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/LongFloatMap.count|count(kotlin.Function2<kotlin.Long,kotlin.Float,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Long, kotlin/Float, kotlin/Unit>) // androidx.collection/LongFloatMap.forEach|forEach(kotlin.Function2<kotlin.Long,kotlin.Float,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/LongFloatMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -1990,7 +1990,6 @@
final fun <set-values>(kotlin/IntArray) // androidx.collection/LongIntMap.values.<set-values>|<set-values>(kotlin.IntArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/LongIntMap.any|any(){}[0]
- final fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongIntMap.contains|contains(kotlin.Long){}[0]
final fun containsKey(kotlin/Long): kotlin/Boolean // androidx.collection/LongIntMap.containsKey|containsKey(kotlin.Long){}[0]
final fun containsValue(kotlin/Int): kotlin/Boolean // androidx.collection/LongIntMap.containsValue|containsValue(kotlin.Int){}[0]
final fun count(): kotlin/Int // androidx.collection/LongIntMap.count|count(){}[0]
@@ -2003,6 +2002,7 @@
final fun none(): kotlin/Boolean // androidx.collection/LongIntMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Long, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongIntMap.all|all(kotlin.Function2<kotlin.Long,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Long, kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongIntMap.any|any(kotlin.Function2<kotlin.Long,kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongIntMap.contains|contains(kotlin.Long){}[0]
final inline fun count(kotlin/Function2<kotlin/Long, kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/LongIntMap.count|count(kotlin.Function2<kotlin.Long,kotlin.Int,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Long, kotlin/Int, kotlin/Unit>) // androidx.collection/LongIntMap.forEach|forEach(kotlin.Function2<kotlin.Long,kotlin.Int,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/LongIntMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -2088,7 +2088,6 @@
final fun <set-values>(kotlin/LongArray) // androidx.collection/LongLongMap.values.<set-values>|<set-values>(kotlin.LongArray){}[0]
final fun any(): kotlin/Boolean // androidx.collection/LongLongMap.any|any(){}[0]
- final fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongLongMap.contains|contains(kotlin.Long){}[0]
final fun containsKey(kotlin/Long): kotlin/Boolean // androidx.collection/LongLongMap.containsKey|containsKey(kotlin.Long){}[0]
final fun containsValue(kotlin/Long): kotlin/Boolean // androidx.collection/LongLongMap.containsValue|containsValue(kotlin.Long){}[0]
final fun count(): kotlin/Int // androidx.collection/LongLongMap.count|count(){}[0]
@@ -2101,6 +2100,7 @@
final fun none(): kotlin/Boolean // androidx.collection/LongLongMap.none|none(){}[0]
final inline fun all(kotlin/Function2<kotlin/Long, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongLongMap.all|all(kotlin.Function2<kotlin.Long,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun any(kotlin/Function2<kotlin/Long, kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongLongMap.any|any(kotlin.Function2<kotlin.Long,kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongLongMap.contains|contains(kotlin.Long){}[0]
final inline fun count(kotlin/Function2<kotlin/Long, kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/LongLongMap.count|count(kotlin.Function2<kotlin.Long,kotlin.Long,kotlin.Boolean>){}[0]
final inline fun forEach(kotlin/Function2<kotlin/Long, kotlin/Long, kotlin/Unit>) // androidx.collection/LongLongMap.forEach|forEach(kotlin.Function2<kotlin.Long,kotlin.Long,kotlin.Unit>){}[0]
final inline fun forEachIndexed(kotlin/Function1<kotlin/Int, kotlin/Unit>) // androidx.collection/LongLongMap.forEachIndexed|forEachIndexed(kotlin.Function1<kotlin.Int,kotlin.Unit>){}[0]
@@ -2493,9 +2493,53 @@
final inline fun <#A: kotlin/Any?> (androidx.collection/SparseArrayCompat<#A>).androidx.collection/isNotEmpty(): kotlin/Boolean // androidx.collection/isNotEmpty|[email protected]<0:0>(){0§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?> (androidx.collection/SparseArrayCompat<#A>).androidx.collection/set(kotlin/Int, #A) // androidx.collection/set|[email protected]<0:0>(kotlin.Int;0:0){0§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?> androidx.collection/arraySetOf(): androidx.collection/ArraySet<#A> // androidx.collection/arraySetOf|arraySetOf(){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildFloatObjectMap(kotlin/Function1<androidx.collection/MutableFloatObjectMap<#A>, kotlin/Unit>): androidx.collection/FloatObjectMap<#A> // androidx.collection/buildFloatObjectMap|buildFloatObjectMap(kotlin.Function1<androidx.collection.MutableFloatObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildFloatObjectMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatObjectMap<#A>, kotlin/Unit>): androidx.collection/FloatObjectMap<#A> // androidx.collection/buildFloatObjectMap|buildFloatObjectMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildIntObjectMap(kotlin/Function1<androidx.collection/MutableIntObjectMap<#A>, kotlin/Unit>): androidx.collection/IntObjectMap<#A> // androidx.collection/buildIntObjectMap|buildIntObjectMap(kotlin.Function1<androidx.collection.MutableIntObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildIntObjectMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntObjectMap<#A>, kotlin/Unit>): androidx.collection/IntObjectMap<#A> // androidx.collection/buildIntObjectMap|buildIntObjectMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildLongObjectMap(kotlin/Function1<androidx.collection/MutableLongObjectMap<#A>, kotlin/Unit>): androidx.collection/LongObjectMap<#A> // androidx.collection/buildLongObjectMap|buildLongObjectMap(kotlin.Function1<androidx.collection.MutableLongObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildLongObjectMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongObjectMap<#A>, kotlin/Unit>): androidx.collection/LongObjectMap<#A> // androidx.collection/buildLongObjectMap|buildLongObjectMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongObjectMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectFloatMap(kotlin/Function1<androidx.collection/MutableObjectFloatMap<#A>, kotlin/Unit>): androidx.collection/ObjectFloatMap<#A> // androidx.collection/buildObjectFloatMap|buildObjectFloatMap(kotlin.Function1<androidx.collection.MutableObjectFloatMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectFloatMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableObjectFloatMap<#A>, kotlin/Unit>): androidx.collection/ObjectFloatMap<#A> // androidx.collection/buildObjectFloatMap|buildObjectFloatMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableObjectFloatMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectIntMap(kotlin/Function1<androidx.collection/MutableObjectIntMap<#A>, kotlin/Unit>): androidx.collection/ObjectIntMap<#A> // androidx.collection/buildObjectIntMap|buildObjectIntMap(kotlin.Function1<androidx.collection.MutableObjectIntMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectIntMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableObjectIntMap<#A>, kotlin/Unit>): androidx.collection/ObjectIntMap<#A> // androidx.collection/buildObjectIntMap|buildObjectIntMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableObjectIntMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectLongMap(kotlin/Function1<androidx.collection/MutableObjectLongMap<#A>, kotlin/Unit>): androidx.collection/ObjectLongMap<#A> // androidx.collection/buildObjectLongMap|buildObjectLongMap(kotlin.Function1<androidx.collection.MutableObjectLongMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
+final inline fun <#A: kotlin/Any?> androidx.collection/buildObjectLongMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableObjectLongMap<#A>, kotlin/Unit>): androidx.collection/ObjectLongMap<#A> // androidx.collection/buildObjectLongMap|buildObjectLongMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableObjectLongMap<0:0>,kotlin.Unit>){0§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?> androidx.collection/mutableObjectListOf(): androidx.collection/MutableObjectList<#A> // androidx.collection/mutableObjectListOf|mutableObjectListOf(){0§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?> androidx.collection/mutableObjectListOf(kotlin/Array<out #A>...): androidx.collection/MutableObjectList<#A> // androidx.collection/mutableObjectListOf|mutableObjectListOf(kotlin.Array<out|0:0>...){0§<kotlin.Any?>}[0]
final inline fun androidx.collection.internal/floatFromBits(kotlin/Int): kotlin/Float // androidx.collection.internal/floatFromBits|floatFromBits(kotlin.Int){}[0]
+final inline fun androidx.collection/buildDoubleList(kotlin/Function1<androidx.collection/MutableDoubleList, kotlin/Unit>): androidx.collection/DoubleList // androidx.collection/buildDoubleList|buildDoubleList(kotlin.Function1<androidx.collection.MutableDoubleList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildDoubleList(kotlin/Int, kotlin/Function1<androidx.collection/MutableDoubleList, kotlin/Unit>): androidx.collection/DoubleList // androidx.collection/buildDoubleList|buildDoubleList(kotlin.Int;kotlin.Function1<androidx.collection.MutableDoubleList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatFloatMap(kotlin/Function1<androidx.collection/MutableFloatFloatMap, kotlin/Unit>): androidx.collection/FloatFloatMap // androidx.collection/buildFloatFloatMap|buildFloatFloatMap(kotlin.Function1<androidx.collection.MutableFloatFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatFloatMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatFloatMap, kotlin/Unit>): androidx.collection/FloatFloatMap // androidx.collection/buildFloatFloatMap|buildFloatFloatMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatIntMap(kotlin/Function1<androidx.collection/MutableFloatIntMap, kotlin/Unit>): androidx.collection/FloatIntMap // androidx.collection/buildFloatIntMap|buildFloatIntMap(kotlin.Function1<androidx.collection.MutableFloatIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatIntMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatIntMap, kotlin/Unit>): androidx.collection/FloatIntMap // androidx.collection/buildFloatIntMap|buildFloatIntMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatList(kotlin/Function1<androidx.collection/MutableFloatList, kotlin/Unit>): androidx.collection/FloatList // androidx.collection/buildFloatList|buildFloatList(kotlin.Function1<androidx.collection.MutableFloatList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatList(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatList, kotlin/Unit>): androidx.collection/FloatList // androidx.collection/buildFloatList|buildFloatList(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatLongMap(kotlin/Function1<androidx.collection/MutableFloatLongMap, kotlin/Unit>): androidx.collection/FloatLongMap // androidx.collection/buildFloatLongMap|buildFloatLongMap(kotlin.Function1<androidx.collection.MutableFloatLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatLongMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatLongMap, kotlin/Unit>): androidx.collection/FloatLongMap // androidx.collection/buildFloatLongMap|buildFloatLongMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatSet(kotlin/Function1<androidx.collection/MutableFloatSet, kotlin/Unit>): androidx.collection/FloatSet // androidx.collection/buildFloatSet|buildFloatSet(kotlin.Function1<androidx.collection.MutableFloatSet,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildFloatSet(kotlin/Int, kotlin/Function1<androidx.collection/MutableFloatSet, kotlin/Unit>): androidx.collection/FloatSet // androidx.collection/buildFloatSet|buildFloatSet(kotlin.Int;kotlin.Function1<androidx.collection.MutableFloatSet,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntFloatMap(kotlin/Function1<androidx.collection/MutableIntFloatMap, kotlin/Unit>): androidx.collection/IntFloatMap // androidx.collection/buildIntFloatMap|buildIntFloatMap(kotlin.Function1<androidx.collection.MutableIntFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntFloatMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntFloatMap, kotlin/Unit>): androidx.collection/IntFloatMap // androidx.collection/buildIntFloatMap|buildIntFloatMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntIntMap(kotlin/Function1<androidx.collection/MutableIntIntMap, kotlin/Unit>): androidx.collection/IntIntMap // androidx.collection/buildIntIntMap|buildIntIntMap(kotlin.Function1<androidx.collection.MutableIntIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntIntMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntIntMap, kotlin/Unit>): androidx.collection/IntIntMap // androidx.collection/buildIntIntMap|buildIntIntMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntList(kotlin/Function1<androidx.collection/MutableIntList, kotlin/Unit>): androidx.collection/IntList // androidx.collection/buildIntList|buildIntList(kotlin.Function1<androidx.collection.MutableIntList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntList(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntList, kotlin/Unit>): androidx.collection/IntList // androidx.collection/buildIntList|buildIntList(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntLongMap(kotlin/Function1<androidx.collection/MutableIntLongMap, kotlin/Unit>): androidx.collection/IntLongMap // androidx.collection/buildIntLongMap|buildIntLongMap(kotlin.Function1<androidx.collection.MutableIntLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntLongMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntLongMap, kotlin/Unit>): androidx.collection/IntLongMap // androidx.collection/buildIntLongMap|buildIntLongMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntSet(kotlin/Function1<androidx.collection/MutableIntSet, kotlin/Unit>): androidx.collection/IntSet // androidx.collection/buildIntSet|buildIntSet(kotlin.Function1<androidx.collection.MutableIntSet,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildIntSet(kotlin/Int, kotlin/Function1<androidx.collection/MutableIntSet, kotlin/Unit>): androidx.collection/IntSet // androidx.collection/buildIntSet|buildIntSet(kotlin.Int;kotlin.Function1<androidx.collection.MutableIntSet,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongFloatMap(kotlin/Function1<androidx.collection/MutableLongFloatMap, kotlin/Unit>): androidx.collection/LongFloatMap // androidx.collection/buildLongFloatMap|buildLongFloatMap(kotlin.Function1<androidx.collection.MutableLongFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongFloatMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongFloatMap, kotlin/Unit>): androidx.collection/LongFloatMap // androidx.collection/buildLongFloatMap|buildLongFloatMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongFloatMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongIntMap(kotlin/Function1<androidx.collection/MutableLongIntMap, kotlin/Unit>): androidx.collection/LongIntMap // androidx.collection/buildLongIntMap|buildLongIntMap(kotlin.Function1<androidx.collection.MutableLongIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongIntMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongIntMap, kotlin/Unit>): androidx.collection/LongIntMap // androidx.collection/buildLongIntMap|buildLongIntMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongIntMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongList(kotlin/Function1<androidx.collection/MutableLongList, kotlin/Unit>): androidx.collection/LongList // androidx.collection/buildLongList|buildLongList(kotlin.Function1<androidx.collection.MutableLongList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongList(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongList, kotlin/Unit>): androidx.collection/LongList // androidx.collection/buildLongList|buildLongList(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongList,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongLongMap(kotlin/Function1<androidx.collection/MutableLongLongMap, kotlin/Unit>): androidx.collection/LongLongMap // androidx.collection/buildLongLongMap|buildLongLongMap(kotlin.Function1<androidx.collection.MutableLongLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongLongMap(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongLongMap, kotlin/Unit>): androidx.collection/LongLongMap // androidx.collection/buildLongLongMap|buildLongLongMap(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongLongMap,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongSet(kotlin/Function1<androidx.collection/MutableLongSet, kotlin/Unit>): androidx.collection/LongSet // androidx.collection/buildLongSet|buildLongSet(kotlin.Function1<androidx.collection.MutableLongSet,kotlin.Unit>){}[0]
+final inline fun androidx.collection/buildLongSet(kotlin/Int, kotlin/Function1<androidx.collection/MutableLongSet, kotlin/Unit>): androidx.collection/LongSet // androidx.collection/buildLongSet|buildLongSet(kotlin.Int;kotlin.Function1<androidx.collection.MutableLongSet,kotlin.Unit>){}[0]
final inline fun androidx.collection/isFull(kotlin/Long): kotlin/Boolean // androidx.collection/isFull|isFull(kotlin.Long){}[0]
final inline fun androidx.collection/mutableDoubleListOf(): androidx.collection/MutableDoubleList // androidx.collection/mutableDoubleListOf|mutableDoubleListOf(){}[0]
final inline fun androidx.collection/mutableDoubleListOf(kotlin/DoubleArray...): androidx.collection/MutableDoubleList // androidx.collection/mutableDoubleListOf|mutableDoubleListOf(kotlin.DoubleArray...){}[0]
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
index 5b23890..b213ddc 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
@@ -23,6 +23,7 @@
import androidx.collection.internal.throwIndexOutOfBoundsException
import androidx.collection.internal.throwNoSuchElementException
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -947,3 +948,35 @@
/** @return a new [MutableDoubleList] with the given elements, in order. */
public inline fun mutableDoubleListOf(vararg elements: Double): MutableDoubleList =
MutableDoubleList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Builds a new [DoubleList] by populating a [MutableDoubleList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableDoubleList] can be populated.
+ */
+public inline fun buildDoubleList(
+ builderAction: MutableDoubleList.() -> Unit,
+): DoubleList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableDoubleList().apply(builderAction)
+}
+
+/**
+ * Builds a new [DoubleList] by populating a [MutableDoubleList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableDoubleList] can be populated.
+ */
+public inline fun buildDoubleList(
+ initialCapacity: Int,
+ builderAction: MutableDoubleList.() -> Unit,
+): DoubleList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableDoubleList(initialCapacity).apply(builderAction)
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
index 072e286..9ef4cff 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,40 @@
}
/**
+ * Builds a new [FloatFloatMap] by populating a [MutableFloatFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatFloatMap] can be populated.
+ */
+public inline fun buildFloatFloatMap(
+ builderAction: MutableFloatFloatMap.() -> Unit,
+): FloatFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatFloatMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatFloatMap] by populating a [MutableFloatFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatFloatMap] can be populated.
+ */
+public inline fun buildFloatFloatMap(
+ initialCapacity: Int,
+ builderAction: MutableFloatFloatMap.() -> Unit,
+): FloatFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatFloatMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [FloatFloatMap] is a container with a [Map]-like interface for [Float] primitive keys and [Float]
* primitive values.
*
@@ -390,13 +428,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Float): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Float): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
index 1ff0eee..8d5f547 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [FloatIntMap] by populating a [MutableFloatIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatIntMap] can be populated.
+ */
+public inline fun buildFloatIntMap(
+ builderAction: MutableFloatIntMap.() -> Unit,
+): FloatIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatIntMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatIntMap] by populating a [MutableFloatIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatIntMap] can be populated.
+ */
+public inline fun buildFloatIntMap(
+ initialCapacity: Int,
+ builderAction: MutableFloatIntMap.() -> Unit,
+): FloatIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatIntMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [FloatIntMap] is a container with a [Map]-like interface for [Float] primitive keys and [Int]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Float): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Int): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
index 3bf1ac3..a35ba55 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -23,6 +23,7 @@
import androidx.collection.internal.throwIndexOutOfBoundsException
import androidx.collection.internal.throwNoSuchElementException
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -940,3 +941,35 @@
/** @return a new [MutableFloatList] with the given elements, in order. */
public inline fun mutableFloatListOf(vararg elements: Float): MutableFloatList =
MutableFloatList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Builds a new [FloatList] by populating a [MutableFloatList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatList] can be populated.
+ */
+public inline fun buildFloatList(
+ builderAction: MutableFloatList.() -> Unit,
+): FloatList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatList().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatList] by populating a [MutableFloatList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatList] can be populated.
+ */
+public inline fun buildFloatList(
+ initialCapacity: Int,
+ builderAction: MutableFloatList.() -> Unit,
+): FloatList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatList(initialCapacity).apply(builderAction)
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
index a60cdc7..5685c12 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,40 @@
}
/**
+ * Builds a new [FloatLongMap] by populating a [MutableFloatLongMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatLongMap] can be populated.
+ */
+public inline fun buildFloatLongMap(
+ builderAction: MutableFloatLongMap.() -> Unit,
+): FloatLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatLongMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatLongMap] by populating a [MutableFloatLongMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatLongMap] can be populated.
+ */
+public inline fun buildFloatLongMap(
+ initialCapacity: Int,
+ builderAction: MutableFloatLongMap.() -> Unit,
+): FloatLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatLongMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [FloatLongMap] is a container with a [Map]-like interface for [Float] primitive keys and [Long]
* primitive values.
*
@@ -390,13 +428,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Float): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Long): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
index bdb784d..4a93b9a 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -214,6 +218,40 @@
}
/**
+ * Builds a new [FloatObjectMap] by populating a [MutableFloatObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatObjectMap] can be populated.
+ */
+public inline fun <V> buildFloatObjectMap(
+ builderAction: MutableFloatObjectMap<V>.() -> Unit,
+): FloatObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatObjectMap<V>().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatObjectMap] by populating a [MutableFloatObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatObjectMap] can be populated.
+ */
+public inline fun <V> buildFloatObjectMap(
+ initialCapacity: Int,
+ builderAction: MutableFloatObjectMap<V>.() -> Unit,
+): FloatObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatObjectMap<V>(initialCapacity).apply(builderAction)
+}
+
+/**
* [FloatObjectMap] is a container with a [Map]-like interface for keys with [Float] primitives and
* reference type values.
*
@@ -383,13 +421,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Float): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: V): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index 0279ff4..bc8473b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -23,11 +23,14 @@
"PrivatePropertyName",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.annotation.IntRange
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -100,6 +103,38 @@
MutableFloatSet(elements.size).apply { plusAssign(elements) }
/**
+ * Builds a new [FloatSet] by populating a [MutableFloatSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableFloatSet] can be populated.
+ */
+public inline fun buildFloatSet(
+ builderAction: MutableFloatSet.() -> Unit,
+): FloatSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatSet().apply(builderAction)
+}
+
+/**
+ * Builds a new [FloatSet] by populating a [MutableFloatSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableFloatSet] can be populated.
+ */
+public inline fun buildFloatSet(
+ initialCapacity: Int,
+ builderAction: MutableFloatSet.() -> Unit,
+): FloatSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableFloatSet(initialCapacity).apply(builderAction)
+}
+
+/**
* [FloatSet] is a container with a [Set]-like interface designed to avoid allocations, including
* boxing.
*
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
index 10225ca..a1255ec 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [IntFloatMap] by populating a [MutableIntFloatMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntFloatMap] can be populated.
+ */
+public inline fun buildIntFloatMap(
+ builderAction: MutableIntFloatMap.() -> Unit,
+): IntFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntFloatMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntFloatMap] by populating a [MutableIntFloatMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntFloatMap] can be populated.
+ */
+public inline fun buildIntFloatMap(
+ initialCapacity: Int,
+ builderAction: MutableIntFloatMap.() -> Unit,
+): IntFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntFloatMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [IntFloatMap] is a container with a [Map]-like interface for [Int] primitive keys and [Float]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Int): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Float): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
index 9f98b11..6b913ad 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [IntIntMap] by populating a [MutableIntIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntIntMap] can be populated.
+ */
+public inline fun buildIntIntMap(
+ builderAction: MutableIntIntMap.() -> Unit,
+): IntIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntIntMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntIntMap] by populating a [MutableIntIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntIntMap] can be populated.
+ */
+public inline fun buildIntIntMap(
+ initialCapacity: Int,
+ builderAction: MutableIntIntMap.() -> Unit,
+): IntIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntIntMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [IntIntMap] is a container with a [Map]-like interface for [Int] primitive keys and [Int]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Int): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Int): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
index a4e6036..0edb357 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -23,6 +23,7 @@
import androidx.collection.internal.throwIndexOutOfBoundsException
import androidx.collection.internal.throwNoSuchElementException
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -932,3 +933,35 @@
/** @return a new [MutableIntList] with the given elements, in order. */
public inline fun mutableIntListOf(vararg elements: Int): MutableIntList =
MutableIntList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Builds a new [IntList] by populating a [MutableIntList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntList] can be populated.
+ */
+public inline fun buildIntList(
+ builderAction: MutableIntList.() -> Unit,
+): IntList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntList().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntList] by populating a [MutableIntList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntList] can be populated.
+ */
+public inline fun buildIntList(
+ initialCapacity: Int,
+ builderAction: MutableIntList.() -> Unit,
+): IntList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntList(initialCapacity).apply(builderAction)
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
index dedc3ae..87acb05 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [IntLongMap] by populating a [MutableIntLongMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntLongMap] can be populated.
+ */
+public inline fun buildIntLongMap(
+ builderAction: MutableIntLongMap.() -> Unit,
+): IntLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntLongMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntLongMap] by populating a [MutableIntLongMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntLongMap] can be populated.
+ */
+public inline fun buildIntLongMap(
+ initialCapacity: Int,
+ builderAction: MutableIntLongMap.() -> Unit,
+): IntLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntLongMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [IntLongMap] is a container with a [Map]-like interface for [Int] primitive keys and [Long]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Int): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Long): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
index ceab287..3e96e19 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -214,6 +218,40 @@
}
/**
+ * Builds a new [IntObjectMap] by populating a [MutableIntObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntObjectMap] can be populated.
+ */
+public inline fun <V> buildIntObjectMap(
+ builderAction: MutableIntObjectMap<V>.() -> Unit,
+): IntObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntObjectMap<V>().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntObjectMap] by populating a [MutableIntObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntObjectMap] can be populated.
+ */
+public inline fun <V> buildIntObjectMap(
+ initialCapacity: Int,
+ builderAction: MutableIntObjectMap<V>.() -> Unit,
+): IntObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntObjectMap<V>(initialCapacity).apply(builderAction)
+}
+
+/**
* [IntObjectMap] is a container with a [Map]-like interface for keys with [Int] primitives and
* reference type values.
*
@@ -383,13 +421,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Int): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: V): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index 036f203..ac70e0e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -23,11 +23,14 @@
"PrivatePropertyName",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.annotation.IntRange
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -98,6 +101,38 @@
MutableIntSet(elements.size).apply { plusAssign(elements) }
/**
+ * Builds a new [IntSet] by populating a [MutableIntSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableIntSet] can be populated.
+ */
+public inline fun buildIntSet(
+ builderAction: MutableIntSet.() -> Unit,
+): IntSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntSet().apply(builderAction)
+}
+
+/**
+ * Builds a new [IntSet] by populating a [MutableIntSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableIntSet] can be populated.
+ */
+public inline fun buildIntSet(
+ initialCapacity: Int,
+ builderAction: MutableIntSet.() -> Unit,
+): IntSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableIntSet(initialCapacity).apply(builderAction)
+}
+
+/**
* [IntSet] is a container with a [Set]-like interface designed to avoid allocations, including
* boxing.
*
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
index bb34619..16d6b23 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,40 @@
}
/**
+ * Builds a new [LongFloatMap] by populating a [MutableLongFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongFloatMap] can be populated.
+ */
+public inline fun buildLongFloatMap(
+ builderAction: MutableLongFloatMap.() -> Unit,
+): LongFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongFloatMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongFloatMap] by populating a [MutableLongFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongFloatMap] can be populated.
+ */
+public inline fun buildLongFloatMap(
+ initialCapacity: Int,
+ builderAction: MutableLongFloatMap.() -> Unit,
+): LongFloatMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongFloatMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [LongFloatMap] is a container with a [Map]-like interface for [Long] primitive keys and [Float]
* primitive values.
*
@@ -390,13 +428,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Long): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Float): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
index a9dc213..cefe009 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [LongIntMap] by populating a [MutableLongIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongIntMap] can be populated.
+ */
+public inline fun buildLongIntMap(
+ builderAction: MutableLongIntMap.() -> Unit,
+): LongIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongIntMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongIntMap] by populating a [MutableLongIntMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongIntMap] can be populated.
+ */
+public inline fun buildLongIntMap(
+ initialCapacity: Int,
+ builderAction: MutableLongIntMap.() -> Unit,
+): LongIntMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongIntMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [LongIntMap] is a container with a [Map]-like interface for [Long] primitive keys and [Int]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Long): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Int): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
index 8f4a32e..927f1f0 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -23,6 +23,7 @@
import androidx.collection.internal.throwIndexOutOfBoundsException
import androidx.collection.internal.throwNoSuchElementException
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -936,3 +937,35 @@
/** @return a new [MutableLongList] with the given elements, in order. */
public inline fun mutableLongListOf(vararg elements: Long): MutableLongList =
MutableLongList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Builds a new [LongList] by populating a [MutableLongList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongList] can be populated.
+ */
+public inline fun buildLongList(
+ builderAction: MutableLongList.() -> Unit,
+): LongList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongList().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongList] by populating a [MutableLongList] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongList] can be populated.
+ */
+public inline fun buildLongList(
+ initialCapacity: Int,
+ builderAction: MutableLongList.() -> Unit,
+): LongList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongList(initialCapacity).apply(builderAction)
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
index dbe6771..5ed07fa 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -212,6 +216,38 @@
}
/**
+ * Builds a new [LongLongMap] by populating a [MutableLongLongMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongLongMap] can be populated.
+ */
+public inline fun buildLongLongMap(
+ builderAction: MutableLongLongMap.() -> Unit,
+): LongLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongLongMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongLongMap] by populating a [MutableLongLongMap] using the given [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongLongMap] can be populated.
+ */
+public inline fun buildLongLongMap(
+ initialCapacity: Int,
+ builderAction: MutableLongLongMap.() -> Unit,
+): LongLongMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongLongMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [LongLongMap] is a container with a [Map]-like interface for [Long] primitive keys and [Long]
* primitive values.
*
@@ -390,13 +426,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Long): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Long): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
index eb674d9..b2168a9 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -15,11 +15,15 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -214,6 +218,40 @@
}
/**
+ * Builds a new [LongObjectMap] by populating a [MutableLongObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongObjectMap] can be populated.
+ */
+public inline fun <V> buildLongObjectMap(
+ builderAction: MutableLongObjectMap<V>.() -> Unit,
+): LongObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongObjectMap<V>().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongObjectMap] by populating a [MutableLongObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongObjectMap] can be populated.
+ */
+public inline fun <V> buildLongObjectMap(
+ initialCapacity: Int,
+ builderAction: MutableLongObjectMap<V>.() -> Unit,
+): LongObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongObjectMap<V>(initialCapacity).apply(builderAction)
+}
+
+/**
* [LongObjectMap] is a container with a [Map]-like interface for keys with [Long] primitives and
* reference type values.
*
@@ -383,13 +421,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: Long): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: V): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index 675d18e..09f75dc 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -23,11 +23,14 @@
"PrivatePropertyName",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.annotation.IntRange
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -99,6 +102,38 @@
MutableLongSet(elements.size).apply { plusAssign(elements) }
/**
+ * Builds a new [LongSet] by populating a [MutableLongSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableLongSet] can be populated.
+ */
+public inline fun buildLongSet(
+ builderAction: MutableLongSet.() -> Unit,
+): LongSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongSet().apply(builderAction)
+}
+
+/**
+ * Builds a new [LongSet] by populating a [MutableLongSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableLongSet] can be populated.
+ */
+public inline fun buildLongSet(
+ initialCapacity: Int,
+ builderAction: MutableLongSet.() -> Unit,
+): LongSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableLongSet(initialCapacity).apply(builderAction)
+}
+
+/**
* [LongSet] is a container with a [Set]-like interface designed to avoid allocations, including
* boxing.
*
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
index 6c8b64f..c555fed 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -15,12 +15,16 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -217,6 +221,40 @@
}
/**
+ * Builds a new [ObjectFloatMap] by populating a [MutableObjectFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableObjectFloatMap] can be populated.
+ */
+public inline fun <K> buildObjectFloatMap(
+ builderAction: MutableObjectFloatMap<K>.() -> Unit,
+): ObjectFloatMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectFloatMap<K>().apply(builderAction)
+}
+
+/**
+ * Builds a new [ObjectFloatMap] by populating a [MutableObjectFloatMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableObjectFloatMap] can be populated.
+ */
+public inline fun <K> buildObjectFloatMap(
+ initialCapacity: Int,
+ builderAction: MutableObjectFloatMap<K>.() -> Unit,
+): ObjectFloatMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectFloatMap<K>(initialCapacity).apply(builderAction)
+}
+
+/**
* [ObjectFloatMap] is a container with a [Map]-like interface for keys with reference types and
* [Float] primitives for values.
*
@@ -396,13 +434,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: K): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Float): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
index ec7ec4b..2dba86b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -15,12 +15,16 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -217,6 +221,40 @@
}
/**
+ * Builds a new [ObjectIntMap] by populating a [MutableObjectIntMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableObjectIntMap] can be populated.
+ */
+public inline fun <K> buildObjectIntMap(
+ builderAction: MutableObjectIntMap<K>.() -> Unit,
+): ObjectIntMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectIntMap<K>().apply(builderAction)
+}
+
+/**
+ * Builds a new [ObjectIntMap] by populating a [MutableObjectIntMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableObjectIntMap] can be populated.
+ */
+public inline fun <K> buildObjectIntMap(
+ initialCapacity: Int,
+ builderAction: MutableObjectIntMap<K>.() -> Unit,
+): ObjectIntMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectIntMap<K>(initialCapacity).apply(builderAction)
+}
+
+/**
* [ObjectIntMap] is a container with a [Map]-like interface for keys with reference types and [Int]
* primitives for values.
*
@@ -396,13 +434,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: K): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Int): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
index 3622689..9077f59 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -15,12 +15,16 @@
*/
@file:Suppress("RedundantVisibilityModifier", "NOTHING_TO_INLINE")
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -217,6 +221,40 @@
}
/**
+ * Builds a new [ObjectLongMap] by populating a [MutableObjectLongMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableObjectLongMap] can be populated.
+ */
+public inline fun <K> buildObjectLongMap(
+ builderAction: MutableObjectLongMap<K>.() -> Unit,
+): ObjectLongMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectLongMap<K>().apply(builderAction)
+}
+
+/**
+ * Builds a new [ObjectLongMap] by populating a [MutableObjectLongMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableObjectLongMap] can be populated.
+ */
+public inline fun <K> buildObjectLongMap(
+ initialCapacity: Int,
+ builderAction: MutableObjectLongMap<K>.() -> Unit,
+): ObjectLongMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectLongMap<K>(initialCapacity).apply(builderAction)
+}
+
+/**
* [ObjectLongMap] is a container with a [Map]-like interface for keys with reference types and
* [Long] primitives for values.
*
@@ -396,13 +434,13 @@
return count
}
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
- public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
+ public inline operator fun contains(key: K): Boolean = containsKey(key)
- /** Returns true if the specified [key] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [key] is present in this map, false otherwise. */
public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
- /** Returns true if the specified [value] is present in this hash map, false otherwise. */
+ /** Returns true if the specified [value] is present in this map, false otherwise. */
public fun containsValue(value: Long): Boolean {
forEachValue { v -> if (value == v) return true }
return false
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
index 4c324c7..e3bde2e 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
@@ -715,6 +715,36 @@
}
@Test
+ fun buildDoubleListFunction() {
+ val contract: Boolean
+ val l = buildDoubleList {
+ contract = true
+ add(2.0)
+ add(10.0)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertEquals(2.0, l[0])
+ assertEquals(10.0, l[1])
+ }
+
+ @Test
+ fun buildDoubleListWithCapacityFunction() {
+ val contract: Boolean
+ val l =
+ buildDoubleList(20) {
+ contract = true
+ add(2.0)
+ add(10.0)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertTrue(l.content.size >= 20)
+ assertEquals(2.0, l[0])
+ assertEquals(10.0, l[1])
+ }
+
+ @Test
fun binarySearchDoubleList() {
val l = mutableDoubleListOf(-2.0, -1.0, 2.0, 10.0, 10.0)
assertEquals(0, l.binarySearch(-2))
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
index c6b52ef..f8068cc 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildFloatFloatMapFunction() {
+ val contract: Boolean
+ val map = buildFloatFloatMap {
+ contract = true
+ put(1f, 1f)
+ put(2f, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1f])
+ assertEquals(2f, map[2f])
+ }
+
+ @Test
+ fun buildFloatObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildFloatFloatMap(20) {
+ contract = true
+ put(1f, 1f)
+ put(2f, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1f, map[1f])
+ assertEquals(2f, map[2f])
+ }
+
+ @Test
fun addToMap() {
val map = MutableFloatFloatMap()
map[1f] = 1f
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
index 4d99c61..0e116a2 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildFloatIntMapFunction() {
+ val contract: Boolean
+ val map = buildFloatIntMap {
+ contract = true
+ put(1f, 1)
+ put(2f, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1, map[1f])
+ assertEquals(2, map[2f])
+ }
+
+ @Test
+ fun buildFloatObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildFloatIntMap(20) {
+ contract = true
+ put(1f, 1)
+ put(2f, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1, map[1f])
+ assertEquals(2, map[2f])
+ }
+
+ @Test
fun addToMap() {
val map = MutableFloatIntMap()
map[1f] = 1
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
index e1b4649c..17b2a9d 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -715,6 +715,36 @@
}
@Test
+ fun buildFloatListFunction() {
+ val contract: Boolean
+ val l = buildFloatList {
+ contract = true
+ add(2f)
+ add(10f)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertEquals(2f, l[0])
+ assertEquals(10f, l[1])
+ }
+
+ @Test
+ fun buildFloatListWithCapacityFunction() {
+ val contract: Boolean
+ val l =
+ buildFloatList(20) {
+ contract = true
+ add(2f)
+ add(10f)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertTrue(l.content.size >= 20)
+ assertEquals(2f, l[0])
+ assertEquals(10f, l[1])
+ }
+
+ @Test
fun binarySearchFloatList() {
val l = mutableFloatListOf(-2f, -1f, 2f, 10f, 10f)
assertEquals(0, l.binarySearch(-2))
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
index 6029c51..a307716 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildFloatLongMapFunction() {
+ val contract: Boolean
+ val map = buildFloatLongMap {
+ contract = true
+ put(1f, 1L)
+ put(2f, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1f])
+ assertEquals(2L, map[2f])
+ }
+
+ @Test
+ fun buildFloatObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildFloatLongMap(20) {
+ contract = true
+ put(1f, 1L)
+ put(2f, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1L, map[1f])
+ assertEquals(2L, map[2f])
+ }
+
+ @Test
fun addToMap() {
val map = MutableFloatLongMap()
map[1f] = 1L
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
index 572dd68..f0e20dc 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildFloatObjectMapFunction() {
+ val contract: Boolean
+ val map = buildFloatObjectMap {
+ contract = true
+ put(1f, "World")
+ put(2f, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals("World", map[1f])
+ assertEquals("Monde", map[2f])
+ }
+
+ @Test
+ fun buildFloatObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildFloatObjectMap(20) {
+ contract = true
+ put(1f, "World")
+ put(2f, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals("World", map[1f])
+ assertEquals("Monde", map[2f])
+ }
+
+ @Test
fun addToMap() {
val map = MutableFloatObjectMap<String>()
map[1f] = "World"
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
index e5a105e..70d38a5 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -552,6 +552,36 @@
}
@Test
+ fun buildFloatSetFunction() {
+ val contract: Boolean
+ val set = buildFloatSet {
+ contract = true
+ add(1f)
+ add(2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(1f in set)
+ assertTrue(2f in set)
+ }
+
+ @Test
+ fun buildFloatSetWithCapacityFunction() {
+ val contract: Boolean
+ val set =
+ buildFloatSet(20) {
+ contract = true
+ add(1f)
+ add(2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(set.capacity >= 18)
+ assertTrue(1f in set)
+ assertTrue(2f in set)
+ }
+
+ @Test
fun insertManyRemoveMany() {
val set = mutableFloatSetOf()
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
index cf9a934..eb3ea90 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildIntFloatMapFunction() {
+ val contract: Boolean
+ val map = buildIntFloatMap {
+ contract = true
+ put(1, 1f)
+ put(2, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1])
+ assertEquals(2f, map[2])
+ }
+
+ @Test
+ fun buildIntObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildIntFloatMap(20) {
+ contract = true
+ put(1, 1f)
+ put(2, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1f, map[1])
+ assertEquals(2f, map[2])
+ }
+
+ @Test
fun addToMap() {
val map = MutableIntFloatMap()
map[1] = 1f
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
index b7d318b..4a2691d 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildIntIntMapFunction() {
+ val contract: Boolean
+ val map = buildIntIntMap {
+ contract = true
+ put(1, 1)
+ put(2, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1, map[1])
+ assertEquals(2, map[2])
+ }
+
+ @Test
+ fun buildIntObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildIntIntMap(20) {
+ contract = true
+ put(1, 1)
+ put(2, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1, map[1])
+ assertEquals(2, map[2])
+ }
+
+ @Test
fun addToMap() {
val map = MutableIntIntMap()
map[1] = 1
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
index cdc0cf9..6023ec2 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -715,6 +715,36 @@
}
@Test
+ fun buildIntListFunction() {
+ val contract: Boolean
+ val l = buildIntList {
+ contract = true
+ add(2)
+ add(10)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ }
+
+ @Test
+ fun buildIntListWithCapacityFunction() {
+ val contract: Boolean
+ val l =
+ buildIntList(20) {
+ contract = true
+ add(2)
+ add(10)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertTrue(l.content.size >= 20)
+ assertEquals(2, l[0])
+ assertEquals(10, l[1])
+ }
+
+ @Test
fun binarySearchIntList() {
val l = mutableIntListOf(-2, -1, 2, 10, 10)
assertEquals(0, l.binarySearch(-2))
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
index e115dc6..7256aa0 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildIntLongMapFunction() {
+ val contract: Boolean
+ val map = buildIntLongMap {
+ contract = true
+ put(1, 1L)
+ put(2, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1])
+ assertEquals(2L, map[2])
+ }
+
+ @Test
+ fun buildIntObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildIntLongMap(20) {
+ contract = true
+ put(1, 1L)
+ put(2, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1L, map[1])
+ assertEquals(2L, map[2])
+ }
+
+ @Test
fun addToMap() {
val map = MutableIntLongMap()
map[1] = 1L
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
index 9d002e6..2193d75 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildIntObjectMapFunction() {
+ val contract: Boolean
+ val map = buildIntObjectMap {
+ contract = true
+ put(1, "World")
+ put(2, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals("World", map[1])
+ assertEquals("Monde", map[2])
+ }
+
+ @Test
+ fun buildIntObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildIntObjectMap(20) {
+ contract = true
+ put(1, "World")
+ put(2, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals("World", map[1])
+ assertEquals("Monde", map[2])
+ }
+
+ @Test
fun addToMap() {
val map = MutableIntObjectMap<String>()
map[1] = "World"
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
index 66c30d3..46a4c63 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -552,6 +552,36 @@
}
@Test
+ fun buildIntSetFunction() {
+ val contract: Boolean
+ val set = buildIntSet {
+ contract = true
+ add(1)
+ add(2)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(1 in set)
+ assertTrue(2 in set)
+ }
+
+ @Test
+ fun buildIntSetWithCapacityFunction() {
+ val contract: Boolean
+ val set =
+ buildIntSet(20) {
+ contract = true
+ add(1)
+ add(2)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(set.capacity >= 18)
+ assertTrue(1 in set)
+ assertTrue(2 in set)
+ }
+
+ @Test
fun insertManyRemoveMany() {
val set = mutableIntSetOf()
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
index 507ddf0..4201211 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildLongFloatMapFunction() {
+ val contract: Boolean
+ val map = buildLongFloatMap {
+ contract = true
+ put(1L, 1f)
+ put(2L, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1f, map[1L])
+ assertEquals(2f, map[2L])
+ }
+
+ @Test
+ fun buildLongObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildLongFloatMap(20) {
+ contract = true
+ put(1L, 1f)
+ put(2L, 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1f, map[1L])
+ assertEquals(2f, map[2L])
+ }
+
+ @Test
fun addToMap() {
val map = MutableLongFloatMap()
map[1L] = 1f
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
index 75488a2..568fc7a 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildLongIntMapFunction() {
+ val contract: Boolean
+ val map = buildLongIntMap {
+ contract = true
+ put(1L, 1)
+ put(2L, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1, map[1L])
+ assertEquals(2, map[2L])
+ }
+
+ @Test
+ fun buildLongObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildLongIntMap(20) {
+ contract = true
+ put(1L, 1)
+ put(2L, 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1, map[1L])
+ assertEquals(2, map[2L])
+ }
+
+ @Test
fun addToMap() {
val map = MutableLongIntMap()
map[1L] = 1
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
index af0536e..64019c1 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -715,6 +715,36 @@
}
@Test
+ fun buildLongListFunction() {
+ val contract: Boolean
+ val l = buildLongList {
+ contract = true
+ add(2L)
+ add(10L)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertEquals(2L, l[0])
+ assertEquals(10L, l[1])
+ }
+
+ @Test
+ fun buildLongListWithCapacityFunction() {
+ val contract: Boolean
+ val l =
+ buildLongList(20) {
+ contract = true
+ add(2L)
+ add(10L)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertTrue(l.content.size >= 20)
+ assertEquals(2L, l[0])
+ assertEquals(10L, l[1])
+ }
+
+ @Test
fun binarySearchLongList() {
val l = mutableLongListOf(-2L, -1L, 2L, 10L, 10L)
assertEquals(0, l.binarySearch(-2))
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
index a22631b..6af2158 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildLongLongMapFunction() {
+ val contract: Boolean
+ val map = buildLongLongMap {
+ contract = true
+ put(1L, 1L)
+ put(2L, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1L, map[1L])
+ assertEquals(2L, map[2L])
+ }
+
+ @Test
+ fun buildLongObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildLongLongMap(20) {
+ contract = true
+ put(1L, 1L)
+ put(2L, 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1L, map[1L])
+ assertEquals(2L, map[2L])
+ }
+
+ @Test
fun addToMap() {
val map = MutableLongLongMap()
map[1L] = 1L
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
index f8be517..3b6747d 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
@@ -228,6 +228,36 @@
}
@Test
+ fun buildLongObjectMapFunction() {
+ val contract: Boolean
+ val map = buildLongObjectMap {
+ contract = true
+ put(1L, "World")
+ put(2L, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals("World", map[1L])
+ assertEquals("Monde", map[2L])
+ }
+
+ @Test
+ fun buildLongObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildLongObjectMap(20) {
+ contract = true
+ put(1L, "World")
+ put(2L, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals("World", map[1L])
+ assertEquals("Monde", map[2L])
+ }
+
+ @Test
fun addToMap() {
val map = MutableLongObjectMap<String>()
map[1L] = "World"
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
index dc5b7e7..577e722e 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -552,6 +552,36 @@
}
@Test
+ fun buildLongSetFunction() {
+ val contract: Boolean
+ val set = buildLongSet {
+ contract = true
+ add(1L)
+ add(2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(1L in set)
+ assertTrue(2L in set)
+ }
+
+ @Test
+ fun buildLongSetWithCapacityFunction() {
+ val contract: Boolean
+ val set =
+ buildLongSet(20) {
+ contract = true
+ add(1L)
+ add(2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(set.capacity >= 18)
+ assertTrue(1L in set)
+ assertTrue(2L in set)
+ }
+
+ @Test
fun insertManyRemoveMany() {
val set = mutableLongSetOf()
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
index 90244ab..19c1cc6 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
@@ -229,6 +229,36 @@
}
@Test
+ fun buildObjectFloatMapFunction() {
+ val contract: Boolean
+ val map = buildObjectFloatMap {
+ contract = true
+ put("Hello", 1f)
+ put("Bonjour", 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1f, map["Hello"])
+ assertEquals(2f, map["Bonjour"])
+ }
+
+ @Test
+ fun buildObjectFloatMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildObjectFloatMap(20) {
+ contract = true
+ put("Hello", 1f)
+ put("Bonjour", 2f)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1f, map["Hello"])
+ assertEquals(2f, map["Bonjour"])
+ }
+
+ @Test
fun addToMap() {
val map = MutableObjectFloatMap<String>()
map["Hello"] = 1f
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
index bc640eb..62b7a2c 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
@@ -229,6 +229,36 @@
}
@Test
+ fun buildObjectIntMapFunction() {
+ val contract: Boolean
+ val map = buildObjectIntMap {
+ contract = true
+ put("Hello", 1)
+ put("Bonjour", 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1, map["Hello"])
+ assertEquals(2, map["Bonjour"])
+ }
+
+ @Test
+ fun buildObjectIntMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildObjectIntMap(20) {
+ contract = true
+ put("Hello", 1)
+ put("Bonjour", 2)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1, map["Hello"])
+ assertEquals(2, map["Bonjour"])
+ }
+
+ @Test
fun addToMap() {
val map = MutableObjectIntMap<String>()
map["Hello"] = 1
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
index f728b95..6eba1a6 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
@@ -229,6 +229,36 @@
}
@Test
+ fun buildObjectLongMapFunction() {
+ val contract: Boolean
+ val map = buildObjectLongMap {
+ contract = true
+ put("Hello", 1L)
+ put("Bonjour", 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1L, map["Hello"])
+ assertEquals(2L, map["Bonjour"])
+ }
+
+ @Test
+ fun buildObjectLongMapWithCapacityFunction() {
+ val contract: Boolean
+ val map =
+ buildObjectLongMap(20) {
+ contract = true
+ put("Hello", 1L)
+ put("Bonjour", 2L)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1L, map["Hello"])
+ assertEquals(2L, map["Bonjour"])
+ }
+
+ @Test
fun addToMap() {
val map = MutableObjectLongMap<String>()
map["Hello"] = 1L
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
index bfc791c..878080f 100644
--- a/collection/collection/template/ObjectPValueMap.kt.template
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -18,12 +18,16 @@
"RedundantVisibilityModifier",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -240,6 +244,40 @@
}
/**
+ * Builds a new [ObjectPValueMap] by populating a [MutableObjectPValueMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableObjectPValueMap] can be populated.
+ */
+public inline fun <K> buildObjectPValueMap(
+ builderAction: MutableObjectPValueMap<K>.() -> Unit,
+): ObjectPValueMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectPValueMap<K>().apply(builderAction)
+}
+
+/**
+ * Builds a new [ObjectPValueMap] by populating a [MutableObjectPValueMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableObjectPValueMap] can be populated.
+ */
+public inline fun <K> buildObjectPValueMap(
+ initialCapacity: Int,
+ builderAction: MutableObjectPValueMap<K>.() -> Unit,
+): ObjectPValueMap<K> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableObjectPValueMap<K>(initialCapacity).apply(builderAction)
+}
+
+/**
* [ObjectPValueMap] is a container with a [Map]-like interface for keys with
* reference types and [PValue] primitives for values.
*
@@ -470,19 +508,19 @@
}
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
- public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+ public inline operator fun contains(key: K): Boolean = containsKey(key)
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
/**
- * Returns true if the specified [value] is present in this hash map, false
+ * Returns true if the specified [value] is present in this map, false
* otherwise.
*/
public fun containsValue(value: PValue): Boolean {
diff --git a/collection/collection/template/ObjectPValueMapTest.kt.template b/collection/collection/template/ObjectPValueMapTest.kt.template
index 988abb3..c84db2d 100644
--- a/collection/collection/template/ObjectPValueMapTest.kt.template
+++ b/collection/collection/template/ObjectPValueMapTest.kt.template
@@ -189,6 +189,35 @@
}
@Test
+ fun buildObjectPValueMapFunction() {
+ val contract: Boolean
+ val map = buildObjectPValueMap {
+ contract = true
+ put("Hello", 1ValueSuffix)
+ put("Bonjour", 2ValueSuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1ValueSuffix, map["Hello"])
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ }
+
+ @Test
+ fun buildObjectPValueMapWithCapacityFunction() {
+ val contract: Boolean
+ val map = buildObjectPValueMap(20) {
+ contract = true
+ put("Hello", 1ValueSuffix)
+ put("Bonjour", 2ValueSuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1ValueSuffix, map["Hello"])
+ assertEquals(2ValueSuffix, map["Bonjour"])
+ }
+
+ @Test
fun addToMap() {
val map = MutableObjectPValueMap<String>()
map["Hello"] = 1ValueSuffix
diff --git a/collection/collection/template/PKeyList.kt.template b/collection/collection/template/PKeyList.kt.template
index 03233b7..61a2ff2 100644
--- a/collection/collection/template/PKeyList.kt.template
+++ b/collection/collection/template/PKeyList.kt.template
@@ -23,6 +23,7 @@
import androidx.collection.internal.throwIndexOutOfBoundsException
import androidx.collection.internal.throwNoSuchElementException
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -1012,3 +1013,37 @@
*/
public inline fun mutablePKeyListOf(vararg elements: PKey): MutablePKeyList =
MutablePKeyList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Builds a new [PKeyList] by populating a [MutablePKeyList] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutablePKeyList] can be populated.
+ */
+public inline fun buildPKeyList(
+ builderAction: MutablePKeyList.() -> Unit,
+): PKeyList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyList().apply(builderAction)
+}
+
+/**
+ * Builds a new [PKeyList] by populating a [MutablePKeyList] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutablePKeyList] can be populated.
+ */
+public inline fun buildPKeyList(
+ initialCapacity: Int,
+ builderAction: MutablePKeyList.() -> Unit,
+): PKeyList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyList(initialCapacity).apply(builderAction)
+}
diff --git a/collection/collection/template/PKeyListTest.kt.template b/collection/collection/template/PKeyListTest.kt.template
index 8f3d8e0..417c489 100644
--- a/collection/collection/template/PKeyListTest.kt.template
+++ b/collection/collection/template/PKeyListTest.kt.template
@@ -751,6 +751,35 @@
}
@Test
+ fun buildPKeyListFunction() {
+ val contract: Boolean
+ val l = buildPKeyList {
+ contract = true
+ add(2KeySuffix)
+ add(10KeySuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ }
+
+ @Test
+ fun buildPKeyListWithCapacityFunction() {
+ val contract: Boolean
+ val l = buildPKeyList(20) {
+ contract = true
+ add(2KeySuffix)
+ add(10KeySuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, l.size)
+ assertTrue(l.content.size >= 20)
+ assertEquals(2KeySuffix, l[0])
+ assertEquals(10KeySuffix, l[1])
+ }
+
+ @Test
fun binarySearchPKeyList() {
val l = mutablePKeyListOf(-2KeySuffix, -1KeySuffix, 2KeySuffix, 10KeySuffix, 10KeySuffix)
assertEquals(0, l.binarySearch(-2))
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
index 39bb237..aa140dd5 100644
--- a/collection/collection/template/PKeyObjectMap.kt.template
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -18,11 +18,15 @@
"RedundantVisibilityModifier",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.requirePrecondition
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -227,6 +231,40 @@
}
/**
+ * Builds a new [PKeyObjectMap] by populating a [MutablePKeyObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutablePKeyObjectMap] can be populated.
+ */
+public inline fun <V> buildPKeyObjectMap(
+ builderAction: MutablePKeyObjectMap<V>.() -> Unit,
+): PKeyObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyObjectMap<V>().apply(builderAction)
+}
+
+/**
+ * Builds a new [PKeyObjectMap] by populating a [MutablePKeyObjectMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutablePKeyObjectMap] can be populated.
+ */
+public inline fun <V> buildPKeyObjectMap(
+ initialCapacity: Int,
+ builderAction: MutablePKeyObjectMap<V>.() -> Unit,
+): PKeyObjectMap<V> {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyObjectMap<V>(initialCapacity).apply(builderAction)
+}
+
+/**
* [PKeyObjectMap] is a container with a [Map]-like interface for keys with
* [PKey] primitives and reference type values.
*
@@ -451,19 +489,19 @@
}
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
- public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+ public inline operator fun contains(key: PKey): Boolean = containsKey(key)
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
/**
- * Returns true if the specified [value] is present in this hash map, false
+ * Returns true if the specified [value] is present in this map, false
* otherwise.
*/
public fun containsValue(value: V): Boolean {
diff --git a/collection/collection/template/PKeyObjectMapTest.kt.template b/collection/collection/template/PKeyObjectMapTest.kt.template
index ea5cfb8..de9c4ff 100644
--- a/collection/collection/template/PKeyObjectMapTest.kt.template
+++ b/collection/collection/template/PKeyObjectMapTest.kt.template
@@ -188,6 +188,35 @@
}
@Test
+ fun buildPKeyObjectMapFunction() {
+ val contract: Boolean
+ val map = buildPKeyObjectMap {
+ contract = true
+ put(1KeySuffix, "World")
+ put(2KeySuffix, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals("World", map[1KeySuffix])
+ assertEquals("Monde", map[2KeySuffix])
+ }
+
+ @Test
+ fun buildPKeyObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map = buildPKeyObjectMap(20) {
+ contract = true
+ put(1KeySuffix, "World")
+ put(2KeySuffix, "Monde")
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals("World", map[1KeySuffix])
+ assertEquals("Monde", map[2KeySuffix])
+ }
+
+ @Test
fun addToMap() {
val map = MutablePKeyObjectMap<String>()
map[1KeySuffix] = "World"
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
index 524dbe7..61ac9d8 100644
--- a/collection/collection/template/PKeyPValueMap.kt.template
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -18,11 +18,15 @@
"RedundantVisibilityModifier",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -225,6 +229,40 @@
}
/**
+ * Builds a new [PKeyPValueMap] by populating a [MutablePKeyPValueMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutablePKeyPValueMap] can be populated.
+ */
+public inline fun buildPKeyPValueMap(
+ builderAction: MutablePKeyPValueMap.() -> Unit,
+): PKeyPValueMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyPValueMap().apply(builderAction)
+}
+
+/**
+ * Builds a new [PKeyPValueMap] by populating a [MutablePKeyPValueMap] using the given
+ * [builderAction].
+ *
+ * The instance passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of pairs added in the [builderAction].
+ * @param builderAction Lambda in which the [MutablePKeyPValueMap] can be populated.
+ */
+public inline fun buildPKeyPValueMap(
+ initialCapacity: Int,
+ builderAction: MutablePKeyPValueMap.() -> Unit,
+): PKeyPValueMap {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeyPValueMap(initialCapacity).apply(builderAction)
+}
+
+/**
* [PKeyPValueMap] is a container with a [Map]-like interface for
* [PKey] primitive keys and [PValue] primitive values.
*
@@ -453,19 +491,19 @@
}
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
- public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+ public inline operator fun contains(key: PKey): Boolean = containsKey(key)
/**
- * Returns true if the specified [key] is present in this hash map, false
+ * Returns true if the specified [key] is present in this map, false
* otherwise.
*/
public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
/**
- * Returns true if the specified [value] is present in this hash map, false
+ * Returns true if the specified [value] is present in this map, false
* otherwise.
*/
public fun containsValue(value: PValue): Boolean {
diff --git a/collection/collection/template/PKeyPValueMapTest.kt.template b/collection/collection/template/PKeyPValueMapTest.kt.template
index a61070f..a190dae 100644
--- a/collection/collection/template/PKeyPValueMapTest.kt.template
+++ b/collection/collection/template/PKeyPValueMapTest.kt.template
@@ -188,6 +188,35 @@
}
@Test
+ fun buildPKeyPValueMapFunction() {
+ val contract: Boolean
+ val map = buildPKeyPValueMap {
+ contract = true
+ put(1KeySuffix, 1ValueSuffix)
+ put(2KeySuffix, 2ValueSuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ }
+
+ @Test
+ fun buildPKeyObjectMapWithCapacityFunction() {
+ val contract: Boolean
+ val map = buildPKeyPValueMap(20) {
+ contract = true
+ put(1KeySuffix, 1ValueSuffix)
+ put(2KeySuffix, 2ValueSuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, map.size)
+ assertTrue(map.capacity >= 18)
+ assertEquals(1ValueSuffix, map[1KeySuffix])
+ assertEquals(2ValueSuffix, map[2KeySuffix])
+ }
+
+ @Test
fun addToMap() {
val map = MutablePKeyPValueMap()
map[1KeySuffix] = 1ValueSuffix
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
index 38e36a4..a33c487 100644
--- a/collection/collection/template/PKeySet.kt.template
+++ b/collection/collection/template/PKeySet.kt.template
@@ -23,12 +23,15 @@
"PrivatePropertyName",
"NOTHING_TO_INLINE"
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.collection
import androidx.annotation.IntRange
import androidx.collection.internal.requirePrecondition
import androidx.collection.internal.throwNoSuchElementException
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
@@ -126,6 +129,40 @@
MutablePKeySet(elements.size).apply { plusAssign(elements) }
/**
+ * Builds a new [PKeySet] by populating a [MutablePKeySet] using the given
+ * [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutablePKeySet] can be populated.
+ */
+public inline fun buildPKeySet(
+ builderAction: MutablePKeySet.() -> Unit,
+): PKeySet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeySet().apply(builderAction)
+}
+
+/**
+ * Builds a new [PKeySet] by populating a [MutablePKeySet] using the given
+ * [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutablePKeySet] can be populated.
+ */
+public inline fun buildPKeySet(
+ initialCapacity: Int,
+ builderAction: MutablePKeySet.() -> Unit,
+): PKeySet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutablePKeySet(initialCapacity).apply(builderAction)
+}
+
+/**
* [PKeySet] is a container with a [Set]-like interface designed to avoid
* allocations, including boxing.
*
diff --git a/collection/collection/template/PKeySetTest.kt.template b/collection/collection/template/PKeySetTest.kt.template
index f9b12d5..05813d0 100644
--- a/collection/collection/template/PKeySetTest.kt.template
+++ b/collection/collection/template/PKeySetTest.kt.template
@@ -562,6 +562,35 @@
}
@Test
+ fun buildPKeySetFunction() {
+ val contract: Boolean
+ val set = buildPKeySet {
+ contract = true
+ add(1KeySuffix)
+ add(2KeySuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
+ fun buildPKeySetWithCapacityFunction() {
+ val contract: Boolean
+ val set = buildPKeySet(20) {
+ contract = true
+ add(1KeySuffix)
+ add(2KeySuffix)
+ }
+ assertTrue(contract)
+ assertEquals(2, set.size)
+ assertTrue(set.capacity >= 18)
+ assertTrue(1KeySuffix in set)
+ assertTrue(2KeySuffix in set)
+ }
+
+ @Test
fun insertManyRemoveMany() {
val set = mutablePKeySetOf()
diff --git a/collection/collection/template/ValueClassList.kt.template b/collection/collection/template/ValueClassList.kt.template
index 4ce50e6..88fc2dc 100644
--- a/collection/collection/template/ValueClassList.kt.template
+++ b/collection/collection/template/ValueClassList.kt.template
@@ -24,6 +24,7 @@
"NOTHING_TO_INLINE",
"UnusedImport",
)
+@file:OptIn(ExperimentalContracts::class)
package PACKAGE
@@ -34,6 +35,7 @@
import androidx.collection.mutablePRIMITIVEListOf
import VALUE_PKG.VALUE_CLASS
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmInline
@@ -57,7 +59,6 @@
* the appropriate synchronization. It is also not safe to mutate during reentrancy --
* in the middle of a [forEach], for example. However, concurrent reads are safe.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
VISIBILITY value class VALUE_CLASSList(val list: PRIMITIVEList) {
/**
@@ -377,7 +378,6 @@
*
* @constructor Creates a [MutableVALUE_CLASSList] with a [capacity] of `initialCapacity`.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
VISIBILITY value class MutableVALUE_CLASSList(val list: MutablePRIMITIVEList) {
public constructor(initialCapacity: Int = 16) : this(MutablePRIMITIVEList(initialCapacity))
@@ -934,3 +934,37 @@
element3.BACKING_PROPERTY
)
)
+
+/**
+ * Builds a new [VALUE_CLASSList] by populating a [MutableVALUE_CLASSList] using the given
+ * [builderAction].
+ *
+ * The list passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableVALUE_CLASSList] can be populated.
+ */
+VISIBILITY inline fun buildVALUE_CLASSList(
+ builderAction: MutableVALUE_CLASSList.() -> Unit,
+): VALUE_CLASSList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableVALUE_CLASSList().apply(builderAction).asVALUE_CLASSList()
+}
+
+/**
+ * Builds a new [VALUE_CLASSList] by populating a [MutableVALUE_CLASSList] using the given
+ * [builderAction].
+ *
+ * The list passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableVALUE_CLASSList] can be populated.
+ */
+VISIBILITY inline fun buildVALUE_CLASSList(
+ initialCapacity: Int,
+ builderAction: MutableVALUE_CLASSList.() -> Unit,
+): VALUE_CLASSList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableVALUE_CLASSList(initialCapacity).apply(builderAction).asVALUE_CLASSList()
+}
diff --git a/collection/collection/template/ValueClassSet.kt.template b/collection/collection/template/ValueClassSet.kt.template
index de43cdf..cd2f51f 100644
--- a/collection/collection/template/ValueClassSet.kt.template
+++ b/collection/collection/template/ValueClassSet.kt.template
@@ -24,6 +24,7 @@
"NOTHING_TO_INLINE",
"UnusedImport",
)
+@file:OptIn(ExperimentalContracts::class)
package PACKAGE
@@ -34,6 +35,7 @@
import androidx.collection.mutablePRIMITIVESetOf
import VALUE_PKG.VALUE_CLASS
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmInline
@@ -137,6 +139,37 @@
)
/**
+ * Builds a new [VALUE_CLASSSet] by populating a [MutableVALUE_CLASSSet] using the given
+ * [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ */
+VISIBILITY inline fun buildVALUE_CLASSSet(
+ builderAction: MutableVALUE_CLASSSet.() -> Unit,
+): VALUE_CLASSSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableVALUE_CLASSSet().apply(builderAction).asVALUE_CLASSSet()
+}
+
+/**
+ * Builds a new [VALUE_CLASSSet] by populating a [MutableVALUE_CLASSSet] using the given
+ * [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function.
+ * Using it outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ */
+VISIBILITY inline fun buildVALUE_CLASSSet(
+ initialCapacity: Int,
+ builderAction: MutableVALUE_CLASSSet.() -> Unit,
+): VALUE_CLASSSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableVALUE_CLASSSet(initialCapacity).apply(builderAction).asVALUE_CLASSSet()
+}
+
+/**
* [VALUE_CLASSSet] is a container with a [Set]-like interface designed to avoid
* allocations, including boxing.
*
@@ -151,7 +184,6 @@
*
* @see [MutableVALUE_CLASSSet]
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
VISIBILITY value class VALUE_CLASSSet(val set: PRIMITIVESet) {
/**
@@ -302,7 +334,6 @@
* the set (insertion or removal for instance), the calling code must provide
* the appropriate synchronization. Concurrent reads are however safe.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
VISIBILITY value class MutableVALUE_CLASSSet(val set: MutablePRIMITIVESet) {
/**
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index 1bb80e5..5611700 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -18,6 +18,7 @@
package androidx.compose.animation
+import android.annotation.SuppressLint
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.MutableTransitionState
@@ -28,6 +29,7 @@
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -63,6 +65,7 @@
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -93,7 +96,6 @@
@get:Rule
val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()).around(rule)
- @OptIn(InternalAnimationApi::class)
@Test
fun AnimatedContentSizeTransformTest() {
val size1 = 40
@@ -990,6 +992,76 @@
rule.waitForIdle()
}
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @SuppressLint("UnusedContentLambdaTargetStateParameter")
+ @Test
+ fun testSizeTransformAlwaysContinuous() {
+ var large by mutableStateOf(false)
+ var currentWidth: Int = 0
+ var currentHeight: Int = 0
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ LookaheadScope {
+ Box(Modifier.clickable { large = !large }) {
+ AnimatedContent(
+ label = "Test",
+ modifier =
+ Modifier.animateBounds(
+ this@LookaheadScope,
+ if (!large) Modifier.size(200.dp) else Modifier.size(300.dp)
+ )
+ .layout { m, c ->
+ m.measure(
+ c.copy(
+ maxWidth = Constraints.Infinity,
+ maxHeight = Constraints.Infinity
+ )
+ )
+ .run {
+ if (!isLookingAhead) {
+ currentWidth = width
+ currentHeight = height
+ }
+ layout(width, height) { place(0, 0) }
+ }
+ }
+ .background(Color.Gray),
+ targetState = large,
+ contentKey = { true },
+ transitionSpec = { fadeIn().togetherWith(fadeOut()) }
+ ) {
+ Box(Modifier.background(Color.Black).size(200.dp, 100.dp))
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+ large = true
+
+ assertEquals(200, currentWidth)
+ assertEquals(200, currentHeight)
+ // Expect size to grow
+ var lastWidth: Int = 200
+ var lastHeight: Int = 200
+
+ fun doFrame() {
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ assertTrue(currentWidth >= lastWidth)
+ assertTrue(currentHeight >= lastHeight)
+ lastWidth = currentWidth
+ lastHeight = currentHeight
+ }
+
+ repeat(5) { doFrame() }
+
+ while (currentWidth != 300 || currentHeight != 300) {
+ doFrame()
+ }
+ }
+
@Test
fun testTargetChangeLookaheadPlacement() {
var lookaheadPosition1: Offset? = null
@@ -1087,7 +1159,6 @@
assertEquals(expected.y, actual.y, 0.00001f)
}
- @OptIn(InternalAnimationApi::class)
private val Transition<*>.playTimeMillis
get() = (playTimeNanos / 1_000_000L).toInt()
}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index f095141..a8d32ea 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -60,6 +60,8 @@
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -89,8 +91,8 @@
* If [targetState] is expected to mutate frequently and not all mutations should be treated as
* target state change, consider defining a mapping between [targetState] and a key in [contentKey].
* As a result, transitions will be triggered when the resulting key changes. In other words, there
- * will be no animation when switching between [targetState]s that share the same same key. By
- * default, the key will be the same as the targetState object.
+ * will be no animation when switching between [targetState]s that share the same key. By default,
+ * the key will be the same as the targetState object.
*
* By default, the [ContentTransform] will be a delayed [fadeIn] of the target content and a delayed
* [scaleIn] [togetherWith] a [fadeOut] of the initial content, using a [SizeTransform] to animate
@@ -563,16 +565,23 @@
shouldAnimateSize = true
}
}
+ val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>?
return if (shouldAnimateSize) {
- val sizeAnimation = transition.createDeferredAnimation(IntSize.VectorConverter)
- remember(sizeAnimation) {
- (if (sizeTransform.value?.clip == false) Modifier else Modifier.clipToBounds())
- .then(SizeModifier(sizeAnimation, sizeTransform))
+ sizeAnimation = transition.createDeferredAnimation(IntSize.VectorConverter)
+ remember(sizeAnimation) {
+ (if (sizeTransform.value?.clip == false) Modifier else Modifier.clipToBounds())
+ }
+ } else {
+ sizeAnimation = null
+ animatedSize = null
+ Modifier
}
- } else {
- animatedSize = null
- Modifier
- }
+ .then(
+ // Keep the SizeModifier in the chain and switch between active animating and
+ // passive
+ // observing based on sizeAnimation's value
+ SizeModifierElement(sizeAnimation, sizeTransform)
+ )
}
// This helps track the target measurable without affecting the placement order. Target
@@ -587,31 +596,88 @@
}
}
- private inner class SizeModifier(
- val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>,
- val sizeTransform: State<SizeTransform?>,
- ) : LayoutModifierWithPassThroughIntrinsics() {
+ private inner class SizeModifierElement(
+ val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>?,
+ val sizeTransform: State<SizeTransform?>
+ ) : ModifierNodeElement<SizeModifierNode>() {
+ override fun create(): SizeModifierNode {
+ return SizeModifierNode(sizeAnimation, sizeTransform)
+ }
+
+ override fun hashCode(): Int {
+ return sizeAnimation.hashCode() * 31 + sizeTransform.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is AnimatedContentTransitionScopeImpl<*>.SizeModifierElement &&
+ other.sizeAnimation == sizeAnimation &&
+ other.sizeTransform == sizeTransform
+ }
+
+ override fun update(node: SizeModifierNode) {
+ node.sizeAnimation = sizeAnimation
+ node.sizeTransform = sizeTransform
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "sizeTransform"
+ properties["sizeAnimation"] = sizeAnimation
+ properties["sizeTransform"] = sizeTransform
+ }
+ }
+
+ private inner class SizeModifierNode(
+ var sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>?,
+ var sizeTransform: State<SizeTransform?>,
+ ) : LayoutModifierNodeWithPassThroughIntrinsics() {
+ // This is used to track the on-going size change so that when the target state changes,
+ // we always start from the last seen size to the new target size to ensure continuity.
+ private var lastSize: IntSize = UnspecifiedSize
+
+ private fun lastContinuousSizeOrDefault(default: IntSize) =
+ if (lastSize == UnspecifiedSize) default else lastSize
+
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
- val size =
- sizeAnimation.animate(
- transitionSpec = {
- val initial = targetSizeMap[initialState]?.value ?: IntSize.Zero
- val target = targetSizeMap[targetState]?.value ?: IntSize.Zero
- sizeTransform.value?.createAnimationSpec(initial, target) ?: spring()
- }
- ) {
- targetSizeMap[it]?.value ?: IntSize.Zero
- }
- animatedSize = size
val measuredSize: IntSize
if (isLookingAhead) {
measuredSize = IntSize(placeable.width, placeable.height)
+ } else if (sizeAnimation == null) {
+ // Observing mode
+ measuredSize = IntSize(placeable.width, placeable.height)
+ lastSize = IntSize(placeable.width, placeable.height)
} else {
+ val currentSize = IntSize(placeable.width, placeable.height)
+ val size =
+ sizeAnimation!!.animate(
+ transitionSpec = {
+ val initial =
+ if (
+ initialState ==
+ [email protected]
+ ) {
+ lastContinuousSizeOrDefault(currentSize)
+ } else {
+ targetSizeMap[initialState]?.value ?: IntSize.Zero
+ }
+ val target = targetSizeMap[targetState]?.value ?: IntSize.Zero
+ sizeTransform.value?.createAnimationSpec(initial, target)
+ ?: spring(stiffness = Spring.StiffnessMediumLow)
+ }
+ ) {
+ // Animate from the approach size to the lookahead size.
+ if (it == initialState) {
+ lastContinuousSizeOrDefault(currentSize)
+ } else {
+ targetSizeMap[it]?.value ?: IntSize.Zero
+ }
+ }
+ animatedSize = size
measuredSize = size.value
+ lastSize = size.value
}
return layout(measuredSize.width, measuredSize.height) {
val offset =
@@ -626,6 +692,8 @@
}
}
+private val UnspecifiedSize: IntSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+
/**
* Receiver scope for content lambda for AnimatedContent. In this scope,
* [transition][AnimatedVisibilityScope.transition] can be used to observe the state of the
@@ -657,7 +725,7 @@
* treated as target state change, consider defining a mapping between [Transition.targetState] and
* a key in [contentKey]. As a result, transitions will be triggered when the resulting key changes.
* In other words, there will be no animation when switching between [Transition.targetState]s that
- * share the same same key. By default, the key will be the same as the targetState object.
+ * share the same key. By default, the key will be the same as the targetState object.
*
* By default, the [ContentTransform] will be a delayed [fadeIn] of the target content and a delayed
* [scaleIn] [togetherWith] a [fadeOut] of the initial content, using a [SizeTransform] to animate
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
index 4c79c36..1b2fec6 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -54,8 +54,8 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastMaxOfOrNull
+import androidx.compose.ui.util.fastMaxOfOrDefault
+import kotlin.math.max
/**
* [AnimatedVisibility] composable animates the appearance and disappearance of its content, as
@@ -781,9 +781,15 @@
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
- val placeables = measurables.fastMap { it.measure(constraints) }
- val maxWidth: Int = placeables.fastMaxBy { it.width }?.width ?: 0
- val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0
+ var maxWidth = 0
+ var maxHeight = 0
+ val placeables =
+ measurables.fastMap {
+ it.measure(constraints).apply {
+ maxWidth = max(maxWidth, width)
+ maxHeight = max(maxHeight, height)
+ }
+ }
// Position the children.
if (isLookingAhead) {
hasLookaheadOccurred = true
@@ -798,22 +804,22 @@
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
- ) = measurables.fastMaxOfOrNull { it.minIntrinsicWidth(height) } ?: 0
+ ) = measurables.fastMaxOfOrDefault(0) { it.minIntrinsicWidth(height) }
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
- ) = measurables.fastMaxOfOrNull { it.minIntrinsicHeight(width) } ?: 0
+ ) = measurables.fastMaxOfOrDefault(0) { it.minIntrinsicHeight(width) }
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
- ) = measurables.fastMaxOfOrNull { it.maxIntrinsicWidth(height) } ?: 0
+ ) = measurables.fastMaxOfOrDefault(0) { it.maxIntrinsicWidth(height) }
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
- ) = measurables.fastMaxOfOrNull { it.maxIntrinsicHeight(width) } ?: 0
+ ) = measurables.fastMaxOfOrDefault(0) { it.maxIntrinsicHeight(width) }
}
// This converts Boolean visible to EnterExitState
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index 57d5833..8102e2c 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -266,25 +266,3 @@
width: Int
) = measurable.maxIntrinsicHeight(width)
}
-
-internal abstract class LayoutModifierWithPassThroughIntrinsics : LayoutModifier {
- final override fun IntrinsicMeasureScope.minIntrinsicWidth(
- measurable: IntrinsicMeasurable,
- height: Int
- ) = measurable.minIntrinsicWidth(height)
-
- final override fun IntrinsicMeasureScope.minIntrinsicHeight(
- measurable: IntrinsicMeasurable,
- width: Int
- ) = measurable.minIntrinsicHeight(width)
-
- final override fun IntrinsicMeasureScope.maxIntrinsicWidth(
- measurable: IntrinsicMeasurable,
- height: Int
- ) = measurable.maxIntrinsicWidth(height)
-
- final override fun IntrinsicMeasureScope.maxIntrinsicHeight(
- measurable: IntrinsicMeasurable,
- width: Int
- ) = measurable.maxIntrinsicHeight(width)
-}
diff --git a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetector.kt b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetector.kt
index 52974ba..f481919 100644
--- a/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetector.kt
+++ b/compose/foundation/foundation-lint/src/main/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetector.kt
@@ -33,7 +33,7 @@
import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
import com.intellij.psi.PsiMethod
import java.util.EnumSet
-import kotlinx.metadata.KmClassifier
+import kotlin.metadata.KmClassifier
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UDeclaration
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorList.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorList.kt
index 85264af..aa9c110 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorList.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorList.kt
@@ -24,15 +24,18 @@
"NOTHING_TO_INLINE",
"UnusedImport",
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.compose.foundation.demos.collection
+import androidx.annotation.IntRange
import androidx.collection.LongList
import androidx.collection.MutableLongList
import androidx.collection.emptyLongList
import androidx.collection.mutableLongListOf
import androidx.compose.ui.graphics.Color
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmInline
@@ -55,21 +58,20 @@
* calling code must provide the appropriate synchronization. It is also not safe to mutate during
* reentrancy -- in the middle of a [forEach], for example. However, concurrent reads are safe.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
internal value class ColorList(val list: LongList) {
/** The number of elements in the [ColorList]. */
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val size: Int
get() = list.size
/** Returns the last valid index in the [ColorList]. This can be `-1` when the list is empty. */
- @get:androidx.annotation.IntRange(from = -1)
+ @get:IntRange(from = -1)
public inline val lastIndex: Int
get() = list.lastIndex
/** Returns an [IntRange] of the valid indices for this [ColorList]. */
- public inline val indices: IntRange
+ public inline val indices: kotlin.ranges.IntRange
get() = list.indices
/** Returns `true` if the collection has no elements in it. */
@@ -244,14 +246,14 @@
* Returns the element at the given [index] or throws [IndexOutOfBoundsException] if the [index]
* is out of bounds of this collection.
*/
- public inline operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): Color =
+ public inline operator fun get(@IntRange(from = 0) index: Int): Color =
Color(list[index].toULong())
/**
* Returns the element at the given [index] or throws [IndexOutOfBoundsException] if the [index]
* is out of bounds of this collection.
*/
- public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): Color =
+ public inline fun elementAt(@IntRange(from = 0) index: Int): Color =
Color(list[index].toULong())
/**
@@ -263,7 +265,7 @@
* index not in the list.
*/
public inline fun elementAtOrElse(
- @androidx.annotation.IntRange(from = 0) index: Int,
+ @IntRange(from = 0) index: Int,
defaultValue: (index: Int) -> Color
): Color = Color(list.elementAtOrElse(index) { defaultValue(it).value.toLong() }.toULong())
@@ -350,23 +352,22 @@
*
* @constructor Creates a [MutableColorList] with a [capacity] of `initialCapacity`.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
internal value class MutableColorList(val list: MutableLongList) {
public constructor(initialCapacity: Int = 16) : this(MutableLongList(initialCapacity))
/** The number of elements in the [ColorList]. */
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val size: Int
get() = list.size
/** Returns the last valid index in the [ColorList]. This can be `-1` when the list is empty. */
- @get:androidx.annotation.IntRange(from = -1)
+ @get:IntRange(from = -1)
public inline val lastIndex: Int
get() = list.lastIndex
/** Returns an [IntRange] of the valid indices for this [ColorList]. */
- public inline val indices: IntRange
+ public inline val indices: kotlin.ranges.IntRange
get() = list.indices
/** Returns `true` if the collection has no elements in it. */
@@ -541,14 +542,14 @@
* Returns the element at the given [index] or throws [IndexOutOfBoundsException] if the [index]
* is out of bounds of this collection.
*/
- public inline operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): Color =
+ public inline operator fun get(@IntRange(from = 0) index: Int): Color =
Color(list[index].toULong())
/**
* Returns the element at the given [index] or throws [IndexOutOfBoundsException] if the [index]
* is out of bounds of this collection.
*/
- public inline fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): Color =
+ public inline fun elementAt(@IntRange(from = 0) index: Int): Color =
Color(list[index].toULong())
/**
@@ -560,7 +561,7 @@
* index not in the list.
*/
public inline fun elementAtOrElse(
- @androidx.annotation.IntRange(from = 0) index: Int,
+ @IntRange(from = 0) index: Int,
defaultValue: (index: Int) -> Color
): Color = Color(list.elementAtOrElse(index) { defaultValue(it).value.toLong() }.toULong())
@@ -641,7 +642,7 @@
*
* @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
*/
- public inline fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: Color) =
+ public inline fun add(@IntRange(from = 0) index: Int, element: Color) =
list.add(index, element.value.toLong())
/**
@@ -651,10 +652,8 @@
* @return `true` if the [MutableColorList] was changed or `false` if [elements] was empty
* @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
*/
- public inline fun addAll(
- @androidx.annotation.IntRange(from = 0) index: Int,
- elements: ColorList
- ): Boolean = list.addAll(index, elements.list)
+ public inline fun addAll(@IntRange(from = 0) index: Int, elements: ColorList): Boolean =
+ list.addAll(index, elements.list)
/**
* Adds all [elements] to the [MutableColorList] at the given [index], shifting over any
@@ -663,10 +662,8 @@
* @return `true` if the [MutableColorList] was changed or `false` if [elements] was empty
* @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
*/
- public inline fun addAll(
- @androidx.annotation.IntRange(from = 0) index: Int,
- elements: MutableColorList
- ): Boolean = list.addAll(index, elements.list)
+ public inline fun addAll(@IntRange(from = 0) index: Int, elements: MutableColorList): Boolean =
+ list.addAll(index, elements.list)
/**
* Adds all [elements] to the end of the [MutableColorList] and returns `true` if the
@@ -747,7 +744,7 @@
*
* @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
*/
- public inline fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): Color =
+ public inline fun removeAt(@IntRange(from = 0) index: Int): Color =
Color(list.removeAt(index).toULong())
/**
@@ -756,10 +753,8 @@
* @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
* @throws IllegalArgumentException if [start] is greater than [end]
*/
- public inline fun removeRange(
- @androidx.annotation.IntRange(from = 0) start: Int,
- @androidx.annotation.IntRange(from = 0) end: Int
- ) = list.removeRange(start, end)
+ public inline fun removeRange(@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int) =
+ list.removeRange(start, end)
/**
* Keeps only [elements] in the [MutableColorList] and removes all other values.
@@ -781,10 +776,8 @@
* @return the previous value set at [index]
* @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
*/
- public inline operator fun set(
- @androidx.annotation.IntRange(from = 0) index: Int,
- element: Color
- ): Color = Color(list.set(index, element.value.toLong()).toULong())
+ public inline operator fun set(@IntRange(from = 0) index: Int, element: Color): Color =
+ Color(list.set(index, element.value.toLong()).toULong())
}
/** @return a read-only [ColorList] with nothing in it. */
@@ -833,3 +826,35 @@
MutableColorList(
mutableLongListOf(element1.value.toLong(), element2.value.toLong(), element3.value.toLong())
)
+
+/**
+ * Builds a new [ColorList] by populating a [MutableColorList] using the given [builderAction].
+ *
+ * The list passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param builderAction Lambda in which the [MutableColorList] can be populated.
+ */
+internal inline fun buildColorList(
+ builderAction: MutableColorList.() -> Unit,
+): ColorList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableColorList().apply(builderAction).asColorList()
+}
+
+/**
+ * Builds a new [ColorList] by populating a [MutableColorList] using the given [builderAction].
+ *
+ * The list passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ * @param builderAction Lambda in which the [MutableColorList] can be populated.
+ */
+internal inline fun buildColorList(
+ initialCapacity: Int,
+ builderAction: MutableColorList.() -> Unit,
+): ColorList {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableColorList(initialCapacity).apply(builderAction).asColorList()
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorSet.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorSet.kt
index 20479137..4ceecf1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorSet.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/collection/ColorSet.kt
@@ -24,15 +24,18 @@
"NOTHING_TO_INLINE",
"UnusedImport",
)
+@file:OptIn(ExperimentalContracts::class)
package androidx.compose.foundation.demos.collection
+import androidx.annotation.IntRange
import androidx.collection.LongSet
import androidx.collection.MutableLongSet
import androidx.collection.emptyLongSet
import androidx.collection.mutableLongSetOf
import androidx.compose.ui.graphics.Color
import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmInline
@@ -103,6 +106,35 @@
)
/**
+ * Builds a new [ColorSet] by populating a [MutableColorSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ */
+internal inline fun buildColorSet(
+ builderAction: MutableColorSet.() -> Unit,
+): ColorSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableColorSet().apply(builderAction).asColorSet()
+}
+
+/**
+ * Builds a new [ColorSet] by populating a [MutableColorSet] using the given [builderAction].
+ *
+ * The set passed as a receiver to the [builderAction] is valid only inside that function. Using it
+ * outside of the function produces an unspecified behavior.
+ *
+ * @param initialCapacity Hint for the expected number of elements added in the [builderAction].
+ */
+internal inline fun buildColorSet(
+ initialCapacity: Int,
+ builderAction: MutableColorSet.() -> Unit,
+): ColorSet {
+ contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) }
+ return MutableColorSet(initialCapacity).apply(builderAction).asColorSet()
+}
+
+/**
* [ColorSet] is a container with a [Set]-like interface designed to avoid allocations, including
* boxing.
*
@@ -116,19 +148,18 @@
*
* @see [MutableColorSet]
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
internal value class ColorSet(val set: LongSet) {
/**
* Returns the number of elements that can be stored in this set without requiring internal
* storage reallocation.
*/
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val capacity: Int
get() = set.capacity
/** Returns the number of elements in this set. */
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val size: Int
get() = set.size
@@ -200,7 +231,7 @@
}
/** Returns the number of elements in this set. */
- @androidx.annotation.IntRange(from = 0) public inline fun count(): Int = set.count()
+ @IntRange(from = 0) public inline fun count(): Int = set.count()
/**
* Returns the number of elements matching the given [predicate].
@@ -208,7 +239,7 @@
* @param predicate Called for all elements in the set to count the number for which it returns
* `true`.
*/
- @androidx.annotation.IntRange(from = 0)
+ @IntRange(from = 0)
public inline fun count(predicate: (element: Color) -> Boolean): Int {
contract { callsInPlace(predicate) }
return set.count { predicate(Color(it.toULong())) }
@@ -256,19 +287,18 @@
* and one or more threads modify the structure of the set (insertion or removal for instance), the
* calling code must provide the appropriate synchronization. Concurrent reads are however safe.
*/
-@OptIn(ExperimentalContracts::class)
@JvmInline
internal value class MutableColorSet(val set: MutableLongSet) {
/**
* Returns the number of elements that can be stored in this set without requiring internal
* storage reallocation.
*/
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val capacity: Int
get() = set.capacity
/** Returns the number of elements in this set. */
- @get:androidx.annotation.IntRange(from = 0)
+ @get:IntRange(from = 0)
public inline val size: Int
get() = set.size
@@ -340,7 +370,7 @@
}
/** Returns the number of elements in this set. */
- @androidx.annotation.IntRange(from = 0) public inline fun count(): Int = set.count()
+ @IntRange(from = 0) public inline fun count(): Int = set.count()
/**
* Returns the number of elements matching the given [predicate].
@@ -348,7 +378,7 @@
* @param predicate Called for all elements in the set to count the number for which it returns
* `true`.
*/
- @androidx.annotation.IntRange(from = 0)
+ @IntRange(from = 0)
public inline fun count(predicate: (element: Color) -> Boolean): Int {
contract { callsInPlace(predicate) }
return set.count { predicate(Color(it.toULong())) }
@@ -485,5 +515,5 @@
* Returns the number of empty elements removed from this set's storage. Returns 0 if no
* trimming is necessary or possible.
*/
- @androidx.annotation.IntRange(from = 0) public inline fun trim(): Int = set.trim()
+ @IntRange(from = 0) public inline fun trim(): Int = set.trim()
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMinTouchTargetTextSelectionDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMinTouchTargetTextSelectionDemo.kt
index 65d6c77..bc32206 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMinTouchTargetTextSelectionDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMinTouchTargetTextSelectionDemo.kt
@@ -17,7 +17,7 @@
package androidx.compose.foundation.demos.text
import androidx.compose.foundation.border
-import androidx.compose.foundation.demos.collection.MutableColorList
+import androidx.compose.foundation.demos.collection.buildColorList
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -61,16 +61,14 @@
// red is used for the selection container color
private val Rainbow =
- MutableColorList(initialCapacity = 6)
- .apply {
- add(Orange)
- add(Yellow)
- add(Green)
- add(Blue)
- add(Indigo)
- add(Purple)
- }
- .asColorList()
+ buildColorList(initialCapacity = 6) {
+ add(Orange)
+ add(Yellow)
+ add(Green)
+ add(Blue)
+ add(Indigo)
+ add(Purple)
+ }
@Composable
fun MinTouchTargetTextSelection() {
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index 84d022c..59530fe 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -47,6 +47,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -418,6 +419,7 @@
rule.runOnIdle { assertThat(remeasuresCount).isEqualTo(1) }
}
+ @Ignore("b/369188686")
@Test
fun nodeIsReusedWhenRemovedFirst() {
var itemCount by mutableStateOf(1)
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 5eb45a6..d2cea9b 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -59,6 +59,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
@@ -119,6 +120,7 @@
import java.util.concurrent.CountDownLatch
import kotlin.math.abs
import kotlin.math.roundToInt
+import kotlin.random.Random
import kotlin.test.assertEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -2789,6 +2791,36 @@
rule.onNodeWithTag(LazyListTag).assertStartPositionInRootIsEqualTo(itemSize)
}
+ @Test
+ fun reorderingInLookeahead() {
+ var items by mutableStateOf(List(500) { it })
+
+ val itemSizePx = 50f
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+
+ rule.setContent {
+ LookaheadScope {
+ LazyColumnOrRow(Modifier.mainAxisSize(itemSize * 2)) {
+ items(items, key = { it }) {
+ Box(Modifier.animateItem().mainAxisSize(itemSize)) {
+ Box { BasicText("Item $it") }
+ }
+ }
+ }
+ }
+ }
+
+ val random = Random(42)
+ repeat(20) {
+ val newItems = items.shuffled(random)
+ items = newItems
+ rule.runOnUiThread {
+ Snapshot.sendApplyNotifications()
+ rule.mainClock.advanceTimeByFrame()
+ }
+ }
+ }
+
// ********************* END OF TESTS *********************
// Helper functions, etc. live below here
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyLayoutSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyLayoutSamples.kt
new file mode 100644
index 0000000..d418c0a
--- /dev/null
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyLayoutSamples.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.samples
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
+
+private val Items = (0..100).toList().map { it.toString() }
+
+/**
+ * In this example, the lazy layout simply lays out content based on its visibility on the screen.
+ */
+@Composable
+@Preview
+fun LazyLayoutDisplayVisibleItemsOnlySample() {
+ BasicNonScrollableLazyLayout(
+ modifier = Modifier.size(500.dp),
+ orientation = Orientation.Vertical,
+ items = Items
+ ) { item, index ->
+ Box(
+ modifier =
+ Modifier.width(100.dp)
+ .height(100.dp)
+ .background(color = if (index % 2 == 0) Color.Red else Color.Green)
+ ) {
+ Text(text = item)
+ }
+ }
+}
+
+/**
+ * A simple Layout that will place items top to down, or right to left, without any scrolling or
+ * offset until they fit the viewport.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun <T> BasicNonScrollableLazyLayout(
+ items: List<T>,
+ modifier: Modifier = Modifier,
+ orientation: Orientation = Orientation.Vertical,
+ content: @Composable (item: T, index: Int) -> Unit
+) {
+ val measurePolicy = remember(items, orientation) { basicMeasurePolicy(orientation, items.size) }
+ val itemProvider = remember(items, content) { { BasicLazyLayoutItemProvider(items, content) } }
+ LazyLayout(modifier = modifier, itemProvider = itemProvider, measurePolicy = measurePolicy)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun basicMeasurePolicy(
+ orientation: Orientation,
+ itemCount: Int
+): LazyLayoutMeasureScope.(Constraints) -> MeasureResult = { constraints ->
+ fun Placeable.mainAxisSize() = if (orientation == Orientation.Vertical) height else width
+ fun Placeable.crossAxisSize() = if (orientation == Orientation.Vertical) width else height
+
+ val viewportSize =
+ if (orientation == Orientation.Vertical) constraints.maxHeight else constraints.maxWidth
+
+ val childConstraints =
+ Constraints(
+ maxWidth =
+ if (orientation == Orientation.Vertical) viewportSize else Constraints.Infinity,
+ maxHeight =
+ if (orientation == Orientation.Horizontal) viewportSize else Constraints.Infinity
+ )
+
+ var currentItemIndex = 0
+ // saves placeables and their main axis position
+ val placeables = mutableListOf<Pair<Placeable, Int>>()
+ var crossAxisSize = 0
+ var mainAxisSize = 0
+
+ // measure items until we either fill in the space or run out of items.
+ while (mainAxisSize < viewportSize && currentItemIndex < itemCount) {
+ val itemPlaceables = measure(currentItemIndex, childConstraints)
+ for (item in itemPlaceables) {
+ // save placeable to be placed later.
+ placeables.add(item to mainAxisSize)
+
+ mainAxisSize += item.mainAxisSize() // item size contributes to main axis size
+ // cross axis size will the size of tallest/widest item
+ crossAxisSize = maxOf(crossAxisSize, item.crossAxisSize())
+ }
+ currentItemIndex++
+ }
+
+ val layoutWidth =
+ if (orientation == Orientation.Horizontal) {
+ minOf(mainAxisSize, viewportSize)
+ } else {
+ crossAxisSize
+ }
+ val layoutHeight =
+ if (orientation == Orientation.Vertical) {
+ minOf(mainAxisSize, viewportSize)
+ } else {
+ crossAxisSize
+ }
+
+ layout(layoutWidth, layoutHeight) {
+ // since this is a linear list all items are placed on the same cross-axis position
+ for ((placeable, position) in placeables) {
+ if (orientation == Orientation.Vertical) {
+ placeable.place(0, position)
+ } else {
+ placeable.place(position, 0)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class BasicLazyLayoutItemProvider<T>(
+ private val items: List<T>,
+ private val content: @Composable (item: T, index: Int) -> Unit
+) : LazyLayoutItemProvider {
+ override val itemCount: Int = items.size
+
+ @Composable
+ override fun Item(index: Int, key: Any) {
+ content.invoke(items[index], index)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 4e75878..fb71215 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -55,6 +55,7 @@
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
@@ -129,6 +130,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -141,7 +143,6 @@
import org.junit.After
import org.junit.Assert
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -1849,6 +1850,66 @@
}
@Test
+ fun scrollable_nestedFling_parentShouldFlingWithVelocityLeft_whenInnerDisappears() {
+ var postFlingCalled = false
+ var postFlingAvailableVelocity = Velocity.Zero
+ var postFlingConsumedVelocity = Velocity.Zero
+ var flingDelta by mutableFloatStateOf(0.0f)
+ var preFlingVelocity = Velocity.Zero
+
+ val topConnection =
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ // accumulate deltas for second fling only
+ if (source == NestedScrollSource.SideEffect) {
+ flingDelta += available.y
+ }
+ return super.onPreScroll(available, source)
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ preFlingVelocity = available
+ return super.onPreFling(available)
+ }
+
+ override suspend fun onPostFling(
+ consumed: Velocity,
+ available: Velocity
+ ): Velocity {
+ postFlingCalled = true
+ postFlingAvailableVelocity = available
+ postFlingConsumedVelocity = consumed
+ return super.onPostFling(consumed, available)
+ }
+ }
+
+ val columnState = ScrollState(with(rule.density) { (50 * 200.dp).roundToPx() })
+
+ rule.setContent {
+ Box(Modifier.nestedScroll(topConnection)) {
+ if (flingDelta.absoluteValue < 100) {
+ Column(Modifier.testTag("column").verticalScroll(columnState)) {
+ repeat(100) { Box(Modifier.size(200.dp)) }
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("column").performTouchInput { swipeUp() }
+ rule.waitForIdle()
+ // removed scrollable
+ rule.onNodeWithTag("column").assertDoesNotExist()
+ rule.runOnIdle {
+ // we fired a post fling call after the disappearance
+ assertThat(postFlingCalled).isTrue()
+
+ // fling velocity in onPostFling is correctly propagated
+ assertThat(postFlingConsumedVelocity + postFlingAvailableVelocity)
+ .isEqualTo(preFlingVelocity)
+ }
+ }
+
+ @Test
fun scrollable_bothOrientations_proxiesPostFling() {
val velocityFlung = 5000f
val outerState = ScrollableState(consumeScrollDelta = { 0f })
@@ -2521,77 +2582,6 @@
}
@Test
- @Ignore("b/175010956") // re-enable when we come back to fling continuation fix
- fun nestedScrollable_shouldImmediateScrollIfChildIsFlinging() {
- var innerDelta = 0f
- var middleDelta = 0f
- var outerDelta = 0f
- var touchSlop = 0f
-
- val outerStateController = ScrollableState {
- outerDelta += it
- 0f
- }
-
- val middleController = ScrollableState {
- middleDelta += it
- 0f
- }
-
- val innerController = ScrollableState {
- innerDelta += it
- it / 2f
- }
-
- rule.setContentAndGetScope {
- touchSlop = LocalViewConfiguration.current.touchSlop
- Box(
- modifier =
- Modifier.testTag("outerScrollable")
- .size(600.dp)
- .background(Color.Red)
- .scrollable(outerStateController, orientation = Orientation.Vertical),
- contentAlignment = Alignment.BottomStart
- ) {
- Box(
- modifier =
- Modifier.testTag("middleScrollable")
- .size(300.dp)
- .background(Color.Blue)
- .scrollable(middleController, orientation = Orientation.Vertical),
- contentAlignment = Alignment.BottomStart
- ) {
- Box(
- modifier =
- Modifier.testTag("innerScrollable")
- .size(50.dp)
- .background(Color.Yellow)
- .scrollable(innerController, orientation = Orientation.Vertical)
- )
- }
- }
- }
-
- rule.mainClock.autoAdvance = false
- rule.onNodeWithTag("innerScrollable").performTouchInput { swipeUp() }
-
- rule.mainClock.advanceTimeByFrame()
- rule.mainClock.advanceTimeByFrame()
-
- val previousOuter = outerDelta
-
- rule.onNodeWithTag("outerScrollable").performTouchInput {
- down(topCenter)
- // Move less than touch slop, should start immediately
- moveBy(Offset(0f, touchSlop / 2))
- }
-
- rule.mainClock.autoAdvance = true
-
- rule.runOnIdle { assertThat(outerDelta).isEqualTo(previousOuter + touchSlop / 2) }
- }
-
- @Test
fun nestedScrollable_noFlingContinuationInCrossAxis_shouldAllowClicksOnCrossAxis_scrollable() {
var clicked = 0
rule.setContentAndGetScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index f81e6db..5d6a2da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -298,6 +298,16 @@
reverseDirection = reverseDirection,
flingBehavior = flingBehavior ?: defaultFlingBehavior,
nestedScrollDispatcher = nestedScrollDispatcher,
+ shouldCancelFling = { flingPixels ->
+ // fling should be cancelled if we try to scroll more than we can or if this node
+ // is detached during a fling.
+ // tries to scroll forward but cannot.
+ (flingPixels > 0.0f && !state.canScrollForward) ||
+ // tries to scroll backward but cannot.
+ (flingPixels < 0.0f && !state.canScrollBackward) ||
+ // node is detached.
+ !isAttached
+ }
)
private val nestedScrollConnection =
@@ -340,12 +350,7 @@
@OptIn(ExperimentalFoundationApi::class)
override fun onDragStopped(velocity: Velocity) {
- if (NewNestedFlingPropagationEnabled) {
- if (!isAttached) return
- coroutineScope.launch { scrollingLogic.onDragStopped(velocity) }
- } else {
- nestedScrollDispatcher.coroutineScope.launch { scrollingLogic.onDragStopped(velocity) }
- }
+ nestedScrollDispatcher.coroutineScope.launch { scrollingLogic.onDragStopped(velocity) }
}
override fun startDragImmediately(): Boolean {
@@ -597,6 +602,7 @@
private var orientation: Orientation,
private var reverseDirection: Boolean,
private var nestedScrollDispatcher: NestedScrollDispatcher,
+ private val shouldCancelFling: (Float) -> Boolean
) {
fun Float.toOffset(): Offset =
@@ -717,20 +723,14 @@
val reverseScope =
object : ScrollScope {
override fun scrollBy(pixels: Float): Float {
- // Fling has hit the bounds, cancel it to allow continuation. This will
- // conclude this node's fling, allowing the onPostFling signal to be called
+ // Fling has hit the bounds or node left composition,
+ // cancel it to allow continuation. This will conclude this node's fling,
+ // allowing the onPostFling signal to be called
// with the leftover velocity from the fling animation. Any nested scroll
// node above will be able to pick up the left over velocity and continue
// the fling.
- if (NewNestedFlingPropagationEnabled) {
- if (
- pixels > 0.0f && !scrollableState.canScrollForward ||
- pixels < 0.0f && !scrollableState.canScrollBackward
- ) {
- throw kotlin.coroutines.cancellation.CancellationException(
- "The fling was cancelled"
- )
- }
+ if (NewNestedFlingPropagationEnabled && shouldCancelFling(pixels)) {
+ throw FlingCancellationException()
}
return nestedScrollScope
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 3cb700c..2c1bc66 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -20,7 +20,6 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.ReusableContentHost
import androidx.compose.runtime.Stable
import androidx.compose.runtime.saveable.SaveableStateHolder
import kotlin.jvm.JvmInline
@@ -94,7 +93,7 @@
if (index != -1) this.index = index
}
- ReusableContentHost(active = index != -1) {
+ if (index != -1) {
SkippableItem(
itemProvider,
StableValue(saveableStateHolder),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 81a4eb3..d6cf4ec 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -37,7 +37,7 @@
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.fastJoinToString
-import androidx.compose.ui.util.fastMaxOfOrNull
+import androidx.compose.ui.util.fastMaxOfOrDefault
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.packInts
import androidx.compose.ui.util.unpackInt1
@@ -1198,14 +1198,14 @@
override fun getParentData(index: Int) = placeables[index].parentData
val mainAxisSize: Int =
- placeables.fastMaxOfOrNull { placeable ->
+ placeables.fastMaxOfOrDefault(0) { placeable ->
if (isVertical) placeable.height else placeable.width
- } ?: 0
+ }
override val mainAxisSizeWithSpacings: Int = (mainAxisSize + spacing).coerceAtLeast(0)
val crossAxisSize: Int =
- placeables.fastMaxOfOrNull { if (isVertical) it.width else it.height } ?: 0
+ placeables.fastMaxOfOrDefault(0) { if (isVertical) it.width else it.height }
private var mainAxisLayoutSize: Int = Unset
private var minMainAxisOffset: Int = 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SimpleLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SimpleLayout.kt
index 48d0d2e..c68c442 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SimpleLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SimpleLayout.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.util.fastFold
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import kotlin.math.max
@@ -31,14 +30,15 @@
@Composable
internal fun SimpleLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
- val placeables = measurables.fastMap { measurable -> measurable.measure(constraints) }
-
- val width =
- placeables.fastFold(0) { maxWidth, placeable -> max(maxWidth, (placeable.width)) }
-
- val height =
- placeables.fastFold(0) { minWidth, placeable -> max(minWidth, (placeable.height)) }
-
+ var width = 0
+ var height = 0
+ val placeables =
+ measurables.fastMap { measurable ->
+ val placeable = measurable.measure(constraints)
+ width = max(width, placeable.width)
+ height = max(height, placeable.height)
+ placeable
+ }
layout(width, height) { placeables.fastForEach { placeable -> placeable.place(0, 0) } }
}
}
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt
index 84183af..531c84a 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt
@@ -21,7 +21,7 @@
import com.intellij.psi.PsiParameter
import com.intellij.psi.impl.compiled.ClsParameterImpl
import com.intellij.psi.impl.light.LightParameter
-import kotlinx.metadata.jvm.annotations
+import kotlin.metadata.jvm.annotations
import org.jetbrains.kotlin.psi.KtAnnotated
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtProperty
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index 1425e05..ec7d114 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -24,11 +24,11 @@
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.compiled.ClsMethodImpl
import com.intellij.psi.util.ClassUtil
-import kotlinx.metadata.KmDeclarationContainer
-import kotlinx.metadata.KmFunction
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import kotlinx.metadata.jvm.Metadata
-import kotlinx.metadata.jvm.signature
+import kotlin.metadata.KmDeclarationContainer
+import kotlin.metadata.KmFunction
+import kotlin.metadata.jvm.KotlinClassMetadata
+import kotlin.metadata.jvm.Metadata
+import kotlin.metadata.jvm.signature
/**
* @return the corresponding [KmFunction] for this [PsiMethod], or `null` if there is no
@@ -60,7 +60,7 @@
} catch (e: Exception) {
// Don't crash if we are trying to parse metadata from a newer version of Kotlin, than
// is
- // supported by the bundled version of kotlinx-metadata-jvm
+ // supported by the bundled version of kotlin-metadata-jvm
return null
}
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
index b9478f3..81b56b6 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
@@ -16,7 +16,7 @@
package androidx.compose.lint
-import kotlinx.metadata.ClassName
+import kotlin.metadata.ClassName
/** Contains common names used for lint checks. */
object Names {
@@ -121,7 +121,7 @@
get() = pkg.segments.joinToString(".", postfix = ".") + nameSegments.joinToString(".")
/**
- * The [ClassName] for use with kotlinx.metadata. Note that in kotlinx.metadata the actual type
+ * The [ClassName] for use with kotlin.metadata. Note that in kotlin.metadata the actual type
* might be different from the underlying JVM type, for example: kotlin/Int -> java/lang/Integer
*/
val kmClassName: ClassName
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
index a15c376..b925fda 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
@@ -28,7 +28,7 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.impl.compiled.ClsMethodImpl
-import kotlinx.metadata.KmClassifier
+import kotlin.metadata.KmClassifier
import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index 74fbeb5..b09f73b 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -55,6 +55,19 @@
field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class MutableThreePaneScaffoldState extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState {
+ ctor public MutableThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
+ method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+ method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+ method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+ property @FloatRange(from=0.0, to=1.0) public float progressFraction;
+ property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+ }
+
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
}
@@ -125,6 +138,30 @@
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
+ }
+
+ public static final class PaneMotion.Companion {
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getAnimateBounds();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeft();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeftDelayed();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRight();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRightDelayed();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterWithExpand();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToLeft();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToRight();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitWithShrink();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getNoMotion();
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion AnimateBounds;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeft;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeftDelayed;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRight;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRightDelayed;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterWithExpand;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToLeft;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToRight;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitWithShrink;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion NoMotion;
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
@@ -283,17 +320,13 @@
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class ThreePaneScaffoldState {
- ctor public ThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
- method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
- method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
- method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
- property @FloatRange(from=0.0, to=1.0) public final float progressFraction;
- property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public abstract sealed class ThreePaneScaffoldState {
+ method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+ method @FloatRange(from=0.0, to=1.0) public abstract float getProgressFraction();
+ method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+ property @FloatRange(from=0.0, to=1.0) public abstract float progressFraction;
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index 74fbeb5..b09f73b 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -55,6 +55,19 @@
field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class MutableThreePaneScaffoldState extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState {
+ ctor public MutableThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
+ method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+ method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+ method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+ property @FloatRange(from=0.0, to=1.0) public float progressFraction;
+ property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+ }
+
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
}
@@ -125,6 +138,30 @@
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ field public static final androidx.compose.material3.adaptive.layout.PaneMotion.Companion Companion;
+ }
+
+ public static final class PaneMotion.Companion {
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getAnimateBounds();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeft();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromLeftDelayed();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRight();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterFromRightDelayed();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getEnterWithExpand();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToLeft();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitToRight();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getExitWithShrink();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getNoMotion();
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion AnimateBounds;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeft;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromLeftDelayed;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRight;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterFromRightDelayed;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion EnterWithExpand;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToLeft;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitToRight;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion ExitWithShrink;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion NoMotion;
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
@@ -283,17 +320,13 @@
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class ThreePaneScaffoldState {
- ctor public ThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
- method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
- method @FloatRange(from=0.0, to=1.0) public float getProgressFraction();
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
- method public suspend Object? seekTo(@FloatRange(from=0.0, to=1.0) float fraction, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? snapTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
- property @FloatRange(from=0.0, to=1.0) public final float progressFraction;
- property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public abstract sealed class ThreePaneScaffoldState {
+ method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
+ method @FloatRange(from=0.0, to=1.0) public abstract float getProgressFraction();
+ method public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getTargetState();
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue currentState;
+ property @FloatRange(from=0.0, to=1.0) public abstract float progressFraction;
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt
index 4626533..e716554 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt
@@ -51,7 +51,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress0() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValueSinglePane) }
}
@@ -67,7 +67,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress10() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.1f, MockTargetScaffoldValueSinglePane) }
}
@@ -83,7 +83,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress15() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.15f, MockTargetScaffoldValueSinglePane) }
}
@@ -99,7 +99,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress20() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.2f, MockTargetScaffoldValueSinglePane) }
}
@@ -115,7 +115,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress50() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.5f, MockTargetScaffoldValueSinglePane) }
}
@@ -131,7 +131,7 @@
@Test
fun singlePaneLayout_defaultPaneMotion_progress100() {
rule.setContent {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValueSinglePane) }
}
@@ -147,7 +147,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress0() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValuePaneSwitching) }
}
@@ -163,7 +163,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress10() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) {
scaffoldState.seekTo(0.1f, MockTargetScaffoldValuePaneSwitching)
@@ -181,7 +181,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress15() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) {
scaffoldState.seekTo(0.15f, MockTargetScaffoldValuePaneSwitching)
@@ -199,7 +199,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress20() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) {
scaffoldState.seekTo(0.2f, MockTargetScaffoldValuePaneSwitching)
@@ -217,7 +217,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress50() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) {
scaffoldState.seekTo(0.5f, MockTargetScaffoldValuePaneSwitching)
@@ -235,7 +235,7 @@
@Test
fun dualPaneLayout_defaultPaneSwitching_progress100() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValuePaneSwitching) }
}
@@ -251,7 +251,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress0() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValuePaneShifting) }
}
@@ -267,7 +267,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress10() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.1f, MockTargetScaffoldValuePaneShifting) }
}
@@ -283,7 +283,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress15() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) {
scaffoldState.seekTo(0.15f, MockTargetScaffoldValuePaneShifting)
@@ -301,7 +301,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress20() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.2f, MockTargetScaffoldValuePaneShifting) }
}
@@ -317,7 +317,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress50() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(0.5f, MockTargetScaffoldValuePaneShifting) }
}
@@ -333,7 +333,7 @@
@Test
fun dualPaneLayout_defaultPaneShifting_progress100() {
rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
- val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ val scaffoldState = MutableThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
SampleThreePaneScaffold(scaffoldState)
LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValuePaneShifting) }
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
index 10d5ba2..a55e8ba 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
@@ -23,18 +23,18 @@
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.AnimateBounds
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterFromLeft
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterFromLeftDelayed
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterFromRight
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterFromRightDelayed
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterWithExpand
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.ExitToLeft
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.ExitToRight
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.ExitWithShrink
-import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.NoMotion
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion.Expanded
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion.Hidden
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.AnimateBounds
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.EnterFromLeft
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.EnterFromLeftDelayed
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.EnterFromRight
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.EnterFromRightDelayed
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.EnterWithExpand
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.ExitToLeft
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.ExitToRight
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.ExitWithShrink
+import androidx.compose.material3.adaptive.layout.PaneMotion.Companion.NoMotion
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -79,7 +79,7 @@
ExitWithShrink.assertTransitions(EnterTransition.None, mockExitWithShrinkTransition)
}
- private fun DefaultPaneMotion.assertTransitions(
+ private fun PaneMotion.assertTransitions(
expectedEnterTransition: EnterTransition,
expectedExitTransition: ExitTransition
) {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
index df65278..7059197 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -43,7 +43,7 @@
modifier: Modifier = Modifier,
content: (@Composable AnimatedPaneScope.() -> Unit),
) {
- val animatingBounds = paneMotion == DefaultPaneMotion.AnimateBounds
+ val animatingBounds = paneMotion == PaneMotion.AnimateBounds
val motionProgress = { motionProgress }
scaffoldStateTransition.AnimatedVisibility(
visible = { value: T -> value[paneRole] != PaneAdaptedValue.Hidden },
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
index 58798db..8d99c09 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
@@ -97,7 +97,7 @@
*/
@ExperimentalMaterial3AdaptiveApi
class PaneMotionData internal constructor() {
- var motion: PaneMotion = DefaultPaneMotion.NoMotion
+ var motion: PaneMotion = PaneMotion.NoMotion
internal set
var currentSize: IntSize = IntSize.Zero
@@ -119,8 +119,8 @@
// Find the right edge offset of the rightmost pane that enters from its left
paneMotionDataList.fastForEachReversed {
if (
- it.motion == DefaultPaneMotion.EnterFromLeft ||
- it.motion == DefaultPaneMotion.EnterFromLeftDelayed
+ it.motion == PaneMotion.EnterFromLeft ||
+ it.motion == PaneMotion.EnterFromLeftDelayed
) {
return -it.targetPosition.x - it.targetSize.width
}
@@ -134,8 +134,8 @@
// Find the left edge offset of the leftmost pane that enters from its right
paneMotionDataList.fastForEach {
if (
- it.motion == DefaultPaneMotion.EnterFromRight ||
- it.motion == DefaultPaneMotion.EnterFromRightDelayed
+ it.motion == PaneMotion.EnterFromRight ||
+ it.motion == PaneMotion.EnterFromRightDelayed
) {
return scaffoldSize.width - it.targetPosition.x
}
@@ -148,7 +148,7 @@
get() {
// Find the right edge offset of the rightmost pane that exits to its left
paneMotionDataList.fastForEachReversed {
- if (it.motion == DefaultPaneMotion.ExitToLeft) {
+ if (it.motion == PaneMotion.ExitToLeft) {
return -it.currentPosition.x - it.currentSize.width
}
}
@@ -160,7 +160,7 @@
get() {
// Find the left edge offset of the leftmost pane that exits to its right
paneMotionDataList.fastForEach {
- if (it.motion == DefaultPaneMotion.ExitToRight) {
+ if (it.motion == PaneMotion.ExitToRight) {
return scaffoldSize.width - it.currentPosition.x
}
}
@@ -175,66 +175,116 @@
/** The [ExitTransition] of a pane under the given [PaneScaffoldMotionScope] */
val PaneScaffoldMotionScope.exitTransition: ExitTransition
-}
-@ExperimentalMaterial3AdaptiveApi
-@JvmInline
-internal value class DefaultPaneMotion private constructor(val value: Int) : PaneMotion {
- companion object {
- val NoMotion = DefaultPaneMotion(0)
- val AnimateBounds = DefaultPaneMotion(1)
- val EnterFromLeft = DefaultPaneMotion(2)
- val EnterFromRight = DefaultPaneMotion(3)
- val EnterFromLeftDelayed = DefaultPaneMotion(4)
- val EnterFromRightDelayed = DefaultPaneMotion(5)
- val ExitToLeft = DefaultPaneMotion(6)
- val ExitToRight = DefaultPaneMotion(7)
- val EnterWithExpand = DefaultPaneMotion(8)
- val ExitWithShrink = DefaultPaneMotion(9)
+ private abstract class DefaultImpl(val name: String) : PaneMotion {
+ override val PaneScaffoldMotionScope.enterTransition
+ get() = EnterTransition.None
+
+ override val PaneScaffoldMotionScope.exitTransition
+ get() = ExitTransition.None
+
+ override fun toString() = name
}
- override val PaneScaffoldMotionScope.enterTransition: EnterTransition
- get() =
- when (this@DefaultPaneMotion) {
- EnterFromLeft ->
- slideInHorizontally(positionAnimationSpec) { slideInFromLeftOffset }
- EnterFromRight ->
- slideInHorizontally(positionAnimationSpec) { slideInFromRightOffset }
- EnterFromLeftDelayed ->
- slideInHorizontally(delayedPositionAnimationSpec) { slideInFromLeftOffset }
- EnterFromRightDelayed ->
- slideInHorizontally(delayedPositionAnimationSpec) { slideInFromRightOffset }
- // TODO(conradche): Figure out how to expand with position change
- EnterWithExpand ->
- expandHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
- else -> EnterTransition.None
+ companion object {
+ /** The default pane motion that no animation will be performed. */
+ val NoMotion: PaneMotion = object : DefaultImpl("NoMotion") {}
+
+ /**
+ * The default pane motion that will animate panes bounds with the given animation specs
+ * during motion. Note that this should only be used when the associated pane is keeping
+ * showing during the motion.
+ */
+ val AnimateBounds: PaneMotion = object : DefaultImpl("AnimateBounds") {}
+
+ /**
+ * The default pane motion that will slide panes in from left. Note that this should only be
+ * used when the associated pane is entering - i.e. becoming visible from a hidden state.
+ */
+ val EnterFromLeft: PaneMotion =
+ object : DefaultImpl("EnterFromLeft") {
+ override val PaneScaffoldMotionScope.enterTransition
+ get() = slideInHorizontally(positionAnimationSpec) { slideInFromLeftOffset }
}
- override val PaneScaffoldMotionScope.exitTransition: ExitTransition
- get() =
- when (this@DefaultPaneMotion) {
- ExitToLeft -> slideOutHorizontally(positionAnimationSpec) { slideOutToLeftOffset }
- ExitToRight -> slideOutHorizontally(positionAnimationSpec) { slideOutToRightOffset }
- // TODO(conradche): Figure out how to shrink with position change
- ExitWithShrink ->
- shrinkHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
- else -> ExitTransition.None
+ /**
+ * The default pane motion that will slide panes in from right. Note that this should only
+ * be used when the associated pane is entering - i.e. becoming visible from a hidden state.
+ */
+ val EnterFromRight: PaneMotion =
+ object : DefaultImpl("EnterFromRight") {
+ override val PaneScaffoldMotionScope.enterTransition
+ get() = slideInHorizontally(positionAnimationSpec) { slideInFromRightOffset }
}
- override fun toString(): String =
- when (this) {
- NoMotion -> "NoMotion"
- AnimateBounds -> "AnimateBounds"
- EnterFromLeft -> "EnterFromLeft"
- EnterFromRight -> "EnterFromRight"
- EnterFromLeftDelayed -> "EnterFromLeftDelayed"
- EnterFromRightDelayed -> "EnterFromRightDelayed"
- ExitToLeft -> "ExitToLeft"
- ExitToRight -> "ExitToRight"
- EnterWithExpand -> "EnterWithExpand"
- ExitWithShrink -> "ExitWithShrink"
- else -> "Undefined($value)"
- }
+ /**
+ * The default pane motion that will slide panes in from left with a delay, usually to avoid
+ * the interference of other exiting panes. Note that this should only be used when the
+ * associated pane is entering - i.e. becoming visible from a hidden state.
+ */
+ val EnterFromLeftDelayed: PaneMotion =
+ object : DefaultImpl("EnterFromLeftDelayed") {
+ override val PaneScaffoldMotionScope.enterTransition
+ get() =
+ slideInHorizontally(delayedPositionAnimationSpec) { slideInFromLeftOffset }
+ }
+
+ /**
+ * The default pane motion that will slide panes in from right with a delay, usually to
+ * avoid the interference of other exiting panes. Note that this should only be used when
+ * the associated pane is entering - i.e. becoming visible from a hidden state.
+ */
+ val EnterFromRightDelayed: PaneMotion =
+ object : DefaultImpl("EnterFromRightDelayed") {
+ override val PaneScaffoldMotionScope.enterTransition
+ get() =
+ slideInHorizontally(delayedPositionAnimationSpec) { slideInFromRightOffset }
+ }
+
+ /**
+ * The default pane motion that will slide panes out to left. Note that this should only be
+ * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
+ */
+ val ExitToLeft: PaneMotion =
+ object : DefaultImpl("ExitToLeft") {
+ override val PaneScaffoldMotionScope.exitTransition
+ get() = slideOutHorizontally(positionAnimationSpec) { slideOutToLeftOffset }
+ }
+
+ /**
+ * The default pane motion that will slide panes out to right. Note that this should only be
+ * used when the associated pane is exiting - i.e. becoming hidden from a visible state.
+ */
+ val ExitToRight: PaneMotion =
+ object : DefaultImpl("ExitToRight") {
+ override val PaneScaffoldMotionScope.exitTransition
+ get() = slideOutHorizontally(positionAnimationSpec) { slideOutToRightOffset }
+ }
+
+ /**
+ * The default pane motion that will expand panes from a zero size. Note that this should
+ * only be used when the associated pane is entering - i.e. becoming visible from a hidden
+ * state.
+ */
+ val EnterWithExpand: PaneMotion =
+ object : DefaultImpl("EnterWithExpand") {
+ // TODO(conradchen): Expand with position change
+ override val PaneScaffoldMotionScope.enterTransition
+ get() = expandHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
+ }
+
+ /**
+ * The default pane motion that will shrink panes until it's gone. Note that this should
+ * only be used when the associated pane is exiting - i.e. becoming hidden from a visible
+ * state.
+ */
+ val ExitWithShrink: PaneMotion =
+ object : DefaultImpl("ExitWithShrink") {
+ // TODO(conradchen): Shrink with position change
+ override val PaneScaffoldMotionScope.exitTransition
+ get() = shrinkHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
+ }
+ }
}
@ExperimentalMaterial3AdaptiveApi
@@ -245,7 +295,7 @@
): List<PaneMotion> {
val numOfPanes = paneOrder.size
val paneStatus = Array(numOfPanes) { PaneMotionStatus.Hidden }
- val paneMotions = MutableList<PaneMotion>(numOfPanes) { DefaultPaneMotion.NoMotion }
+ val paneMotions = MutableList(numOfPanes) { PaneMotion.NoMotion }
var firstShownPaneIndex = numOfPanes
var firstEnteringPaneIndex = numOfPanes
var lastShownPaneIndex = -1
@@ -261,7 +311,7 @@
PaneMotionStatus.Shown -> {
firstShownPaneIndex = min(firstShownPaneIndex, i)
lastShownPaneIndex = max(lastShownPaneIndex, i)
- paneMotions[i] = DefaultPaneMotion.AnimateBounds
+ paneMotions[i] = PaneMotion.AnimateBounds
}
PaneMotionStatus.Entering -> {
firstEnteringPaneIndex = min(firstEnteringPaneIndex, i)
@@ -286,26 +336,26 @@
// No panes will interfere the motion on the right, exit to right.
hasPanesExitToRight = true
firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
- DefaultPaneMotion.ExitToRight
+ PaneMotion.ExitToRight
} else if (!hasShownPanesOnLeft && !hasEnteringPanesOnLeft) {
// No panes will interfere the motion on the left, exit to left.
hasPanesExitToLeft = true
lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
- DefaultPaneMotion.ExitToLeft
+ PaneMotion.ExitToLeft
} else if (!hasShownPanesOnRight) {
// Only showing panes can interfere the motion on the right, exit to right.
hasPanesExitToRight = true
firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
- DefaultPaneMotion.ExitToRight
+ PaneMotion.ExitToRight
} else if (!hasShownPanesOnLeft) { // Only showing panes on left
// Only showing panes can interfere the motion on the left, exit to left.
hasPanesExitToLeft = true
lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
- DefaultPaneMotion.ExitToLeft
+ PaneMotion.ExitToLeft
} else {
// Both sides has panes that keep being visible during transition, shrink to
// exit
- DefaultPaneMotion.ExitWithShrink
+ PaneMotion.ExitWithShrink
}
}
}
@@ -326,20 +376,20 @@
paneMotions[i] =
if (noBlockingPanesOnRight && !hasPanesExitToRight) {
// No panes will block the motion on the right, enter from right.
- DefaultPaneMotion.EnterFromRight
+ PaneMotion.EnterFromRight
} else if (noBlockingPanesOnLeft && !hasPanesExitToLeft) {
// No panes will block the motion on the left, enter from left.
- DefaultPaneMotion.EnterFromLeft
+ PaneMotion.EnterFromLeft
} else if (noBlockingPanesOnRight) {
// Only hiding panes can interfere the motion on the right, enter from right.
- DefaultPaneMotion.EnterFromRightDelayed
+ PaneMotion.EnterFromRightDelayed
} else if (noBlockingPanesOnLeft) {
// Only hiding panes can interfere the motion on the left, enter from left.
- DefaultPaneMotion.EnterFromLeftDelayed
+ PaneMotion.EnterFromLeftDelayed
} else {
// Both sides has panes that keep being visible during transition, expand to
// enter
- DefaultPaneMotion.EnterWithExpand
+ PaneMotion.EnterWithExpand
}
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
index 23a9c59..7924ad8 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
@@ -235,11 +235,7 @@
companion object {
/** A default [ThreePaneMotion] instance that specifies no motions. */
val NoMotion =
- ThreePaneMotion(
- DefaultPaneMotion.NoMotion,
- DefaultPaneMotion.NoMotion,
- DefaultPaneMotion.NoMotion
- )
+ ThreePaneMotion(PaneMotion.NoMotion, PaneMotion.NoMotion, PaneMotion.NoMotion)
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index ad081d7..c4346cf 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -85,7 +85,7 @@
null,
primaryPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
) {
- val scaffoldState = remember { ThreePaneScaffoldState(scaffoldValue) }
+ val scaffoldState = remember { MutableThreePaneScaffoldState(scaffoldValue) }
LaunchedEffect(key1 = scaffoldValue) { scaffoldState.animateTo(scaffoldValue) }
ThreePaneScaffold(
modifier = modifier,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
index 8454908..15420c9 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
@@ -51,7 +51,7 @@
override val paneRole: ThreePaneScaffoldRole,
scaffoldScope: ThreePaneScaffoldScope,
) : ThreePaneScaffoldPaneScope, ThreePaneScaffoldScope by scaffoldScope {
- override var paneMotion: PaneMotion by mutableStateOf(DefaultPaneMotion.ExitToLeft)
+ override var paneMotion: PaneMotion by mutableStateOf(PaneMotion.ExitToLeft)
private set
fun updatePaneMotion(paneMotions: ThreePaneMotion) {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
index 6363568..21077d3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
@@ -27,33 +27,23 @@
import androidx.compose.runtime.Stable
/**
- * The state of a three pane scaffold. It serves as the [SeekableTransitionState] to manipulate the
- * [Transition] between [ThreePaneScaffoldValue]s.
+ * A read-only state of a three pane scaffold. It provides information about the [Transition]
+ * between [ThreePaneScaffoldValue]s.
*/
@ExperimentalMaterial3AdaptiveApi
@Stable
-class ThreePaneScaffoldState
-internal constructor(
- private val transitionState: SeekableTransitionState<ThreePaneScaffoldValue>,
-) {
- /** Constructs a [ThreePaneScaffoldState] with an initial value of [initialScaffoldValue]. */
- constructor(
- initialScaffoldValue: ThreePaneScaffoldValue
- ) : this(SeekableTransitionState(initialScaffoldValue))
-
+sealed class ThreePaneScaffoldState {
/**
* Current [ThreePaneScaffoldValue] state of the transition. If there is an active transition,
* [currentState] and [targetState] are different.
*/
- val currentState
- get() = transitionState.currentState
+ abstract val currentState: ThreePaneScaffoldValue
/**
* Target [ThreePaneScaffoldValue] state of the transition. If this is the same as
* [currentState], no transition is active.
*/
- val targetState
- get() = transitionState.targetState
+ abstract val targetState: ThreePaneScaffoldValue
/**
* The progress of the transition from [currentState] to [targetState] as a fraction of the
@@ -61,26 +51,46 @@
*
* If [targetState] and [currentState] are the same, [progressFraction] will be 0.
*/
+ @get:FloatRange(from = 0.0, to = 1.0) abstract val progressFraction: Float
+
+ @Composable internal abstract fun rememberTransition(): Transition<ThreePaneScaffoldValue>
+}
+
+/**
+ * The seekable state of a three pane scaffold. It serves as the [SeekableTransitionState] to
+ * manipulate the [Transition] between [ThreePaneScaffoldValue]s.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Stable
+class MutableThreePaneScaffoldState(initialScaffoldValue: ThreePaneScaffoldValue) :
+ ThreePaneScaffoldState() {
+ private val transitionState = SeekableTransitionState(initialScaffoldValue)
+
+ override val currentState
+ get() = transitionState.currentState
+
+ override val targetState
+ get() = transitionState.targetState
+
@get:FloatRange(from = 0.0, to = 1.0)
- val progressFraction
+ override val progressFraction
get() = transitionState.fraction
private val mutatorMutex = MutatorMutex()
/**
- * Creates a [Transition] and puts it in the [currentState] of this [ThreePaneScaffoldState]. If
- * [targetState] changes, the [Transition] will change where it will animate to.
- *
- * @param label The optional label for the transition.
+ * Creates a [Transition] and puts it in the [currentState] of this
+ * [MutableThreePaneScaffoldState]. If [targetState] changes, the [Transition] will change where
+ * it will animate to.
*/
@Composable
- internal fun rememberTransition(label: String? = null): Transition<ThreePaneScaffoldValue> =
- rememberTransition(transitionState, label)
+ override fun rememberTransition(): Transition<ThreePaneScaffoldValue> =
+ rememberTransition(transitionState, label = "ThreePaneScaffoldState")
/**
- * Sets [currentState] and [targetState][ThreePaneScaffoldState.targetState] to [targetState]
- * and snaps all values to those at that state. The transition will not have any animations
- * running after running [snapTo].
+ * Sets [currentState] and [targetState][MutableThreePaneScaffoldState.targetState] to
+ * [targetState] and snaps all values to those at that state. The transition will not have any
+ * animations running after running [snapTo].
*
* @param targetState The [ThreePaneScaffoldValue] state to snap to.
* @see SeekableTransitionState.snapTo
@@ -105,8 +115,8 @@
}
/**
- * Updates the current [targetState][ThreePaneScaffoldState.targetState] to [targetState] with
- * an animation to the new state.
+ * Updates the current [targetState][MutableThreePaneScaffoldState.targetState] to [targetState]
+ * with an animation to the new state.
*
* @param targetState The [ThreePaneScaffoldValue] state to animate towards.
* @param animationSpec If provided, is used to animate the animation fraction. If `null`, the
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
index 2f2af3d..d119518 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
@@ -47,7 +47,7 @@
* @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
* how many expanded panes can be shown at the same time.
* @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
- * the default value will be [ThreePaneScaffoldDefaults.threePaneScaffoldAdaptStrategies].
+ * the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
* @param currentDestination The current destination item, which will be treated as having the
* highest priority, can be `null`.
*/
@@ -84,7 +84,7 @@
* @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
* how many expanded panes can be shown at the same time.
* @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
- * the default value will be [ThreePaneScaffoldDefaults.threePaneScaffoldAdaptStrategies].
+ * the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
* @param destinationHistory The history of past destination items. The last destination will have
* the highest priority, and the second last destination will have the second highest priority,
* and so forth until all panes have a priority assigned. Note that the last destination is
diff --git a/compose/material3/adaptive/adaptive-navigation/api/current.txt b/compose/material3/adaptive/adaptive-navigation/api/current.txt
index 4612aff..6acaa74 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/current.txt
@@ -25,15 +25,18 @@
method public boolean canNavigateBack(optional String backNavigationBehavior);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? getCurrentDestination();
method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
method public boolean isDestinationHistoryAware();
- method public boolean navigateBack(optional String backNavigationBehavior);
- method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
+ method public suspend Object? navigateBack(optional String backNavigationBehavior, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
+ method public suspend Object? seekBack(optional String backNavigationBehavior, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public void setDestinationHistoryAware(boolean);
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
property public abstract boolean isDestinationHistoryAware;
property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue scaffoldValue;
}
diff --git a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
index 4612aff..6acaa74 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
@@ -25,15 +25,18 @@
method public boolean canNavigateBack(optional String backNavigationBehavior);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? getCurrentDestination();
method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective getScaffoldDirective();
+ method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState getScaffoldState();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
method public boolean isDestinationHistoryAware();
- method public boolean navigateBack(optional String backNavigationBehavior);
- method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
+ method public suspend Object? navigateBack(optional String backNavigationBehavior, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
+ method public suspend Object? seekBack(optional String backNavigationBehavior, optional @FloatRange(from=0.0, to=1.0) float fraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public void setDestinationHistoryAware(boolean);
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
property public abstract boolean isDestinationHistoryAware;
property public abstract androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective;
+ property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState;
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue scaffoldValue;
}
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
index 5296dfd..28068dd 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
@@ -23,12 +23,16 @@
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,10 +45,12 @@
@Test
fun singlePaneLayout_navigateTo_makeDestinationPaneExpanded() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator<Int>(
scaffoldDirective = MockSinglePaneScaffoldDirective
@@ -52,7 +58,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Hidden)
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
@@ -70,10 +76,12 @@
@Test
fun dualPaneLayout_navigateTo_keepDestinationPaneExpanded() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator<Int>(
scaffoldDirective = MockDualPaneScaffoldDirective
@@ -81,7 +89,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Expanded)
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
@@ -99,10 +107,12 @@
@Test
fun dualPaneLayout_navigateToExtra_hideListWhenNotHistoryAware() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator<Int>(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -111,7 +121,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.List])
.isEqualTo(PaneAdaptedValue.Expanded)
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Extra, 0)
@@ -129,10 +139,12 @@
@Test
fun dualPaneLayout_navigateToExtra_keepListExpandedWhenHistoryAware() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator<Int>(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -141,7 +153,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.List])
.isEqualTo(PaneAdaptedValue.Expanded)
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Extra, 0)
@@ -159,10 +171,12 @@
@Test
fun singlePaneLayout_navigateBack_makeDestinationPaneHidden() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator<Int>(
scaffoldDirective = MockSinglePaneScaffoldDirective
@@ -170,9 +184,11 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle { scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0) }
+ scope.runBlockingOnIdle {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
+ }
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -218,9 +234,11 @@
@Test
fun dualPaneLayout_withSimplePop_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -232,7 +250,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -253,9 +271,11 @@
@Test
fun dualPaneLayout_enforceCurrentDestinationChange_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -268,7 +288,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
@@ -319,9 +339,11 @@
@Test
fun dualPaneLayout_enforceContentChange_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -334,7 +356,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
@@ -354,9 +376,11 @@
@Test
fun dualPaneLayout_enforceContentChange_canNavigateBack_withOnlyScaffoldValueChange() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -369,7 +393,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Extra)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
@@ -416,9 +440,11 @@
@Test
fun dualPaneLayout_enforceScaffoldChangeWhenHistoryAware_notSkipBackstackEntry() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -431,7 +457,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Hidden,
PaneAdaptedValue.Expanded,
@@ -443,7 +469,7 @@
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -469,9 +495,11 @@
@Test
fun dualPaneLayout_enforceScaffoldChangeWhenNotHistoryAware_skipBackstackEntry() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -484,7 +512,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -496,7 +524,7 @@
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -522,9 +550,11 @@
@Test
fun singlePaneLayout_previousScaffoldValue_popLatest() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockSinglePaneScaffoldDirective,
@@ -536,7 +566,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.List])
@@ -581,9 +611,11 @@
@Test
fun singlePaneLayout_previousScaffoldValue_popUntilScaffoldValueChange() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockSinglePaneScaffoldDirective,
@@ -596,7 +628,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.scaffoldValue[ListDetailPaneScaffoldRole.List])
@@ -676,6 +708,11 @@
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
+
+ private fun CoroutineScope.runBlockingOnIdle(block: suspend CoroutineScope.() -> Unit) {
+ val job = composeRule.runOnIdle { launch(block = block) }
+ runBlocking { job.join() }
+ }
}
private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective.Default
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
index ef9c1bf..7e15c38 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
@@ -23,12 +23,16 @@
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,10 +45,12 @@
@Test
fun singlePaneLayout_navigateTo_makeDestinationPaneExpanded() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator<Int>(
scaffoldDirective = MockSinglePaneScaffoldDirective
@@ -52,7 +58,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Hidden)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting, 0)
@@ -70,10 +76,12 @@
@Test
fun dualPaneLayout_navigateTo_keepDestinationPaneExpanded() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator<Int>(
scaffoldDirective = MockDualPaneScaffoldDirective
@@ -81,7 +89,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Main])
.isEqualTo(PaneAdaptedValue.Expanded)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
@@ -99,10 +107,12 @@
@Test
fun dualPaneLayout_navigateToExtra_hideSupportingWhenNotHistoryAware() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
initialDestinationHistory =
@@ -118,7 +128,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -139,10 +149,12 @@
@Test
fun dualPaneLayout_navigateToExtra_keepSupportingExpandedWhenHistoryAware() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
initialDestinationHistory =
@@ -158,7 +170,7 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -179,10 +191,12 @@
@Test
fun singlePaneLayout_navigateBack_makeDestinationPaneHidden() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
var canNavigateBack by Delegates.notNull<Boolean>()
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator<Int>(
scaffoldDirective = MockSinglePaneScaffoldDirective
@@ -190,11 +204,11 @@
canNavigateBack = scaffoldNavigator.canNavigateBack()
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting, 0)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -243,9 +257,11 @@
@Test
fun dualPaneLayout_withSimplePop_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -260,7 +276,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Main])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
@@ -281,9 +297,11 @@
@Test
fun dualPaneLayout_enforceCurrentDestinationChange_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -302,7 +320,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
@@ -353,9 +371,11 @@
@Test
fun dualPaneLayout_enforceContentChange_canNavigateBack() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -374,7 +394,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
@@ -394,9 +414,11 @@
@Test
fun dualPaneLayout_enforceContentChange_canNavigateBack_withOnlyScaffoldValueChange() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -412,7 +434,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Extra)
assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
@@ -462,9 +484,11 @@
@Test
fun dualPaneLayout_enforceScaffoldChangeWhenHistoryAware_notSkipBackstackEntry() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -480,7 +504,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Hidden,
PaneAdaptedValue.Expanded,
@@ -492,7 +516,7 @@
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -518,9 +542,11 @@
@Test
fun dualPaneLayout_enforceScaffoldChangeWhenNotHistoryAware_skipBackstackEntry() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective = MockDualPaneScaffoldDirective,
@@ -536,7 +562,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -548,7 +574,7 @@
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
scaffoldNavigator.scaffoldValue.assert(
PaneAdaptedValue.Expanded,
PaneAdaptedValue.Expanded,
@@ -574,9 +600,11 @@
@Test
fun singlePaneLayout_previousScaffoldValue_popLatest() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockSinglePaneScaffoldDirective,
@@ -591,7 +619,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Main])
@@ -636,9 +664,11 @@
@Test
fun singlePaneLayout_previousScaffoldValue_popUntilScaffoldValueChange() {
+ lateinit var scope: CoroutineScope
lateinit var scaffoldNavigator: ThreePaneScaffoldNavigator<Int>
composeRule.setContent {
+ scope = rememberCoroutineScope()
scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = MockSinglePaneScaffoldDirective,
@@ -657,7 +687,7 @@
)
}
- composeRule.runOnIdle {
+ scope.runBlockingOnIdle {
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting])
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Main])
@@ -740,6 +770,11 @@
assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
+
+ private fun CoroutineScope.runBlockingOnIdle(block: suspend CoroutineScope.() -> Unit) {
+ val job = composeRule.runOnIdle { launch(block = block) }
+ runBlocking { job.join() }
+ }
}
private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective.Default
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
index 859c8b8..9b802fc8 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
@@ -16,24 +16,30 @@
package androidx.compose.material3.adaptive.navigation
-import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.Animatable
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold as BaseListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneExpansionDragHandle
import androidx.compose.material3.adaptive.layout.PaneExpansionState
-import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold as BaseSupportingPaneScaffold
+import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
+import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.ThreePaneMotion
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
import androidx.compose.material3.adaptive.layout.calculateListDetailPaneScaffoldMotion
import androidx.compose.material3.adaptive.layout.calculateSupportingPaneScaffoldMotion
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
/**
- * A version of [androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold] that supports
- * navigation and back handling out of the box, controlled by [ThreePaneScaffoldNavigator].
+ * A version of [ListDetailPaneScaffold] that supports navigation and predictive back handling out
+ * of the box, controlled by [ThreePaneScaffoldNavigator].
*
* @param navigator The navigator instance to navigate through the scaffold.
* @param listPane the list pane of the scaffold, which is supposed to hold a list of item summaries
@@ -67,19 +73,23 @@
modifier: Modifier = Modifier,
extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
defaultBackBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilContentChange,
- paneMotions: ThreePaneMotion = calculateListDetailPaneScaffoldMotion(navigator.scaffoldValue),
+ paneMotions: ThreePaneMotion = navigator.scaffoldState.calculateListDetailPaneScaffoldMotion(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue),
) {
- // TODO(b/330584029): support predictive back
- BackHandler(enabled = navigator.canNavigateBack(defaultBackBehavior)) {
- navigator.navigateBack(defaultBackBehavior)
- }
- BaseListDetailPaneScaffold(
- modifier = modifier,
+ val predictiveBackScale = remember { Animatable(initialValue = 1f) }
+
+ ThreePaneScaffoldPredictiveBackHandler(
+ navigator = navigator,
+ backBehavior = defaultBackBehavior,
+ scale = predictiveBackScale,
+ )
+
+ ListDetailPaneScaffold(
+ modifier = modifier.predictiveBackTransform(predictiveBackScale::value),
directive = navigator.scaffoldDirective,
- value = navigator.scaffoldValue,
+ scaffoldState = navigator.scaffoldState,
detailPane = detailPane,
listPane = listPane,
extraPane = extraPane,
@@ -90,8 +100,8 @@
}
/**
- * A version of [androidx.compose.material3.adaptive.layout.SupportingPaneScaffold] that supports
- * navigation and back handling out of the box, controlled by [ThreePaneScaffoldNavigator].
+ * A version of [SupportingPaneScaffold] that supports navigation and predictive back handling out
+ * of the box, controlled by [ThreePaneScaffoldNavigator].
*
* @param navigator The navigator instance to navigate through the scaffold.
* @param mainPane the main pane of the scaffold, which is supposed to hold the major content of an
@@ -124,19 +134,23 @@
modifier: Modifier = Modifier,
extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
defaultBackBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilContentChange,
- paneMotions: ThreePaneMotion = calculateSupportingPaneScaffoldMotion(navigator.scaffoldValue),
+ paneMotions: ThreePaneMotion = navigator.scaffoldState.calculateSupportingPaneScaffoldMotion(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue),
) {
- // TODO(b/330584029): support predictive back
- BackHandler(enabled = navigator.canNavigateBack(defaultBackBehavior)) {
- navigator.navigateBack(defaultBackBehavior)
- }
- BaseSupportingPaneScaffold(
- modifier = modifier,
+ val predictiveBackScale = remember { Animatable(initialValue = 1f) }
+
+ ThreePaneScaffoldPredictiveBackHandler(
+ navigator = navigator,
+ backBehavior = defaultBackBehavior,
+ scale = predictiveBackScale,
+ )
+
+ SupportingPaneScaffold(
+ modifier = modifier.predictiveBackTransform(predictiveBackScale::value),
directive = navigator.scaffoldDirective,
- value = navigator.scaffoldValue,
+ scaffoldState = navigator.scaffoldState,
mainPane = mainPane,
supportingPane = supportingPane,
extraPane = extraPane,
@@ -145,3 +159,12 @@
paneExpansionState = paneExpansionState,
)
}
+
+private fun Modifier.predictiveBackTransform(scale: () -> Float): Modifier = graphicsLayer {
+ val scaleValue = scale()
+ scaleX = scaleValue
+ scaleY = scaleValue
+ transformOrigin = TransformOriginTopCenter
+}
+
+private val TransformOriginTopCenter = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldPredictiveBackHandler.android.kt b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldPredictiveBackHandler.android.kt
new file mode 100644
index 0000000..afaaf59
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldPredictiveBackHandler.android.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.navigation
+
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.util.lerp
+import kotlin.coroutines.cancellation.CancellationException
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun ThreePaneScaffoldPredictiveBackHandler(
+ navigator: ThreePaneScaffoldNavigator<Any>,
+ backBehavior: BackNavigationBehavior,
+ scale: Animatable<Float, AnimationVector1D>,
+) {
+ fun backProgressToAnimationProgress(value: Float): Float =
+ PredictiveBackDefaults.Easing.transform(value) *
+ when (navigator.scaffoldValue.expandedCount) {
+ 1 -> PredictiveBackDefaults.SinglePaneProgressRatio
+ 2 -> PredictiveBackDefaults.DualPaneProgressRatio
+ else -> PredictiveBackDefaults.TriplePaneProgressRatio
+ }
+ fun backProgressToScale(value: Float): Float =
+ lerp(1f, PredictiveBackDefaults.MinScale, PredictiveBackDefaults.Easing.transform(value))
+
+ key(navigator, backBehavior) {
+ PredictiveBackHandler(enabled = navigator.canNavigateBack(backBehavior)) { progress ->
+ // code for gesture back started
+ try {
+ progress.collect { backEvent ->
+ scale.snapTo(backProgressToScale(backEvent.progress))
+ navigator.seekBack(
+ backBehavior,
+ fraction = backProgressToAnimationProgress(backEvent.progress),
+ )
+ }
+ // code for completion
+ scale.animateTo(1f)
+ navigator.navigateBack(backBehavior)
+ } catch (e: CancellationException) {
+ // code for cancellation
+ scale.animateTo(1f)
+ navigator.seekBack(backBehavior, fraction = 0f)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val ThreePaneScaffoldValue.expandedCount: Int
+ get() {
+ var count = 0
+ if (primary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (secondary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (tertiary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ return count
+ }
+
+private object PredictiveBackDefaults {
+ val Easing: Easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ const val MinScale: Float = 0.95f
+ const val SinglePaneProgressRatio: Float = 0.1f
+ const val DualPaneProgressRatio: Float = 0.15f
+ const val TriplePaneProgressRatio: Float = 0.2f
+}
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
index 42378e4..7ea6353 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
@@ -16,11 +16,13 @@
package androidx.compose.material3.adaptive.navigation
+import androidx.annotation.FloatRange
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.layout.MutableThreePaneScaffoldState
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults
@@ -28,6 +30,7 @@
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.calculateThreePaneScaffoldValue
@@ -73,7 +76,13 @@
val scaffoldDirective: PaneScaffoldDirective
/**
- * The current layout value of the associated three pane scaffold value, which represents unique
+ * The current state of the associated three pane scaffold, used to query the transition between
+ * layout states.
+ */
+ val scaffoldState: ThreePaneScaffoldState
+
+ /**
+ * The current layout value of the associated three pane scaffold, which represents unique
* layout states of the scaffold.
*/
val scaffoldValue: ThreePaneScaffoldValue
@@ -118,7 +127,7 @@
* @param pane the new destination pane.
* @param contentKey the optional key or id representing the content of the new destination.
*/
- fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T? = null)
+ suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T? = null)
/**
* Returns `true` if there is a previous destination to navigate back to.
@@ -144,10 +153,27 @@
* @param backNavigationBehavior the behavior describing which backstack entries may be skipped
* during the back navigation. See [BackNavigationBehavior].
*/
- fun navigateBack(
+ suspend fun navigateBack(
backNavigationBehavior: BackNavigationBehavior =
BackNavigationBehavior.PopUntilScaffoldValueChange
): Boolean
+
+ /**
+ * Seeks the [scaffoldState] transition to the previous destination, as in a predictive back
+ * animation.
+ *
+ * This does not affect the current [scaffoldValue] or backstack. To do so, call [navigateBack]
+ * when the back navigation action is finalized.
+ *
+ * @param backNavigationBehavior the behavior describing which backstack entries may be skipped
+ * during the back navigation. See [BackNavigationBehavior].
+ * @param fraction the progress fraction of the transition of backwards navigation.
+ */
+ suspend fun seekBack(
+ backNavigationBehavior: BackNavigationBehavior =
+ BackNavigationBehavior.PopUntilScaffoldValueChange,
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1.0f,
+ )
}
/**
@@ -341,6 +367,9 @@
calculateScaffoldValue(destinationHistory.lastIndex)
}
+ // Must be updated whenever `destinationHistory` changes to keep in sync.
+ override val scaffoldState = MutableThreePaneScaffoldState(scaffoldValue)
+
override fun peekPreviousScaffoldValue(
backNavigationBehavior: BackNavigationBehavior
): ThreePaneScaffoldValue {
@@ -348,26 +377,40 @@
return if (index == -1) scaffoldValue else calculateScaffoldValue(index)
}
- override fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) {
+ override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) {
destinationHistory.add(ThreePaneScaffoldDestinationItem(pane, contentKey))
+ animateStateToCurrentScaffoldValue()
}
override fun canNavigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean =
getPreviousDestinationIndex(backNavigationBehavior) >= 0
- override fun navigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean {
+ override suspend fun navigateBack(
+ backNavigationBehavior: BackNavigationBehavior,
+ ): Boolean {
val previousDestinationIndex = getPreviousDestinationIndex(backNavigationBehavior)
if (previousDestinationIndex < 0) {
destinationHistory.clear()
+ animateStateToCurrentScaffoldValue()
return false
}
val targetSize = previousDestinationIndex + 1
while (destinationHistory.size > targetSize) {
destinationHistory.removeLastKt()
}
+ animateStateToCurrentScaffoldValue()
return true
}
+ override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) {
+ val previousScaffoldValue = peekPreviousScaffoldValue(backNavigationBehavior)
+ scaffoldState.seekTo(fraction, previousScaffoldValue)
+ }
+
+ private suspend fun animateStateToCurrentScaffoldValue() {
+ scaffoldState.animateTo(scaffoldValue)
+ }
+
private fun getPreviousDestinationIndex(backNavBehavior: BackNavigationBehavior): Int {
if (destinationHistory.size <= 1) {
// No previous destination
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index d30081c..9e242d3 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -61,6 +61,7 @@
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@@ -68,6 +69,7 @@
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@@ -75,6 +77,7 @@
@Composable
fun ListDetailPaneScaffoldSample() {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
+ val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
value = scaffoldNavigator.scaffoldValue,
@@ -84,7 +87,11 @@
) {
Surface(
color = MaterialTheme.colorScheme.secondary,
- onClick = { scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) }
+ onClick = {
+ coroutineScope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
+ }
+ }
) {
Text("List")
}
@@ -94,7 +101,7 @@
AnimatedPane(modifier = Modifier) {
Surface(
color = MaterialTheme.colorScheme.primary,
- onClick = { scaffoldNavigator.navigateBack() }
+ onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
) {
Text("Details")
}
@@ -109,6 +116,7 @@
@Composable
fun ListDetailPaneScaffoldSampleWithExtraPane() {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
+ val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
value = scaffoldNavigator.scaffoldValue,
@@ -118,7 +126,11 @@
) {
Surface(
color = MaterialTheme.colorScheme.secondary,
- onClick = { scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) }
+ onClick = {
+ coroutineScope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
+ }
+ }
) {
Text("List")
}
@@ -136,7 +148,9 @@
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Surface(
- onClick = { scaffoldNavigator.navigateBack() },
+ onClick = {
+ coroutineScope.launch { scaffoldNavigator.navigateBack() }
+ },
modifier = Modifier.weight(0.5f).fillMaxHeight(),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
) {
@@ -150,7 +164,11 @@
VerticalDivider()
Surface(
onClick = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Extra)
+ coroutineScope.launch {
+ scaffoldNavigator.navigateTo(
+ ListDetailPaneScaffoldRole.Extra
+ )
+ }
},
modifier = Modifier.weight(0.5f).fillMaxHeight(),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
@@ -172,7 +190,7 @@
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.tertiary,
- onClick = { scaffoldNavigator.navigateBack() }
+ onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
) {
Text("Extra")
}
@@ -202,9 +220,9 @@
val listDetailRoute = "listdetail"
val items = List(15) { "Item $it" }
val loremIpsum =
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " +
- "tempor incididunt ut labore et dolore magna aliqua. Dui nunc mattis enim ut tellus " +
- "elementum sagittis. Nunc sed augue lacus viverra vitae. Sit amet dictum sit amet justo " +
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
+ "incididunt ut labore et dolore magna aliqua. Dui nunc mattis enim ut tellus " +
+ "elementum sagittis. Nunc sed augue lacus viverra vitae. Sit amet dictum sit amet " +
"donec. Fringilla urna porttitor rhoncus dolor purus non enim praesent elementum."
@Composable
@@ -262,6 +280,7 @@
// scaffold navigator to be aware of its content, you can pass `Nothing`.
val navController = rememberNavController()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<String>()
+ val coroutineScope = rememberCoroutineScope()
NavHost(
navController = navController,
@@ -304,7 +323,7 @@
}
BackHandler(enabled = scaffoldNavigator.canNavigateBack(backBehavior)) {
- scaffoldNavigator.navigateBack(backBehavior)
+ coroutineScope.launch { scaffoldNavigator.navigateBack(backBehavior) }
}
ListDetailPaneScaffold(
@@ -326,10 +345,13 @@
modifier =
Modifier.clickable {
if (item != selectedItem) {
- scaffoldNavigator.navigateTo(
- pane = ListDetailPaneScaffoldRole.Detail,
- contentKey = item,
- )
+ coroutineScope.launch {
+ scaffoldNavigator.navigateTo(
+ pane =
+ ListDetailPaneScaffoldRole.Detail,
+ contentKey = item,
+ )
+ }
}
}
)
@@ -357,7 +379,9 @@
) {
IconButton(
onClick = {
- scaffoldNavigator.navigateBack(backBehavior)
+ coroutineScope.launch {
+ scaffoldNavigator.navigateBack(backBehavior)
+ }
},
content = {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 2fb36a5..a67bce1 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1419,17 +1419,12 @@
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> fastSpatialSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> slowEffectsSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> slowSpatialSpec();
+ field public static final androidx.compose.material3.MotionScheme.Companion Companion;
}
- public final class MotionSchemeKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.MotionScheme expressiveMotionScheme();
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberDefaultEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberDefaultSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberFastEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberFastSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberSlowEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberSlowSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.MotionScheme standardMotionScheme();
+ public static final class MotionScheme.Companion {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.MotionScheme expressiveMotionScheme();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.MotionScheme standardMotionScheme();
}
public interface MultiChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -1972,12 +1967,13 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors(optional long selectedIconColor, optional long selectedTextColor, optional long selectedIndicatorColor, optional long unselectedIconColor, optional long unselectedTextColor, optional long disabledIconColor, optional long disabledTextColor);
field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
}
public final class ShortNavigationBarKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2753,7 +2749,8 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class TopAppBarDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors centerAlignedTopAppBarColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors centerAlignedTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec, optional boolean reverseLayout);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method public float getLargeAppBarCollapsedHeight();
method public float getLargeAppBarExpandedHeight();
@@ -2975,6 +2972,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
method public int getArrangement();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
@@ -2988,6 +2986,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors(optional long selectedIconColor, optional long selectedTextColor, optional long selectedIndicatorColor, optional long unselectedIconColor, optional long unselectedTextColor, optional long disabledIconColor, optional long disabledTextColor);
method public int iconPositionFor(boolean railExpanded);
field public static final androidx.compose.material3.WideNavigationRailItemDefaults INSTANCE;
}
@@ -2996,7 +2995,7 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void DismissibleModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DismissibleModalWideNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalWideNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> scrimOnClick, optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public final class WideNavigationRailStateKt {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 2fb36a5..a67bce1 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1419,17 +1419,12 @@
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> fastSpatialSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> slowEffectsSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> slowSpatialSpec();
+ field public static final androidx.compose.material3.MotionScheme.Companion Companion;
}
- public final class MotionSchemeKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.MotionScheme expressiveMotionScheme();
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberDefaultEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberDefaultSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberFastEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberFastSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberSlowEffectsSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static inline <reified T> androidx.compose.animation.core.FiniteAnimationSpec<T> rememberSlowSpatialSpec(androidx.compose.material3.MotionScheme);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.MotionScheme standardMotionScheme();
+ public static final class MotionScheme.Companion {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.MotionScheme expressiveMotionScheme();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.MotionScheme standardMotionScheme();
}
public interface MultiChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -1972,12 +1967,13 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors(optional long selectedIconColor, optional long selectedTextColor, optional long selectedIndicatorColor, optional long unselectedIconColor, optional long unselectedTextColor, optional long disabledIconColor, optional long disabledTextColor);
field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
}
public final class ShortNavigationBarKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2753,7 +2749,8 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class TopAppBarDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors centerAlignedTopAppBarColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors centerAlignedTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec, optional boolean reverseLayout);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method public float getLargeAppBarCollapsedHeight();
method public float getLargeAppBarExpandedHeight();
@@ -2975,6 +2972,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
method public int getArrangement();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
@@ -2988,6 +2986,7 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailItemDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors(optional long selectedIconColor, optional long selectedTextColor, optional long selectedIndicatorColor, optional long unselectedIconColor, optional long unselectedTextColor, optional long disabledIconColor, optional long disabledTextColor);
method public int iconPositionFor(boolean railExpanded);
field public static final androidx.compose.material3.WideNavigationRailItemDefaults INSTANCE;
}
@@ -2996,7 +2995,7 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void DismissibleModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DismissibleModalWideNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalWideNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> scrimOnClick, optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public final class WideNavigationRailStateKt {
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
index f46d70c..a9d6feb 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SegmentedButtonSamples.kt
@@ -18,11 +18,11 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.size
-import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.TrendingUp
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.StarBorder
+import androidx.compose.material3.Icon
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
index d67790d..5089341 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -21,9 +21,13 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
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.systemBars
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@@ -63,13 +67,16 @@
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
@@ -1438,6 +1445,131 @@
assertThat(TopAppBarSmallTokens.ContainerHeight).isLessThan(boundsAfter.height)
}
+ // Disabled on older APIs which seem to run on a small Nexus device that fails this test.
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ @Test
+ fun topAppBar_enterAlways_scrollingAndContentMovement() {
+ lateinit var scrollBehavior: TopAppBarScrollBehavior
+ lateinit var state: LazyListState
+ var appBarHeightPx = 0f
+ rule.setMaterialContentForSizeAssertions {
+ scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+ state = rememberLazyListState()
+ appBarHeightPx = with(rule.density) { TopAppBarSmallTokens.ContainerHeight.toPx() }
+ Scaffold(
+ modifier = Modifier.fillMaxSize().consumeWindowInsets(WindowInsets.systemBars),
+ topBar = { TopAppBar(title = { Text("Title") }, scrollBehavior = scrollBehavior) },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier =
+ Modifier.fillMaxSize()
+ .nestedScroll(scrollBehavior.nestedScrollConnection)
+ .padding(paddingValues)
+ .testTag(LazyListTag),
+ state = state,
+ ) {
+ items(100) { i ->
+ Text(
+ modifier =
+ Modifier.fillMaxWidth().height(40.dp).padding(horizontal = 16.dp),
+ text = "Item $i",
+ )
+ }
+ }
+ }
+ }
+
+ // Swipe up to scroll the content and collapse the top app bar.
+ rule.onNodeWithTag(LazyListTag).performTouchInput {
+ swipeUp(startY = height - 200f, endY = height - 1000f)
+ }
+ rule.waitForIdle()
+
+ // Store the first visible item's top offset.
+ val topVisibleItemIndex = state.layoutInfo.visibleItemsInfo.first().index
+ val topItemTopBeforeExpansion =
+ rule.onNodeWithTag(LazyListTag).onChildAt(topVisibleItemIndex).getBoundsInRoot().top
+
+ // Swipe down to trigger a top app bar expansion without scrolling much the content.
+ rule.onNodeWithTag(LazyListTag).performTouchInput {
+ swipeDown(startY = height - 1000f, endY = height - (1000f - appBarHeightPx / 2))
+ }
+ rule.waitForIdle()
+
+ // Asserts that the first item has moved along with the expansion of the top app bar.
+ rule
+ .onNodeWithTag(LazyListTag)
+ .onChildAt(topVisibleItemIndex)
+ .assertTopPositionInRootIsEqualTo(
+ topItemTopBeforeExpansion + TopAppBarSmallTokens.ContainerHeight
+ )
+ }
+
+ @Test
+ fun topAppBar_enterAlways_reverseLayout_scrollingAndContentMovement() {
+ lateinit var scrollBehavior: TopAppBarScrollBehavior
+ lateinit var state: LazyListState
+ var appBarHeightPx = 0f
+
+ rule.setMaterialContentForSizeAssertions {
+ scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(reverseLayout = true)
+ state = rememberLazyListState()
+ appBarHeightPx = with(rule.density) { TopAppBarSmallTokens.ContainerHeight.toPx() }
+ Scaffold(
+ modifier = Modifier.fillMaxSize().consumeWindowInsets(WindowInsets.systemBars),
+ topBar = {
+ TopAppBar(
+ title = { Text("Title") },
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ )
+ },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier =
+ Modifier.fillMaxSize()
+ .nestedScroll(scrollBehavior.nestedScrollConnection)
+ .padding(paddingValues)
+ .testTag(LazyListTag),
+ state = state,
+ reverseLayout = true,
+ ) {
+ items(100) { i ->
+ Text(
+ modifier =
+ Modifier.fillMaxWidth().height(48.dp).padding(horizontal = 16.dp),
+ text = "Item $i",
+ )
+ }
+ }
+ }
+ }
+
+ // Swipe down to scroll the content in the reverse layout.
+ rule.onNodeWithTag(LazyListTag).performTouchInput {
+ swipeDown(startY = height - 1000f, endY = height - 300f)
+ }
+ rule.waitForIdle()
+
+ // Swipe up to trigger a top app bar collapse without scrolling much the content.
+ rule.onNodeWithTag(LazyListTag).performTouchInput {
+ down(Offset(x = width / 2f, y = height / 2f))
+ moveTo(Offset(x = width / 2f, y = height / 2f - appBarHeightPx + 50))
+ }
+ rule.waitForIdle()
+
+ // Asserts that the first item has moved along with the collapsing of the top app bar.
+ val newTopVisibleItemIndex = state.layoutInfo.visibleItemsInfo.last().index
+ val bottomAppBarWhileCollapsing =
+ rule.onNodeWithTag(TopAppBarTestTag).getBoundsInRoot().bottom
+ val topVisibleItemTopWhileCollapsing =
+ rule.onNodeWithText("Item $newTopVisibleItemIndex").getBoundsInRoot().top
+ topVisibleItemTopWhileCollapsing.assertIsEqualTo(
+ expected = bottomAppBarWhileCollapsing,
+ subject = "Top item comparison to bottom app bar"
+ )
+ }
+
@Test
fun state_restoresTopAppBarState() {
val restorationTester = StateRestorationTester(rule)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 3e166ef..40418e9 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -80,6 +80,7 @@
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
@@ -1026,4 +1027,62 @@
}
}
}
+
+ @Test
+ fun bottomSheetScaffold_testDragHandleClick() {
+ lateinit var sheetState: SheetState
+ rule.setContent {
+ sheetState = rememberStandardBottomSheetState()
+ BottomSheetScaffold(
+ sheetContent = {
+ Box(Modifier.fillMaxWidth().requiredHeight(sheetHeight).testTag(sheetTag))
+ },
+ sheetDragHandle = { Box(Modifier.testTag(dragHandleTag).size(dragHandleSize)) },
+ sheetPeekHeight = peekHeight,
+ scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState)
+ ) {
+ Text("Content")
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+ }
+
+ @Test
+ fun bottomSheetScaffold_testDragHandleClick_hiddenStateAllowed() {
+ lateinit var sheetState: SheetState
+ rule.setContent {
+ sheetState = rememberStandardBottomSheetState(skipHiddenState = false)
+ BottomSheetScaffold(
+ sheetContent = {
+ Box(Modifier.fillMaxWidth().requiredHeight(sheetHeight).testTag(sheetTag))
+ },
+ sheetDragHandle = { Box(Modifier.testTag(dragHandleTag).size(dragHandleSize)) },
+ sheetPeekHeight = peekHeight,
+ scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState)
+ ) {
+ Text("Content")
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+ }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
index c0565c4..9f1854f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DatePickerScreenshotTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material3
import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.testutils.assertAgainstGolden
@@ -32,6 +33,9 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.MockedMethod
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
@@ -39,6 +43,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.quality.Strictness
@RunWith(Parameterized::class)
@LargeTest
@@ -68,6 +73,76 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun datePicker_todayMarker() {
+ val year = 2021
+ val month = 1
+ val day = 10
+ runWithMockedLocalDate(mockedToday = LocalDate.of(year, month, day)) {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ val monthInUtcMillis =
+ dayInUtcMilliseconds(year = year, month = month, dayOfMonth = day)
+ DatePicker(
+ state =
+ rememberDatePickerState(initialDisplayedMonthMillis = monthInUtcMillis),
+ showModeToggle = false
+ )
+ }
+ }
+ assertAgainstGolden("datePicker_todayMarker_${scheme.name}")
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun datePicker_disabledTodayMarker() {
+ val year = 2021
+ val month = 1
+ val day = 10
+ runWithMockedLocalDate(mockedToday = LocalDate.of(year, month, day)) {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ val monthInUtcMillis =
+ dayInUtcMilliseconds(year = year, month = month, dayOfMonth = day)
+ DatePicker(
+ state =
+ rememberDatePickerState(
+ initialDisplayedMonthMillis = monthInUtcMillis,
+ selectableDates =
+ object : SelectableDates {
+ override fun isSelectableDate(
+ utcTimeMillis: Long
+ ): Boolean = false
+ }
+ ),
+ showModeToggle = false
+ )
+ }
+ }
+ assertAgainstGolden("datePicker_disabledTodayMarker_${scheme.name}")
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun runWithMockedLocalDate(mockedToday: LocalDate, test: () -> Unit) {
+ val session =
+ mockitoSession()
+ .spyStatic(LocalDate::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ try {
+ // Mock `LocalDate.now()` to return a specific date. This will mark the today marker
+ // on the month displayed in this test.
+ doReturn(mockedToday).`when`(MockedMethod { LocalDate.now() })
+ // Run the test
+ test()
+ } finally {
+ session.finishMocking()
+ }
+ }
+
+ @Test
fun datePicker_withModeToggle() {
rule.setMaterialContent(scheme.colorScheme) {
Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
index 074b5a7..9b34712 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -780,8 +780,8 @@
backgroundColor = Color.Red,
backgroundSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
shapeSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
- shapeAndBackgroundCenter =
- with(rule.density) { Offset(70.dp.toPx(), 70.dp.toPx()) },
+ shapeCenter = with(rule.density) { Offset(70.dp.toPx(), 70.dp.toPx()) },
+ backgroundCenter = with(rule.density) { Offset(70.dp.toPx(), 70.dp.toPx()) },
antiAliasingGap = with(rule.density) { 3.dp.toPx() }
)
}
@@ -840,8 +840,8 @@
backgroundColor = Color.Red,
backgroundSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
shapeSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
- shapeAndBackgroundCenter =
- with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ shapeCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ backgroundCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
antiAliasingGap = with(rule.density) { 2.dp.toPx() }
)
}
@@ -900,8 +900,8 @@
backgroundColor = Color.Red,
backgroundSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
shapeSize = with(rule.density) { DpSize(60.dp, 60.dp).toSize() },
- shapeAndBackgroundCenter =
- with(rule.density) { Offset(30.dp.toPx(), 30.dp.toPx()) },
+ shapeCenter = with(rule.density) { Offset(30.dp.toPx(), 30.dp.toPx()) },
+ backgroundCenter = with(rule.density) { Offset(30.dp.toPx(), 30.dp.toPx()) },
antiAliasingGap = with(rule.density) { 2.dp.toPx() }
)
}
@@ -955,8 +955,8 @@
backgroundColor = Color.Red,
backgroundSize = with(rule.density) { DpSize(100.dp, 100.dp).toSize() },
shapeSize = with(rule.density) { DpSize(100.dp, 100.dp).toSize() },
- shapeAndBackgroundCenter =
- with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ shapeCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ backgroundCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
antiAliasingGap = with(rule.density) { 2.dp.toPx() }
)
}
@@ -999,8 +999,8 @@
backgroundColor = Color.Red,
backgroundSize = with(rule.density) { DpSize(100.dp, 100.dp).toSize() },
shapeSize = with(rule.density) { DpSize(100.dp, 100.dp).toSize() },
- shapeAndBackgroundCenter =
- with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ shapeCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
+ backgroundCenter = with(rule.density) { Offset(50.dp.toPx(), 50.dp.toPx()) },
antiAliasingGap = with(rule.density) { 2.dp.toPx() }
)
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 0aa561b..870c16a 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -1150,6 +1150,32 @@
}
}
+ @Test
+ fun modalBottomSheet_testDragHandleClick() {
+ lateinit var sheetState: SheetState
+ rule.setContent {
+ sheetState = rememberModalBottomSheetState()
+ ModalBottomSheet(
+ onDismissRequest = {},
+ sheetState = sheetState,
+ dragHandle = { Box(Modifier.testTag(dragHandleTag).size(dragHandleSize)) },
+ ) {
+ Box(Modifier.fillMaxSize().testTag(sheetTag))
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+ rule.onNodeWithTag(dragHandleTag, useUnmergedTree = true).performClick()
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden)
+ }
+
private val Bundle.traversalBefore: Int
get() = getInt("android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL")
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
index b119e6e..ac1c17a 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
@@ -494,6 +494,40 @@
}
@Test
+ fun item_customColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val customColors =
+ ShortNavigationBarItemDefaults.colors(
+ selectedIconColor = Color.Red,
+ unselectedTextColor = Color.Green,
+ )
+
+ ShortNavigationBar {
+ ShortNavigationBarItem(
+ colors = customColors,
+ icon = { Truth.assertThat(LocalContentColor.current).isEqualTo(Color.Red) },
+ label = {
+ Truth.assertThat(LocalContentColor.current)
+ .isEqualTo(NavigationBarTokens.ItemActiveLabelTextColor.value)
+ },
+ selected = true,
+ onClick = {}
+ )
+ ShortNavigationBarItem(
+ colors = customColors,
+ icon = {
+ Truth.assertThat(LocalContentColor.current)
+ .isEqualTo(NavigationBarTokens.ItemInactiveIconColor.value)
+ },
+ label = { Truth.assertThat(LocalContentColor.current).isEqualTo(Color.Green) },
+ selected = false,
+ onClick = {}
+ )
+ }
+ }
+ }
+
+ @Test
fun itemContent_topIconPosition_sizeAndPosition() {
var minSize: Dp? = null
rule.setMaterialContent(lightColorScheme()) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
index fdfaeea..e568dd5 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
@@ -26,6 +26,7 @@
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.tokens.NavigationRailBaselineItemTokens
import androidx.compose.material3.tokens.NavigationRailCollapsedTokens
+import androidx.compose.material3.tokens.NavigationRailColorTokens
import androidx.compose.material3.tokens.NavigationRailExpandedTokens
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -444,6 +445,40 @@
}
@Test
+ fun item_customColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val customColors =
+ WideNavigationRailItemDefaults.colors(
+ selectedIconColor = Color.Red,
+ unselectedTextColor = Color.Green,
+ )
+
+ WideNavigationRail {
+ WideNavigationRailItem(
+ colors = customColors,
+ icon = { Truth.assertThat(LocalContentColor.current).isEqualTo(Color.Red) },
+ label = {
+ Truth.assertThat(LocalContentColor.current)
+ .isEqualTo(NavigationRailColorTokens.ItemActiveLabelText.value)
+ },
+ selected = true,
+ onClick = {}
+ )
+ WideNavigationRailItem(
+ colors = customColors,
+ icon = {
+ Truth.assertThat(LocalContentColor.current)
+ .isEqualTo(NavigationRailColorTokens.ItemInactiveLabelText.value)
+ },
+ label = { Truth.assertThat(LocalContentColor.current).isEqualTo(Color.Green) },
+ selected = false,
+ onClick = {}
+ )
+ }
+ }
+ }
+
+ @Test
fun header_position() {
rule.setMaterialContent(lightColorScheme()) {
WideNavigationRail(header = { Box(Modifier.testTag("header").size(10.dp)) }) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index 9193761..75b8038 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -1589,6 +1589,7 @@
* @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the top
* app bar when the user flings the app bar itself, or the content below it
*/
+ @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
@ExperimentalMaterial3Api
@Composable
fun enterAlwaysScrollBehavior(
@@ -1597,12 +1598,49 @@
// TODO Load the motionScheme tokens from the component tokens file
snapAnimationSpec: AnimationSpec<Float>? = MotionSchemeKeyTokens.DefaultEffects.value(),
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
+ ): TopAppBarScrollBehavior {
+ return enterAlwaysScrollBehavior(
+ state = state,
+ canScroll = canScroll,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ reverseLayout = false
+ )
+ }
+
+ /**
+ * Returns a [TopAppBarScrollBehavior]. A top app bar that is set up with this
+ * [TopAppBarScrollBehavior] will immediately collapse when the content is pulled up, and will
+ * immediately appear when the content is pulled down.
+ *
+ * @param state the state object to be used to control or observe the top app bar's scroll
+ * state. See [rememberTopAppBarState] for a state that is remembered across compositions.
+ * @param canScroll a callback used to determine whether scroll events are to be handled by this
+ * [EnterAlwaysScrollBehavior]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the top app bar snaps
+ * to either fully collapsed or fully extended state when a fling or a drag scrolled it into
+ * an intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the top
+ * app bar when the user flings the app bar itself, or the content below it
+ * @param reverseLayout indicates that this behavior is applied to a scrollable content that has
+ * a reversed direction of scrolling and layout
+ */
+ @ExperimentalMaterial3Api
+ @Composable
+ fun enterAlwaysScrollBehavior(
+ state: TopAppBarState = rememberTopAppBarState(),
+ canScroll: () -> Boolean = { true },
+ // TODO Load the motionScheme tokens from the component tokens file
+ snapAnimationSpec: AnimationSpec<Float>? = MotionSchemeKeyTokens.DefaultEffects.value(),
+ flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay(),
+ reverseLayout: Boolean = false
): TopAppBarScrollBehavior =
EnterAlwaysScrollBehavior(
state = state,
snapAnimationSpec = snapAnimationSpec,
flingAnimationSpec = flingAnimationSpec,
- canScroll = canScroll
+ canScroll = canScroll,
+ reverseLayout = reverseLayout
)
/**
@@ -2811,13 +2849,16 @@
* bar when the user flings the app bar itself, or the content below it
* @param canScroll a callback used to determine whether scroll events are to be handled by this
* [EnterAlwaysScrollBehavior]
+ * @param reverseLayout indicates that this behavior is applied to a scrollable content that has a
+ * reversed direction of scrolling and layout
*/
@OptIn(ExperimentalMaterial3Api::class)
private class EnterAlwaysScrollBehavior(
override val state: TopAppBarState,
override val snapAnimationSpec: AnimationSpec<Float>?,
override val flingAnimationSpec: DecayAnimationSpec<Float>?,
- val canScroll: () -> Boolean = { true }
+ val canScroll: () -> Boolean = { true },
+ val reverseLayout: Boolean = false
) : TopAppBarScrollBehavior {
override val isPinned: Boolean = false
override var nestedScrollConnection =
@@ -2825,8 +2866,14 @@
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (!canScroll()) return Offset.Zero
val prevHeightOffset = state.heightOffset
- state.heightOffset = state.heightOffset + available.y
- return if (prevHeightOffset != state.heightOffset) {
+ state.heightOffset += available.y
+ // The state's heightOffset is coerce in a minimum value of heightOffsetLimit and a
+ // maximum value 0f, so we check if its value was actually changed after the
+ // available.y was added to it in order to tell if the top app bar is currently
+ // collapsing or expanding.
+ // Note that when the content was set with a revered layout, we always return a
+ // zero offset.
+ return if (!reverseLayout && prevHeightOffset != state.heightOffset) {
// We're in the middle of top app bar collapse or expand.
// Consume only the scroll on the Y axis.
available.copy(x = 0f)
@@ -2849,7 +2896,7 @@
state.contentOffset = 0f
}
}
- state.heightOffset = state.heightOffset + consumed.y
+ if (!reverseLayout) state.heightOffset += consumed.y
return Offset.Zero
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
index 8ef2938..8e2eabd 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
@@ -17,6 +17,7 @@
package androidx.compose.material3
import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -328,37 +329,59 @@
val dismissActionLabel = getString(Strings.BottomSheetDismissDescription)
val expandActionLabel = getString(Strings.BottomSheetExpandDescription)
Box(
- Modifier.align(CenterHorizontally).semantics(mergeDescendants = true) {
- with(state) {
- // Provides semantics to interact with the bottomsheet if there is more
- // than one anchor to swipe to and swiping is enabled.
- if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) {
- if (currentValue == PartiallyExpanded) {
- if (anchoredDraggableState.confirmValueChange(Expanded)) {
- expand(expandActionLabel) {
- scope.launch { expand() }
- true
+ modifier =
+ Modifier.align(CenterHorizontally)
+ .clickable {
+ when (state.currentValue) {
+ Expanded ->
+ scope.launch {
+ if (!state.skipHiddenState) {
+ state.hide()
+ } else {
+ state.partialExpand()
+ }
}
- }
- } else {
- if (
- anchoredDraggableState.confirmValueChange(PartiallyExpanded)
- ) {
- collapse(partialExpandActionLabel) {
- scope.launch { partialExpand() }
- true
- }
- }
- }
- if (!state.skipHiddenState) {
- dismiss(dismissActionLabel) {
- scope.launch { hide() }
- true
- }
+ PartiallyExpanded -> scope.launch { state.expand() }
+ else -> scope.launch { state.show() }
}
}
- }
- },
+ .semantics(mergeDescendants = true) {
+ with(state) {
+ // Provides semantics to interact with the bottomsheet if there
+ // is more than one anchor to swipe to and swiping is enabled.
+ if (
+ anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled
+ ) {
+ if (currentValue == PartiallyExpanded) {
+ if (
+ anchoredDraggableState.confirmValueChange(Expanded)
+ ) {
+ expand(expandActionLabel) {
+ scope.launch { expand() }
+ true
+ }
+ }
+ } else {
+ if (
+ anchoredDraggableState.confirmValueChange(
+ PartiallyExpanded
+ )
+ ) {
+ collapse(partialExpandActionLabel) {
+ scope.launch { partialExpand() }
+ true
+ }
+ }
+ }
+ if (!state.skipHiddenState) {
+ dismiss(dismissActionLabel) {
+ scope.launch { hide() }
+ true
+ }
+ }
+ }
+ }
+ },
) {
dragHandle()
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
index 3857026..f3190ed 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
@@ -902,7 +902,7 @@
selected && !enabled -> disabledSelectedDayContentColor
inRange && enabled -> dayInSelectionRangeContentColor
inRange && !enabled -> disabledDayContentColor
- isToday -> todayContentColor
+ isToday && enabled -> todayContentColor
enabled -> dayContentColor
else -> disabledDayContentColor
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialTheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialTheme.kt
index b0ccf55..7cffbca 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialTheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialTheme.kt
@@ -189,7 +189,7 @@
CompositionLocalProvider(LocalUsingExpressiveTheme provides true) {
MaterialTheme(
colorScheme = colorScheme ?: expressiveLightColorScheme(),
- motionScheme = motionScheme ?: expressiveMotionScheme(),
+ motionScheme = motionScheme ?: MotionScheme.expressiveMotionScheme(),
shapes = shapes ?: Shapes(),
// TODO: replace with calls to Expressive typography default
typography = typography ?: Typography(),
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index 4185410f7..9e0d86f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
@@ -328,35 +329,46 @@
val dismissActionLabel = getString(Strings.BottomSheetDismissDescription)
val expandActionLabel = getString(Strings.BottomSheetExpandDescription)
Box(
- Modifier.align(Alignment.CenterHorizontally).semantics(
- mergeDescendants = true
- ) {
- // Provides semantics to interact with the bottomsheet based on its
- // current value.
- with(sheetState) {
- dismiss(dismissActionLabel) {
- animateToDismiss()
- true
- }
- if (currentValue == PartiallyExpanded) {
- expand(expandActionLabel) {
- if (anchoredDraggableState.confirmValueChange(Expanded)) {
- scope.launch { sheetState.expand() }
- }
- true
- }
- } else if (hasPartiallyExpandedState) {
- collapse(collapseActionLabel) {
- if (
- anchoredDraggableState.confirmValueChange(PartiallyExpanded)
- ) {
- scope.launch { partialExpand() }
- }
- true
+ modifier =
+ Modifier.align(Alignment.CenterHorizontally)
+ .clickable {
+ when (sheetState.currentValue) {
+ Expanded -> scope.launch { sheetState.hide() }
+ PartiallyExpanded -> scope.launch { sheetState.expand() }
+ else -> scope.launch { sheetState.show() }
}
}
- }
- }
+ .semantics(mergeDescendants = true) {
+ // Provides semantics to interact with the bottomsheet based on its
+ // current value.
+ with(sheetState) {
+ dismiss(dismissActionLabel) {
+ animateToDismiss()
+ true
+ }
+ if (currentValue == PartiallyExpanded) {
+ expand(expandActionLabel) {
+ if (
+ anchoredDraggableState.confirmValueChange(Expanded)
+ ) {
+ scope.launch { sheetState.expand() }
+ }
+ true
+ }
+ } else if (hasPartiallyExpandedState) {
+ collapse(collapseActionLabel) {
+ if (
+ anchoredDraggableState.confirmValueChange(
+ PartiallyExpanded
+ )
+ ) {
+ scope.launch { partialExpand() }
+ }
+ true
+ }
+ }
+ }
+ },
) {
dragHandle()
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
index b19a579..1edebad 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
@@ -20,13 +20,15 @@
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.spring
+import androidx.compose.material3.MotionScheme.Companion.expressiveMotionScheme
+import androidx.compose.material3.MotionScheme.Companion.standardMotionScheme
import androidx.compose.material3.tokens.ExpressiveMotionTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.StandardMotionTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
/**
@@ -51,9 +53,6 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberDefaultSpatialSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T>
@@ -66,9 +65,6 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberFastSpatialSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> fastSpatialSpec(): FiniteAnimationSpec<T>
@@ -81,9 +77,6 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberSlowSpatialSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> slowSpatialSpec(): FiniteAnimationSpec<T>
@@ -95,9 +88,6 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberDefaultEffectsSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T>
@@ -109,9 +99,6 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberFastEffectsSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> fastEffectsSpec(): FiniteAnimationSpec<T>
@@ -123,199 +110,147 @@
*
* [T] is the generic data type that will be animated by the system, as long as the appropriate
* [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * When called from a Composable, use [rememberSlowEffectsSpec] extension to ensure that the
- * returned animation spec is remembered across compositions.
*/
fun <T> slowEffectsSpec(): FiniteAnimationSpec<T>
+
+ companion object {
+
+ /** Returns a standard Material motion scheme. */
+ @Suppress("UNCHECKED_CAST")
+ @ExperimentalMaterial3ExpressiveApi
+ fun standardMotionScheme(): MotionScheme =
+ object : MotionScheme {
+ private val defaultSpatialSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringDefaultSpatialDamping,
+ stiffness = StandardMotionTokens.SpringDefaultSpatialStiffness
+ )
+
+ private val fastSpatialSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringFastSpatialDamping,
+ stiffness = StandardMotionTokens.SpringFastSpatialStiffness
+ )
+
+ private val slowSpatialSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringSlowSpatialDamping,
+ stiffness = StandardMotionTokens.SpringSlowSpatialStiffness
+ )
+
+ private val defaultEffectsSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringDefaultEffectsDamping,
+ stiffness = StandardMotionTokens.SpringDefaultEffectsStiffness
+ )
+
+ private val fastEffectsSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringFastEffectsDamping,
+ stiffness = StandardMotionTokens.SpringFastEffectsStiffness
+ )
+
+ private val slowEffectsSpec =
+ spring<Any>(
+ dampingRatio = StandardMotionTokens.SpringSlowEffectsDamping,
+ stiffness = StandardMotionTokens.SpringSlowEffectsStiffness
+ )
+
+ override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
+ return defaultSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
+ return fastSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
+ return slowSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
+ return defaultEffectsSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
+ return fastEffectsSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
+ return slowEffectsSpec as FiniteAnimationSpec<T>
+ }
+ }
+
+ /** Returns an expressive Material motion scheme. */
+ @Suppress("UNCHECKED_CAST")
+ @ExperimentalMaterial3ExpressiveApi
+ fun expressiveMotionScheme(): MotionScheme =
+ object : MotionScheme {
+
+ private val defaultSpatialSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringDefaultSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringDefaultSpatialStiffness
+ )
+
+ private val fastSpatialSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringFastSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringFastSpatialStiffness
+ )
+
+ private val slowSpatialSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringSlowSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringSlowSpatialStiffness
+ )
+
+ private val defaultEffectsSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringDefaultEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringDefaultEffectsStiffness
+ )
+
+ private val fastEffectsSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringFastEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringFastEffectsStiffness
+ )
+
+ private val slowEffectsSpec =
+ spring<Any>(
+ dampingRatio = ExpressiveMotionTokens.SpringSlowEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringSlowEffectsStiffness
+ )
+
+ override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
+ return defaultSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
+ return fastSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
+ return slowSpatialSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
+ return defaultEffectsSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
+ return fastEffectsSpec as FiniteAnimationSpec<T>
+ }
+
+ override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
+ return slowEffectsSpec as FiniteAnimationSpec<T>
+ }
+ }
+ }
}
/**
- * A default spatial motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system, as long as the appropriate
- * [TwoWayConverter] for converting the data to and from an [AnimationVector] is supplied.
- *
- * @see [MotionScheme.defaultSpatialSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberDefaultSpatialSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = defaultSpatialSpec()
- spec
- }
-
-/**
- * A fast spatial motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system.
- *
- * @see [MotionScheme.fastSpatialSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberFastSpatialSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = fastSpatialSpec()
- spec
- }
-
-/**
- * A slow spatial motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system.
- *
- * @see [MotionScheme.slowSpatialSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberSlowSpatialSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = slowSpatialSpec()
- spec
- }
-
-/**
- * A default effects motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system.
- *
- * @see [MotionScheme.defaultEffectsSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberDefaultEffectsSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = defaultEffectsSpec()
- spec
- }
-
-/**
- * A fast effects motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system.
- *
- * @see [MotionScheme.fastEffectsSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberFastEffectsSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = fastEffectsSpec()
- spec
- }
-
-/**
- * A slow effects motion [FiniteAnimationSpec] that is remembered across compositions.
- *
- * [T] is the generic data type that will be animated by the system.
- *
- * @see [MotionScheme.slowEffectsSpec]
- */
-@ExperimentalMaterial3ExpressiveApi
-@Composable
-inline fun <reified T> MotionScheme.rememberSlowEffectsSpec() =
- remember(this, T::class) {
- val spec: FiniteAnimationSpec<T> = slowEffectsSpec()
- spec
- }
-
-/** Returns a standard Material motion scheme. */
-@ExperimentalMaterial3ExpressiveApi
-fun standardMotionScheme(): MotionScheme =
- object : MotionScheme {
- override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringDefaultSpatialDamping,
- stiffness = StandardMotionTokens.SpringDefaultSpatialStiffness
- )
- }
-
- override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringFastSpatialDamping,
- stiffness = StandardMotionTokens.SpringFastSpatialStiffness
- )
- }
-
- override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringSlowSpatialDamping,
- stiffness = StandardMotionTokens.SpringSlowSpatialStiffness
- )
- }
-
- override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringDefaultEffectsDamping,
- stiffness = StandardMotionTokens.SpringDefaultEffectsStiffness
- )
- }
-
- override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringFastEffectsDamping,
- stiffness = StandardMotionTokens.SpringFastEffectsStiffness
- )
- }
-
- override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = StandardMotionTokens.SpringSlowEffectsDamping,
- stiffness = StandardMotionTokens.SpringSlowEffectsStiffness
- )
- }
- }
-
-/** Returns an expressive Material motion scheme. */
-@ExperimentalMaterial3ExpressiveApi
-fun expressiveMotionScheme(): MotionScheme =
- object : MotionScheme {
- override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringDefaultSpatialDamping,
- stiffness = ExpressiveMotionTokens.SpringDefaultSpatialStiffness
- )
- }
-
- override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringFastSpatialDamping,
- stiffness = ExpressiveMotionTokens.SpringFastSpatialStiffness
- )
- }
-
- override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringSlowSpatialDamping,
- stiffness = ExpressiveMotionTokens.SpringSlowSpatialStiffness
- )
- }
-
- override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringDefaultEffectsDamping,
- stiffness = ExpressiveMotionTokens.SpringDefaultEffectsStiffness
- )
- }
-
- override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringFastEffectsDamping,
- stiffness = ExpressiveMotionTokens.SpringFastEffectsStiffness
- )
- }
-
- override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(
- dampingRatio = ExpressiveMotionTokens.SpringSlowEffectsDamping,
- stiffness = ExpressiveMotionTokens.SpringSlowEffectsStiffness
- )
- }
- }
-
-/**
* CompositionLocal used to pass [MotionScheme] down the tree.
*
* Setting the value here is typically done as part of [MaterialTheme]. To retrieve the current
@@ -324,7 +259,7 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
-internal val LocalMotionScheme = staticCompositionLocalOf { standardMotionScheme() }
+internal val LocalMotionScheme = staticCompositionLocalOf { MotionScheme.standardMotionScheme() }
/**
* Helper function for component motion tokens.
@@ -336,19 +271,16 @@
*
* @param value the token's value
*/
-@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Stable
-internal inline fun <reified T> MotionScheme.fromToken(
- value: MotionSchemeKeyTokens
-): FiniteAnimationSpec<T> {
+internal fun <T> MotionScheme.fromToken(value: MotionSchemeKeyTokens): FiniteAnimationSpec<T> {
return when (value) {
- MotionSchemeKeyTokens.DefaultSpatial -> rememberDefaultSpatialSpec()
- MotionSchemeKeyTokens.FastSpatial -> rememberFastSpatialSpec()
- MotionSchemeKeyTokens.SlowSpatial -> rememberSlowSpatialSpec()
- MotionSchemeKeyTokens.DefaultEffects -> rememberDefaultEffectsSpec()
- MotionSchemeKeyTokens.FastEffects -> rememberFastEffectsSpec()
- MotionSchemeKeyTokens.SlowEffects -> rememberSlowEffectsSpec()
+ MotionSchemeKeyTokens.DefaultSpatial -> defaultSpatialSpec()
+ MotionSchemeKeyTokens.FastSpatial -> fastSpatialSpec()
+ MotionSchemeKeyTokens.SlowSpatial -> slowSpatialSpec()
+ MotionSchemeKeyTokens.DefaultEffects -> defaultEffectsSpec()
+ MotionSchemeKeyTokens.FastEffects -> fastEffectsSpec()
+ MotionSchemeKeyTokens.SlowEffects -> slowEffectsSpec()
}
}
@@ -357,6 +289,7 @@
* [MotionScheme].
*/
@Composable
+@ReadOnlyComposable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-internal inline fun <reified T> MotionSchemeKeyTokens.value(): FiniteAnimationSpec<T> =
+internal fun <T> MotionSchemeKeyTokens.value(): FiniteAnimationSpec<T> =
MaterialTheme.motionScheme.fromToken(this)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 771d263..af4c3ad 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -221,7 +221,6 @@
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services
* @param label the text label for this item
- * @param badge optional badge to show on this item, typically a [Badge]
* @param iconPosition the [NavigationItemIconPosition] for this icon
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this item. You can create and pass in your own `remember`ed instance to observe
@@ -245,12 +244,12 @@
modifier: Modifier,
enabled: Boolean,
label: @Composable (() -> Unit)?,
- badge: (@Composable () -> Unit)?,
iconPosition: NavigationItemIconPosition,
interactionSource: MutableInteractionSource
) {
- val iconWithBadge: @Composable () -> Unit = {
- StyledIcon(selected, icon, colors, enabled, badge)
+ val iconColor = colors.iconColor(selected = selected, enabled = enabled)
+ val styledIcon: @Composable () -> Unit = {
+ CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
}
val styledLabel: @Composable (() -> Unit)? =
if (label == null) {
@@ -303,7 +302,7 @@
interactionSource = offsetInteractionSource ?: interactionSource,
indicatorColor = colors.selectedIndicatorColor,
indicatorShape = indicatorShape,
- icon = iconWithBadge,
+ icon = styledIcon,
iconPosition = iconPosition,
label = styledLabel,
indicatorAnimationProgress = { indicatorAnimationProgress.value.coerceAtLeast(0f) },
@@ -343,12 +342,12 @@
modifier: Modifier,
enabled: Boolean,
label: @Composable (() -> Unit),
- badge: (@Composable () -> Unit)?,
iconPosition: NavigationItemIconPosition,
interactionSource: MutableInteractionSource
) {
- val iconWithBadge: @Composable () -> Unit = {
- StyledIcon(selected, icon, colors, enabled, badge)
+ val iconColor = colors.iconColor(selected = selected, enabled = enabled)
+ val styledIcon: @Composable () -> Unit = {
+ CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
}
var itemWidth by remember { mutableIntStateOf(0) }
@@ -451,7 +450,7 @@
indicatorColor = colors.selectedIndicatorColor,
indicatorShape = indicatorShape,
indicatorAnimationProgress = { indicatorAnimationProgress.value.coerceAtLeast(0f) },
- icon = iconWithBadge,
+ icon = styledIcon,
iconPosition = iconPosition,
iconPositionProgress = { iconPositionProgress.coerceAtLeast(0f) },
labelTopIcon = labelTopIcon,
@@ -676,20 +675,15 @@
@Suppress("NAME_SHADOWING") val indicatorAnimationProgress = indicatorAnimationProgress()
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
// When measuring icon, account for the indicator in its constraints.
- val iconConstraints =
- looseConstraints.offset(
- horizontal = -(indicatorHorizontalPadding * 2).roundToPx(),
- vertical = -(indicatorVerticalPadding * 2).roundToPx()
- )
val iconPlaceable =
- measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(iconConstraints)
+ measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)
// When measuring the label, account for the indicator, the icon, and the padding between
// icon and label.
val labelPlaceable =
measurables
.fastFirst { it.layoutId == LabelLayoutIdTag }
.measure(
- iconConstraints.offset(
+ looseConstraints.offset(
horizontal =
-(iconPlaceable.width + startIconToLabelHorizontalPadding.roundToPx())
)
@@ -1093,26 +1087,6 @@
}
@Composable
-private fun StyledIcon(
- selected: Boolean,
- icon: @Composable () -> Unit,
- colors: NavigationItemColors,
- enabled: Boolean,
- badge: (@Composable () -> Unit)?,
-) {
- val iconColor = colors.iconColor(selected = selected, enabled = enabled)
- val styledIcon: @Composable () -> Unit = {
- CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
- }
-
- if (badge != null) {
- BadgedBox(badge = { badge() }) { styledIcon() }
- } else {
- styledIcon()
- }
-}
-
-@Composable
private fun StyledLabel(
selected: Boolean,
labelTextStyle: TextStyle,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
index 54aa142..cd76230 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
@@ -173,7 +173,6 @@
* @param enabled controls the enabled state of this item. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param badge optional badge to show on this item, typically a [Badge]
* @param iconPosition the [NavigationItemIconPosition] for the icon
* @param colors [NavigationItemColors] that will be used to resolve the colors used for this item
* in different states. See [ShortNavigationBarItemDefaults.colors]
@@ -191,7 +190,6 @@
label: @Composable (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- badge: (@Composable () -> Unit)? = null,
iconPosition: NavigationItemIconPosition = NavigationItemIconPosition.Top,
colors: NavigationItemColors = ShortNavigationBarItemDefaults.colors(),
interactionSource: MutableInteractionSource? = null,
@@ -229,7 +227,6 @@
modifier = modifier,
enabled = enabled,
label = label,
- badge = badge,
iconPosition = iconPosition,
interactionSource = interactionSource,
)
@@ -268,6 +265,39 @@
*/
@Composable fun colors() = MaterialTheme.colorScheme.defaultShortNavigationBarItemColors
+ /**
+ * Creates a [NavigationItemColors] with the provided colors according to the Material
+ * specification.
+ *
+ * @param selectedIconColor the color to use for the icon when the item is selected.
+ * @param selectedTextColor the color to use for the text label when the item is selected.
+ * @param selectedIndicatorColor the color to use for the indicator when the item is selected.
+ * @param unselectedIconColor the color to use for the icon when the item is unselected.
+ * @param unselectedTextColor the color to use for the text label when the item is unselected.
+ * @param disabledIconColor the color to use for the icon when the item is disabled.
+ * @param disabledTextColor the color to use for the text label when the item is disabled.
+ * @return the resulting [NavigationItemColors] used for [ShortNavigationBarItem]
+ */
+ @Composable
+ fun colors(
+ selectedIconColor: Color = NavigationBarTokens.ItemActiveIconColor.value,
+ selectedTextColor: Color = NavigationBarTokens.ItemActiveLabelTextColor.value,
+ selectedIndicatorColor: Color = NavigationBarTokens.ItemActiveIndicatorColor.value,
+ unselectedIconColor: Color = NavigationBarTokens.ItemInactiveIconColor.value,
+ unselectedTextColor: Color = NavigationBarTokens.ItemInactiveLabelTextColor.value,
+ disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha),
+ disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha),
+ ): NavigationItemColors =
+ MaterialTheme.colorScheme.defaultShortNavigationBarItemColors.copy(
+ selectedIconColor = selectedIconColor,
+ selectedTextColor = selectedTextColor,
+ selectedIndicatorColor = selectedIndicatorColor,
+ unselectedIconColor = unselectedIconColor,
+ unselectedTextColor = unselectedTextColor,
+ disabledIconColor = disabledIconColor,
+ disabledTextColor = disabledTextColor,
+ )
+
internal val ColorScheme.defaultShortNavigationBarItemColors: NavigationItemColors
get() {
return defaultShortNavigationBarItemColorsCached
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 63959a5..e2e3cae 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
/** Default size for the leading button end corners and trailing button start corners */
// TODO update token to dp size and use it here
val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
- private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
+ private val InnerCornerSizePressed = SplitButtonSmallTokens.InnerCornerPressedSize
/**
* Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index 2ad49ae..76c9a44 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -41,6 +41,7 @@
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.WideNavigationRailItemDefaults.defaultWideNavigationRailItemColors
import androidx.compose.material3.internal.DraggableAnchors
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.draggableAnchors
@@ -673,7 +674,6 @@
* @param enabled controls the enabled state of this item. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param badge optional badge to show on this item, typically a [Badge]
* @param railExpanded whether the associated [WideNavigationRail] is expanded or collapsed
* @param iconPosition the [NavigationItemIconPosition] for the icon
* @param colors [NavigationItemColors] that will be used to resolve the colors used for this item
@@ -692,7 +692,6 @@
label: @Composable (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- badge: (@Composable () -> Unit)? = null,
railExpanded: Boolean = false,
iconPosition: NavigationItemIconPosition =
WideNavigationRailItemDefaults.iconPositionFor(railExpanded),
@@ -724,7 +723,6 @@
modifier = modifier,
enabled = enabled,
label = label,
- badge = badge,
iconPosition = iconPosition,
interactionSource = interactionSource,
)
@@ -746,7 +744,6 @@
modifier = modifier,
enabled = enabled,
label = label,
- badge = badge,
iconPosition = iconPosition,
interactionSource = interactionSource,
)
@@ -865,6 +862,32 @@
*/
@Composable fun colors() = MaterialTheme.colorScheme.defaultWideWideNavigationRailColors
+ /**
+ * Creates a [WideNavigationRailColors] with the provided colors according to the Material
+ * specification.
+ *
+ * @param containerColor the color used for the background of a non-modal wide navigation rail.
+ * @param contentColor the preferred color for content inside a wide navigation rail. Defaults
+ * to either the matching content color for [containerColor], or to the current
+ * [LocalContentColor] if [containerColor] is not a color from the theme
+ * @param modalContainerColor the color used for the background of a modal wide navigation rail.
+ * @param modalScrimColor the color used for the scrim overlay for background content of a modal
+ * wide navigation rail
+ */
+ @Composable
+ fun colors(
+ containerColor: Color = WideNavigationRailDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ modalContainerColor: Color = NavigationRailExpandedTokens.ModalContainerColor.value,
+ modalScrimColor: Color = ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
+ ): WideNavigationRailColors =
+ MaterialTheme.colorScheme.defaultWideWideNavigationRailColors.copy(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ modalContainerColor = modalContainerColor,
+ modalScrimColor = modalScrimColor
+ )
+
private val containerColor: Color
@Composable get() = NavigationRailCollapsedTokens.ContainerColor.value
@@ -900,6 +923,39 @@
*/
@Composable fun colors() = MaterialTheme.colorScheme.defaultWideNavigationRailItemColors
+ /**
+ * Creates a [NavigationItemColors] with the provided colors according to the Material
+ * specification.
+ *
+ * @param selectedIconColor the color to use for the icon when the item is selected.
+ * @param selectedTextColor the color to use for the text label when the item is selected.
+ * @param selectedIndicatorColor the color to use for the indicator when the item is selected.
+ * @param unselectedIconColor the color to use for the icon when the item is unselected.
+ * @param unselectedTextColor the color to use for the text label when the item is unselected.
+ * @param disabledIconColor the color to use for the icon when the item is disabled.
+ * @param disabledTextColor the color to use for the text label when the item is disabled.
+ * @return the resulting [NavigationItemColors] used for [WideNavigationRailItem]
+ */
+ @Composable
+ fun colors(
+ selectedIconColor: Color = NavigationRailColorTokens.ItemActiveIcon.value,
+ selectedTextColor: Color = NavigationRailColorTokens.ItemActiveLabelText.value,
+ selectedIndicatorColor: Color = NavigationRailColorTokens.ItemActiveIndicator.value,
+ unselectedIconColor: Color = NavigationRailColorTokens.ItemInactiveIcon.value,
+ unselectedTextColor: Color = NavigationRailColorTokens.ItemInactiveLabelText.value,
+ disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha),
+ disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha),
+ ): NavigationItemColors =
+ MaterialTheme.colorScheme.defaultWideNavigationRailItemColors.copy(
+ selectedIconColor = selectedIconColor,
+ selectedTextColor = selectedTextColor,
+ selectedIndicatorColor = selectedIndicatorColor,
+ unselectedIconColor = unselectedIconColor,
+ unselectedTextColor = unselectedTextColor,
+ disabledIconColor = disabledIconColor,
+ disabledTextColor = disabledTextColor,
+ )
+
private val ColorScheme.defaultWideNavigationRailItemColors: NavigationItemColors
get() {
return defaultWideNavigationRailItemColorsCached
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
index 8b85857..db345ee 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_14_0
+// VERSION: 7_0_1
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -26,10 +26,14 @@
val BetweenSpace = 2.0.dp
val ContainerHeight = 40.0.dp
val ContainerShape = ShapeKeyTokens.CornerFull
+ val InnerCornerFocusedSize = ShapeDefaults.CornerMedium
+ val InnerCornerHoveredSize = ShapeDefaults.CornerMedium
+ val InnerCornerPressedSize = ShapeDefaults.CornerMedium
val InnerCornerSize = ShapeDefaults.CornerExtraSmall
val LeadingButtonLeadingSpace = 16.0.dp
val LeadingButtonTrailingSpace = 12.0.dp
val TrailingIconSize = 22.0.dp
+ val TrailingInnerCornerSelectedShapePercent = 50.0f
val TrailingButtonLeadingSpace = 13.0.dp
val TrailingButtonTrailingSpace = 13.0.dp
}
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetector.kt
index 2af4535..91cdcf9 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetector.kt
@@ -35,7 +35,7 @@
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.impl.compiled.ClsMethodImpl
import java.util.EnumSet
-import kotlinx.metadata.KmClassifier
+import kotlin.metadata.KmClassifier
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UMethod
diff --git a/compose/runtime/runtime-tracing/build.gradle b/compose/runtime/runtime-tracing/build.gradle
index d6c37d5..d436226 100644
--- a/compose/runtime/runtime-tracing/build.gradle
+++ b/compose/runtime/runtime-tracing/build.gradle
@@ -21,6 +21,8 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
+
+import androidx.build.KotlinTarget
import androidx.build.LibraryType
plugins {
@@ -47,6 +49,7 @@
androidx {
name = "Compose Runtime: Tracing"
type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
inceptionYear = "2022"
description = "Additional tracing in Compose"
legacyDisableKotlinStrictApiMode = true
diff --git a/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AssertShapeTest.kt b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AssertShapeTest.kt
index 8bcd11e..130b044 100644
--- a/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AssertShapeTest.kt
+++ b/compose/test-utils/src/androidInstrumentedTest/kotlin/androidx/compose/testutils/AssertShapeTest.kt
@@ -79,9 +79,10 @@
shape = CircleShape,
shapeColor = Color.Red,
shapeSize = Size(50f, 40f),
+ shapeCenter = Offset(60f, 40f),
backgroundShape = RectangleShape,
backgroundSize = Size(40f, 60f),
- anchor = Offset(60f, 40f),
+ backgroundCenter = Offset(60f, 40f),
antiAliasingGap = 1.21f,
)
}
@@ -98,6 +99,104 @@
)
}
+ @Test
+ fun testAssertShape_rectOnRect_partialOverlap_1() =
+ testAssertShape_partialOverlap_1(RectangleShape, RectangleShape)
+
+ @Test
+ fun testAssertShape_circleOnRect_partialOverlap_1() =
+ testAssertShape_partialOverlap_1(CircleShape, RectangleShape, 1.2f)
+
+ @Test
+ fun testAssertShape_circleOnCircle_partialOverlap_1() =
+ testAssertShape_partialOverlap_1(CircleShape, CircleShape, 1.2f)
+
+ private fun testAssertShape_partialOverlap_1(
+ shape: Shape,
+ backgroundShape: Shape,
+ antiAliasingGap: Float = 1f,
+ ) {
+ // +-----+
+ // +--| . . |--+
+ // | | | |
+ // +--| . . |--+
+ // +-----+
+ testAssertShape(
+ shape = shape,
+ shapeColor = Color.Red,
+ shapeSize = Size(20f, 30f),
+ backgroundShape = backgroundShape,
+ backgroundSize = Size(30f, 20f),
+ antiAliasingGap = antiAliasingGap,
+ )
+ }
+
+ @Test
+ fun testAssertShape_rectOnRect_partialOverlap_2() =
+ testAssertShape_partialOverlap_2(RectangleShape, RectangleShape)
+
+ @Test
+ fun testAssertShape_circleOnRect_partialOverlap_2() =
+ testAssertShape_partialOverlap_2(CircleShape, RectangleShape, 1.2f)
+
+ @Test
+ fun testAssertShape_circleOnCircle_partialOverlap_2() =
+ testAssertShape_partialOverlap_2(CircleShape, CircleShape, 1.2f)
+
+ private fun testAssertShape_partialOverlap_2(
+ shape: Shape,
+ backgroundShape: Shape,
+ antiAliasingGap: Float = 1f,
+ ) {
+ // +-----+
+ // | + .|----+
+ // | . | |
+ // | + .|----+
+ // +-----+
+ testAssertShape(
+ shape = shape,
+ shapeColor = Color.Red,
+ shapeSize = Size(20f, 30f),
+ backgroundShape = backgroundShape,
+ backgroundSize = Size(30f, 20f),
+ backgroundCenter = Offset(65f, 50f),
+ antiAliasingGap = antiAliasingGap,
+ )
+ }
+
+ @Test
+ fun testAssertShape_rectOnRect_partialOverlap_3() =
+ testAssertShape_partialOverlap_3(RectangleShape, RectangleShape)
+
+ @Test
+ fun testAssertShape_circleOnRect_partialOverlap_3() =
+ testAssertShape_partialOverlap_3(CircleShape, RectangleShape, 1.2f)
+
+ @Test
+ fun testAssertShape_circleOnCircle_partialOverlap_3() =
+ testAssertShape_partialOverlap_3(CircleShape, CircleShape, 1.2f)
+
+ private fun testAssertShape_partialOverlap_3(
+ shape: Shape,
+ backgroundShape: Shape,
+ antiAliasingGap: Float = 1f,
+ ) {
+ // +-----+
+ // | + .|---+
+ // | . | |
+ // +-----+ |
+ // +------+
+ testAssertShape(
+ shape = shape,
+ shapeColor = Color.Red,
+ shapeSize = Size(20f, 20f),
+ backgroundShape = backgroundShape,
+ backgroundSize = Size(20f, 20f),
+ backgroundCenter = Offset(60f, 60f),
+ antiAliasingGap = antiAliasingGap,
+ )
+ }
+
/**
* Tests [ImageBitmap.assertShape] by creating a bitmap reflecting the asserted values, and then
* asserting that bitmap with assertShape.
@@ -107,10 +206,11 @@
shape: Shape,
shapeColor: Color,
shapeSize: Size,
+ shapeCenter: Offset = Offset(bitmapSize.width / 2f, bitmapSize.height / 2f),
backgroundShape: Shape = RectangleShape,
backgroundColor: Color = Color.Yellow,
backgroundSize: Size = shapeSize,
- anchor: Offset = Offset(bitmapSize.width / 2f, bitmapSize.height / 2f),
+ backgroundCenter: Offset = Offset(bitmapSize.width / 2f, bitmapSize.height / 2f),
antiAliasingGap: Float = 1f,
) {
val bitmap =
@@ -119,20 +219,22 @@
shape = shape,
shapeColor = shapeColor,
shapeSize = shapeSize,
+ shapeCenter = shapeCenter,
backgroundShape = backgroundShape,
backgroundColor = backgroundColor,
backgroundSize = backgroundSize,
- anchor = anchor
+ backgroundCenter = backgroundCenter,
)
bitmap.assertShape(
density = Density(1f),
shape = shape,
shapeColor = shapeColor,
- backgroundColor = backgroundColor,
- backgroundShape = backgroundShape,
- backgroundSize = backgroundSize,
shapeSize = shapeSize,
- shapeAndBackgroundCenter = anchor,
+ shapeCenter = shapeCenter,
+ backgroundShape = backgroundShape,
+ backgroundColor = backgroundColor,
+ backgroundSize = backgroundSize,
+ backgroundCenter = backgroundCenter,
antiAliasingGap = antiAliasingGap,
)
}
@@ -160,10 +262,11 @@
shape = RectangleShape,
shapeColor = Color.Red,
shapeSize = Size(4f, 4f),
+ shapeCenter = Offset(6f, 4f),
backgroundShape = RectangleShape,
backgroundColor = Color.Yellow,
backgroundSize = Size(8f, 6f),
- anchor = Offset(6f, 4f)
+ backgroundCenter = Offset(6f, 4f),
)
bitmap.assertPixels(IntSize(10, 10)) { pos ->
// If the pixel is within the (larger) background area..
@@ -191,10 +294,11 @@
shape = RectangleShape,
shapeColor = Color.Red,
shapeSize = Size(4f, 4f),
+ shapeCenter = Offset(6f, 4f),
backgroundShape = RectangleShape,
backgroundColor = Color.Yellow,
backgroundSize = Size(8f, 6f),
- anchor = Offset(6f, 4f)
+ backgroundCenter = Offset(6f, 4f),
)
}
@@ -203,15 +307,16 @@
shape: Shape,
shapeColor: Color,
shapeSize: Size,
+ shapeCenter: Offset,
backgroundShape: Shape,
backgroundColor: Color,
backgroundSize: Size,
- anchor: Offset,
+ backgroundCenter: Offset,
): ImageBitmap {
val bitmap = ImageBitmap(bitmapSize.width, bitmapSize.height)
val canvas = Canvas(bitmap)
- canvas.drawShape(backgroundShape, backgroundColor, backgroundSize, anchor)
- canvas.drawShape(shape, shapeColor, shapeSize, anchor)
+ canvas.drawShape(backgroundShape, backgroundColor, backgroundSize, backgroundCenter)
+ canvas.drawShape(shape, shapeColor, shapeSize, shapeCenter)
return bitmap
}
@@ -221,11 +326,11 @@
shape: Shape,
color: Color,
size: Size,
- anchor: Offset,
+ center: Offset,
density: Density = Density(1f),
layoutDirection: LayoutDirection = LayoutDirection.Ltr
) {
- val backgroundOffset = anchor - (size / 2f).asOffset()
+ val backgroundOffset = center - (size / 2f).asOffset()
val backgroundOutline = shape.createOutline(size, layoutDirection, density)
CanvasDrawScope().draw(density, layoutDirection, this, size) {
translate(backgroundOffset.x, backgroundOffset.y) {
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
index 59d784a..3f35bca 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
@@ -17,6 +17,7 @@
package androidx.compose.testutils
import android.graphics.Bitmap
+import androidx.annotation.VisibleForTesting
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
@@ -36,6 +37,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -120,115 +122,6 @@
}
/**
- * Tests to see if the given point is within the path. (That is, whether the point would be in the
- * visible portion of the path if the path was used with [Canvas.clipPath].)
- *
- * The `point` argument is interpreted as an offset from the origin.
- *
- * Returns true if the point is in the path, and false otherwise.
- */
-fun Path.contains(offset: Offset): Boolean {
- val path = android.graphics.Path()
- path.addRect(
- /* left = */ offset.x - 0.01f,
- /* top = */ offset.y - 0.01f,
- /* right = */ offset.x + 0.01f,
- /* bottom = */ offset.y + 0.01f,
- /* dir = */ android.graphics.Path.Direction.CW
- )
- if (path.op(asAndroidPath(), android.graphics.Path.Op.INTERSECT)) {
- return !path.isEmpty
- }
- return false
-}
-
-/**
- * Asserts that the given [shape] is drawn in the bitmap with size [shapeSize] in the color
- * [shapeColor], in front of the [backgroundShape] that has size [backgroundSize].
- *
- * The size of a [Shape] is its rectangular bounding box. The centers of both the [shape] and the
- * [backgroundShape] are at [shapeAndBackgroundCenter]. Pixels that are outside the background area
- * are not checked. Pixels that are outside the [shape] and inside the [backgroundShape] must be
- * [backgroundColor]. Pixels that are inside the [shape] must be [shapeColor].
- *
- * To avoid the pixels on the edge of a shape, where anti-aliasing means the pixel is neither the
- * shape color nor the background color, a gap size can be given with [antiAliasingGap]. Pixels that
- * are close to the border of the shape are not checked. A larger [antiAliasingGap] means more
- * pixels are left unchecked, and a gap of 0 pixels means all pixels are tested.
- *
- * @param density current [Density] or the screen
- * @param shape the [Shape] of the foreground
- * @param shapeColor the color of the foreground shape
- * @param shapeSize the [Size] of the [shape]
- * @param shapeAndBackgroundCenter the center of both the [shape] and the [backgroundShape]
- * @param backgroundShape the [Shape] of the background
- * @param backgroundColor the color of the background shape
- * @param backgroundSize the [Size] of the [backgroundShape]
- * @param antiAliasingGap The size of the border area from the shape outline to leave it untested as
- * it is likely anti-aliased. Only works for convex shapes. The default is 1 pixel
- */
-// TODO (mount, malkov) : to investigate why it flakes when shape is not rect
-fun ImageBitmap.assertShape(
- density: Density,
- shape: Shape,
- shapeColor: Color,
- backgroundColor: Color,
- backgroundShape: Shape = RectangleShape,
- backgroundSize: Size = Size(width.toFloat(), height.toFloat()),
- shapeSize: Size = backgroundSize,
- shapeAndBackgroundCenter: Offset = Offset(width / 2f, height / 2f),
- antiAliasingGap: Float = 1.0f
-) {
- val pixels = toPixelMap()
-
- // the bounding box of the foreground shape in the bitmap
- val foregroundBounds =
- Rect(
- left = shapeAndBackgroundCenter.x - shapeSize.width / 2f,
- top = shapeAndBackgroundCenter.y - shapeSize.height / 2f,
- right = shapeAndBackgroundCenter.x + shapeSize.width / 2f,
- bottom = shapeAndBackgroundCenter.y + shapeSize.height / 2f,
- )
- // the bounding box of the background shape in the bitmap
- val backgroundBounds =
- Rect(
- left = shapeAndBackgroundCenter.x - backgroundSize.width / 2f,
- top = shapeAndBackgroundCenter.y - backgroundSize.height / 2f,
- right = shapeAndBackgroundCenter.x + backgroundSize.width / 2f,
- bottom = shapeAndBackgroundCenter.y + backgroundSize.height / 2f,
- )
-
- // Assert that the checked area is fully enclosed in the bitmap
- assert(backgroundBounds.top >= 0.0f) { "background is out of bounds (top side)" }
- assert(backgroundBounds.right <= width) { "background is out of bounds (right side)" }
- assert(backgroundBounds.bottom <= height) { "background is out of bounds (bottom side)" }
- assert(backgroundBounds.left >= 0.0f) { "background is out of bounds (left side)" }
-
- // Convert the shapes into a paths
- val foregroundPath = shape.asPath(foregroundBounds, density)
- val backgroundPath = backgroundShape.asPath(backgroundBounds, density)
-
- for (y in backgroundBounds.top until backgroundBounds.bottom) {
- for (x in backgroundBounds.left until backgroundBounds.right) {
- val point = Offset(x.toFloat(), y.toFloat())
- val pointFarther =
- pointFartherFromAnchor(point, shapeAndBackgroundCenter, antiAliasingGap)
- val pointCloser = pointCloserToAnchor(point, shapeAndBackgroundCenter, antiAliasingGap)
- if (!backgroundPath.contains(pointFarther)) {
- continue
- }
- val isInside = foregroundPath.contains(pointFarther)
- val isOutside = !foregroundPath.contains(pointCloser)
- if (isInside) {
- pixels.assertPixelColor(shapeColor, x, y)
- } else if (isOutside) {
- pixels.assertPixelColor(backgroundColor, x, y)
- }
- }
- }
-}
-
-/**
* Asserts that the given [shape] is drawn in the bitmap in the color [shapeColor] on a background
* of [backgroundColor].
*
@@ -255,22 +148,142 @@
backgroundColor: Color,
shapeColor: Color,
shape: Shape = RectangleShape,
- antiAliasingGap: Float = 1.0f
-) {
- val shapeSize =
- Size(
- width - with(density) { horizontalPadding.toPx() * 2 },
- height - with(density) { verticalPadding.toPx() * 2 }
- )
- return assertShape(
+ antiAliasingGap: Float = with(density) { 1.dp.toPx() }
+) =
+ assertShape(
density = density,
shape = shape,
shapeColor = shapeColor,
backgroundColor = backgroundColor,
backgroundShape = RectangleShape,
- shapeSize = shapeSize,
+ shapeSize =
+ Size(
+ width - with(density) { horizontalPadding.toPx() * 2 },
+ height - with(density) { verticalPadding.toPx() * 2 }
+ ),
antiAliasingGap = antiAliasingGap
)
+
+/**
+ * Asserts that the given [shape] and [backgroundShape] are drawn in the bitmap according to the
+ * given parameters.
+ *
+ * The [shape]'s bounding box should have size [shapeSize] and be centered at [shapeCenter]. The
+ * [backgroundShape]'s bounding box should have size [backgroundSize] and be centered at
+ * [backgroundCenter].
+ *
+ * Pixels that are outside the background area are not checked. Pixels that are outside the [shape]
+ * and inside the [backgroundShape] must be [backgroundColor]. Pixels that are inside the [shape]
+ * must be [shapeColor].
+ *
+ * If [backgroundColor] is `null`, only pixels inside the [shape] are checked.
+ *
+ * Because pixels on the edge of a shape are anti-aliased, pixels that are close the shape's edges
+ * are not checked. Use [antiAliasingGap] to ignore more (or less) pixels around the shape's edges.
+ * A larger [antiAliasingGap] means more pixels are left unchecked, and a gap of 0 pixels means all
+ * pixels are tested.
+ */
+// TODO (mount, malkov) : to investigate why it flakes when shape is not rect
+fun ImageBitmap.assertShape(
+ density: Density,
+ shape: Shape,
+ shapeColor: Color,
+ shapeSize: Size = Size(width.toFloat(), height.toFloat()),
+ shapeCenter: Offset = Offset(width / 2f, height / 2f),
+ backgroundShape: Shape = RectangleShape,
+ backgroundColor: Color?,
+ backgroundSize: Size = Size(width.toFloat(), height.toFloat()),
+ backgroundCenter: Offset = Offset(width / 2f, height / 2f),
+ antiAliasingGap: Float = with(density) { 1.dp.toPx() }
+) {
+ val pixels = toPixelMap()
+
+ // the bounding box of the foreground shape in the bitmap
+ val shapeBounds =
+ Rect(
+ left = shapeCenter.x - shapeSize.width / 2f,
+ top = shapeCenter.y - shapeSize.height / 2f,
+ right = shapeCenter.x + shapeSize.width / 2f,
+ bottom = shapeCenter.y + shapeSize.height / 2f,
+ )
+ // the bounding box of the background shape in the bitmap
+ val backgroundBounds =
+ Rect(
+ left = backgroundCenter.x - backgroundSize.width / 2f,
+ top = backgroundCenter.y - backgroundSize.height / 2f,
+ right = backgroundCenter.x + backgroundSize.width / 2f,
+ bottom = backgroundCenter.y + backgroundSize.height / 2f,
+ )
+
+ // Convert the shapes into a paths
+ val foregroundPath = shape.asPath(shapeBounds, density)
+ val backgroundPath = backgroundShape.asPath(backgroundBounds, density)
+
+ forEachPixelIn(shapeBounds, backgroundBounds, antiAliasingGap) { x, y, inFgBounds, inBgBounds ->
+ if (inFgBounds && !inBgBounds) {
+ // Only consider the foreground shape
+ if (foregroundPath.contains(x, y, shapeCenter, antiAliasingGap)) {
+ pixels.assertPixelColor(shapeColor, x, y)
+ }
+ } else if (inBgBounds && !inFgBounds) {
+ // Only consider the background shape, if there is one
+ if (
+ backgroundColor != null &&
+ backgroundPath.contains(x, y, backgroundCenter, antiAliasingGap)
+ ) {
+ pixels.assertPixelColor(backgroundColor, x, y)
+ }
+ } else if (inFgBounds /* && inBgBounds */) {
+ // Need to consider both the foreground and background (if there is one)
+ if (foregroundPath.contains(x, y, shapeCenter, antiAliasingGap)) {
+ pixels.assertPixelColor(shapeColor, x, y)
+ } else if (
+ backgroundColor != null &&
+ foregroundPath.notContains(x, y, shapeCenter, antiAliasingGap) &&
+ backgroundPath.contains(x, y, backgroundCenter, antiAliasingGap)
+ ) {
+ pixels.assertPixelColor(backgroundColor, x, y)
+ }
+ }
+ }
+}
+
+private fun ImageBitmap.forEachPixelIn(
+ shapeBounds: Rect,
+ backgroundBounds: Rect,
+ margin: Float,
+ block: (x: Int, y: Int, inShapeBounds: Boolean, inBackgroundBounds: Boolean) -> Unit
+) {
+ // Iterate over all pixels in the background. Usually that's all we have to do.
+ for (y in rowIndices(backgroundBounds)) {
+ for (x in columnIndices(backgroundBounds)) {
+ block.invoke(x, y, shapeBounds.contains(x, y, margin), true)
+ }
+ }
+ // While checking the background area, we potentially checked a lot of foreground area at the
+ // same time. Try to avoid checking pixels again.
+ val shapeBoundsToCheck = shapeBounds.subtract(backgroundBounds)
+ for (y in rowIndices(shapeBoundsToCheck)) {
+ for (x in columnIndices(shapeBoundsToCheck)) {
+ // Anything contained in the background is already checked
+ if (!backgroundBounds.contains(x, y, margin)) {
+ block.invoke(x, y, true, false)
+ }
+ }
+ }
+}
+
+private fun ImageBitmap.rowIndices(bounds: Rect): IntRange =
+ bounds.top.coerceAtLeast(0f) until bounds.bottom.coerceAtMost(height.toFloat())
+
+private fun ImageBitmap.columnIndices(bounds: Rect): IntRange =
+ bounds.left.coerceAtLeast(0f) until bounds.right.coerceAtMost(width.toFloat())
+
+private infix fun Float.until(until: Float): IntRange {
+ val from = this.roundToInt()
+ val to = until.roundToInt()
+ if (from <= Int.MIN_VALUE) return IntRange.EMPTY
+ return from until to
}
private fun Shape.asPath(bounds: Rect, density: Density): Path {
@@ -281,41 +294,102 @@
}
}
-private infix fun Float.until(until: Float): IntRange {
- val from = this.roundToInt()
- val to = until.roundToInt()
- if (from <= Int.MIN_VALUE) return IntRange.EMPTY
- return from until to
+private fun Path.contains(x: Int, y: Int, center: Offset, margin: Float): Boolean =
+ contains(pointFartherFromAnchor(x, y, center, margin))
+
+private fun Path.notContains(x: Int, y: Int, center: Offset, margin: Float): Boolean =
+ !contains(pointCloserToAnchor(x, y, center, margin))
+
+/**
+ * Tests to see if the given point is within the path. (That is, whether the point would be in the
+ * visible portion of the path if the path was used with [Canvas.clipPath].)
+ *
+ * The `point` argument is interpreted as an offset from the origin.
+ *
+ * Returns true if the point is in the path, and false otherwise.
+ */
+@VisibleForTesting
+internal fun Path.contains(offset: Offset): Boolean {
+ val path = android.graphics.Path()
+ path.addRect(
+ /* left = */ offset.x - 0.01f,
+ /* top = */ offset.y - 0.01f,
+ /* right = */ offset.x + 0.01f,
+ /* bottom = */ offset.y + 0.01f,
+ /* dir = */ android.graphics.Path.Direction.CW
+ )
+ if (path.op(asAndroidPath(), android.graphics.Path.Op.INTERSECT)) {
+ return !path.isEmpty
+ }
+ return false
}
-private fun pointCloserToAnchor(point: Offset, anchor: Offset, delta: Float): Offset {
- val x =
+private fun pointCloserToAnchor(x: Int, y: Int, anchor: Offset, delta: Float): Offset {
+ val xx =
when {
- point.x > anchor.x -> max(point.x - delta, anchor.x)
- point.x < anchor.x -> min(point.x + delta, anchor.x)
- else -> point.x
+ x > anchor.x -> max(x - delta, anchor.x)
+ x < anchor.x -> min(x + delta, anchor.x)
+ else -> x.toFloat()
}
- val y =
+ val yy =
when {
- point.y > anchor.y -> max(point.y - delta, anchor.y)
- point.y < anchor.y -> min(point.y + delta, anchor.y)
- else -> point.y
+ y > anchor.y -> max(y - delta, anchor.y)
+ y < anchor.y -> min(y + delta, anchor.y)
+ else -> y.toFloat()
}
- return Offset(x, y)
+ return Offset(xx, yy)
}
-private fun pointFartherFromAnchor(point: Offset, anchor: Offset, delta: Float): Offset {
- val x =
+private fun pointFartherFromAnchor(x: Int, y: Int, anchor: Offset, delta: Float): Offset {
+ val xx =
when {
- point.x > anchor.x -> point.x + delta
- point.x < anchor.x -> point.x - delta
- else -> point.x
+ x > anchor.x -> x + delta
+ x < anchor.x -> x - delta
+ else -> x.toFloat()
}
- val y =
+ val yy =
when {
- point.y > anchor.y -> point.y + delta
- point.y < anchor.y -> point.y - delta
- else -> point.y
+ y > anchor.y -> y + delta
+ y < anchor.y -> y - delta
+ else -> y.toFloat()
}
- return Offset(x, y)
+ return Offset(xx, yy)
+}
+
+private fun Rect.contains(x: Int, y: Int, margin: Float): Boolean =
+ left <= x + margin && top <= y + margin && right >= x - margin && bottom >= y - margin
+
+private fun Rect.subtract(other: Rect): Rect {
+ // Subtraction can only happen if the other rect is overlapping entirely in a dimension
+ if (other.left <= this.left && other.right >= this.right) {
+ // Other rect potentially overlaps over entire width
+ return if (other.top <= this.top && other.bottom >= this.bottom) {
+ // Subtract everything
+ Rect.Zero
+ } else if (other.top <= this.top && other.bottom > this.top) {
+ // Subtract from the top
+ Rect(this.left, other.bottom, this.right, this.bottom)
+ } else if (other.top < this.bottom && other.bottom >= this.bottom) {
+ // Subtract from the bottom
+ Rect(this.left, this.top, this.right, other.top)
+ } else {
+ // Subtract nothing
+ this
+ }
+ } else if (other.top <= this.top && other.bottom >= this.bottom) {
+ // Other rect potentially overlaps over entire height
+ return if (other.left <= this.left && other.right > this.left) {
+ // Subtract from the left
+ Rect(other.right, this.top, this.right, this.bottom)
+ } else if (other.left < this.right && other.right >= this.right) {
+ // Subtract from the right
+ Rect(this.left, this.top, other.left, this.bottom)
+ } else {
+ // Subtract nothing
+ this
+ }
+ } else {
+ // Subtract nothing
+ return this
+ }
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 074a435..82e26eb 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1313,8 +1313,8 @@
}
}
- // Experimentally verified that middle and start ellipsis don't work correctly on API 21
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
+ // Experimentally verified that start ellipsis doesn't work same way on API 22
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
@Test
fun testEllipsis_withMaxLinesOne_doesStartEllipsis() {
with(defaultDensity) {
@@ -1336,7 +1336,7 @@
}
}
- // Experimentally verified that middle and start ellipsis don't work correctly on API 21
+ // Experimentally verified that middle ellipsis doesn't work same way on API 21
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
@Test
fun testEllipsis_withMaxLinesOne_doesMiddleEllipsis() {
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index e12b98e..9359de6 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -15,6 +15,7 @@
*/
package androidx.compose.ui.text
+import android.os.Build
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
@@ -2935,6 +2936,52 @@
}
@Test
+ fun getLineStartEllipsisCount() {
+ val text = "aaaaabbbbbccccc"
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = 10.sp),
+ maxLines = 1,
+ overflow = TextOverflow.StartEllipsis,
+ width = 50f
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(1)
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineStart(0)).isEqualTo(0)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ assertThat(paragraph.getLineEnd(0)).isEqualTo(text.length)
+ } else {
+ assertThat(paragraph.getLineEnd(0)).isEqualTo(5)
+ }
+ }
+
+ @Test
+ fun getLineMiddleEllipsisCount() {
+ val text = "aaaaabbbbbccccc"
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = 10.sp),
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ width = 50f
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(1)
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineStart(0)).isEqualTo(0)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ assertThat(paragraph.getLineEnd(0)).isEqualTo(text.length)
+ } else {
+ assertThat(paragraph.getLineEnd(0)).isEqualTo(5)
+ }
+ }
+
+ @Test
fun lineHeight_inSp() {
val text = "abcdefgh"
val fontSize = 20f
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index 17a6a87..5af082b 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.text
+import android.os.Build
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
@@ -31,6 +32,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
@@ -96,6 +98,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // b/364169257 for details
fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsStartEllipsis() {
val textLayoutResult =
layoutText(
@@ -111,6 +114,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // b/364169257 for details
fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsMiddleEllipsis() {
val textLayoutResult =
layoutText(
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutLineVisibleEndTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutLineVisibleEndTest.kt
index 273ff95..0814b1a 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutLineVisibleEndTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutLineVisibleEndTest.kt
@@ -48,6 +48,47 @@
}
@Test
+ fun singleLine_withEllipsisStart() {
+ val text = "abcdefghij"
+ val textSize = 20.0f
+
+ val layout =
+ simpleLayout(
+ text = text,
+ textSize = textSize,
+ layoutWidth = textSize * 4,
+ maxLines = 1,
+ ellipsize = TextUtils.TruncateAt.START
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(10)
+ } else {
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(4)
+ }
+ }
+
+ @Test
+ fun singleLine_withEllipsisMiddle() {
+ val text = "abcdefghij"
+ val textSize = 20.0f
+
+ val layout =
+ simpleLayout(
+ text = text,
+ textSize = textSize,
+ layoutWidth = textSize * 4,
+ maxLines = 1,
+ ellipsize = TextUtils.TruncateAt.MIDDLE
+ )
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(10)
+ } else {
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(4)
+ }
+ }
+
+ @Test
fun excludesLineBreak_whenMaxLinesPresent_withoutEllipsis() {
val text = "abc\ndef"
val textSize = 20.0f
@@ -81,6 +122,40 @@
}
@Test
+ fun excludesLineBreak_whenMaxLinesPresent_withEllipsisStart() {
+ val text = "abc\ndef"
+ val textSize = 20.0f
+
+ val layout =
+ simpleLayout(
+ text = text,
+ textSize = textSize,
+ layoutWidth = textSize * 10,
+ maxLines = 1,
+ ellipsize = TextUtils.TruncateAt.START
+ )
+
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(3)
+ }
+
+ @Test
+ fun excludesLineBreak_whenMaxLinesPresent_withEllipsisMiddle() {
+ val text = "abc\ndef"
+ val textSize = 20.0f
+
+ val layout =
+ simpleLayout(
+ text = text,
+ textSize = textSize,
+ layoutWidth = textSize * 10,
+ maxLines = 1,
+ ellipsize = TextUtils.TruncateAt.MIDDLE
+ )
+
+ assertThat(layout.getLineVisibleEnd(0)).isEqualTo(3)
+ }
+
+ @Test
fun excludesWhitespace_singleLineContent_withEllipsis() {
val text = "abc def ghi"
val textSize = 20.0f
@@ -166,7 +241,7 @@
)
assertThat(layout.getLineVisibleEnd(0)).isEqualTo(0)
- assertThat(layout.getLineVisibleEnd(1)).isEqualTo(2) // ellipsis character
+ assertThat(layout.getLineVisibleEnd(1)).isEqualTo(1)
}
private fun simpleLayout(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
index 87c2aed..81d7f19 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
@@ -119,7 +119,7 @@
width: Float,
val textPaint: TextPaint,
@TextLayoutAlignment alignment: Int = DEFAULT_ALIGNMENT,
- ellipsize: TextUtils.TruncateAt? = null,
+ private val ellipsize: TextUtils.TruncateAt? = null,
@TextDirection textDirectionHeuristic: Int = DEFAULT_TEXT_DIRECTION,
lineSpacingMultiplier: Float = DEFAULT_LINESPACING_MULTIPLIER,
@Px lineSpacingExtra: Float = DEFAULT_LINESPACING_EXTRA,
@@ -457,13 +457,13 @@
* returns the length of the text.
*/
fun getLineEnd(lineIndex: Int): Int =
- if (layout.getEllipsisStart(lineIndex) == 0) { // no ellipsis
- layout.getLineEnd(lineIndex)
- } else {
+ if (layout.isLineEllipsized(lineIndex) && ellipsize == TextUtils.TruncateAt.END) {
// Layout#getLineEnd usually gets the end of text for the last line even if ellipsis
// happens. However, if LF character is included in the ellipsized region, getLineEnd
// returns LF character offset. So, use end of text for line end here.
layout.text.length
+ } else {
+ layout.getLineEnd(lineIndex)
}
/**
@@ -471,10 +471,10 @@
* whitespaces are not counted as visible characters.
*/
fun getLineVisibleEnd(lineIndex: Int): Int =
- if (layout.getEllipsisStart(lineIndex) == 0) { // no ellipsis
- layoutHelper.getLineVisibleEnd(lineIndex)
- } else {
+ if (layout.isLineEllipsized(lineIndex) && ellipsize == TextUtils.TruncateAt.END) {
layout.getLineStart(lineIndex) + layout.getEllipsisStart(lineIndex)
+ } else {
+ layoutHelper.getLineVisibleEnd(lineIndex)
}
fun isLineEllipsized(lineIndex: Int) = layout.isLineEllipsized(lineIndex)
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index 57823b5..e18cbf3 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -54,6 +54,7 @@
method public static inline <T, R> java.util.List<R> fastMapNotNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R?> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
+ method public static inline <T, R extends java.lang.Comparable<? super R>> R fastMaxOfOrDefault(java.util.List<? extends T>, R defaultValue, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <T, R extends java.lang.Comparable<? super R>> R? fastMaxOfOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMinByOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <S, T extends S> S fastReduce(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super S,? super T,? extends S> operation);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 57823b5..be90ec4 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -54,12 +54,15 @@
method public static inline <T, R> java.util.List<R> fastMapNotNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R?> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
+ method public static inline <T, R extends java.lang.Comparable<? super R>> R fastMaxOfOrDefault(java.util.List<? extends T>, R defaultValue, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <T, R extends java.lang.Comparable<? super R>> R? fastMaxOfOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMinByOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
method public static inline <S, T extends S> S fastReduce(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super S,? super T,? extends S> operation);
method public static inline <T> int fastSumBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Integer> selector);
method public static inline <T, R, V> java.util.List<V> fastZip(java.util.List<? extends T>, java.util.List<? extends R> other, kotlin.jvm.functions.Function2<? super T,? super R,? extends V> transform);
method public static inline <T, R> java.util.List<R> fastZipWithNext(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> transform);
+ method @kotlin.PublishedApi internal static Void throwNoSuchElementException(String message);
+ method @kotlin.PublishedApi internal static void throwUnsupportedOperationException(String message);
}
public final class MathHelpersKt {
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index 9ea3f32..2ce5e6f 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -42,6 +42,7 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
+ implementation("androidx.collection:collection:1.4.3")
}
}
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index 80d197e..889ba5c 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.util
+import androidx.collection.MutableScatterSet
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@@ -307,6 +308,30 @@
}
/**
+ * Returns the largest value among all values produced by selector function applied to each element
+ * in the collection or [defaultValue] if there are no elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+inline fun <T, R : Comparable<R>> List<T>.fastMaxOfOrDefault(
+ defaultValue: R,
+ selector: (T) -> R
+): R {
+ contract { callsInPlace(selector) }
+ if (isEmpty()) return defaultValue
+ var maxValue = selector(get(0))
+ for (i in 1..lastIndex) {
+ val v = selector(get(i))
+ if (v > maxValue) maxValue = v
+ }
+ return maxValue
+}
+
+/**
* Returns a list containing the results of applying the given [transform] function to each pair of
* two adjacent elements in this collection.
*
@@ -320,7 +345,7 @@
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastZipWithNext(transform: (T, T) -> R): List<R> {
contract { callsInPlace(transform) }
- if (size == 0 || size == 1) return emptyList()
+ if (size <= 1) return emptyList()
val result = mutableListOf<R>()
var current = get(0)
// `until` as we don't want to invoke this for the last element, since that won't have a `next`
@@ -351,7 +376,7 @@
@OptIn(ExperimentalContracts::class)
inline fun <S, T : S> List<T>.fastReduce(operation: (acc: S, T) -> S): S {
contract { callsInPlace(operation) }
- if (isEmpty()) throw UnsupportedOperationException("Empty collection can't be reduced.")
+ if (isEmpty()) throwUnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: S = first()
for (i in 1..lastIndex) {
accumulator = operation(accumulator, get(i))
@@ -435,7 +460,7 @@
@OptIn(ExperimentalContracts::class)
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
contract { callsInPlace(selector) }
- val set = HashSet<K>(size)
+ val set = MutableScatterSet<K>(size)
val target = ArrayList<T>(size)
fastForEach { e ->
val key = selector(e)
@@ -517,7 +542,7 @@
inline fun <T> List<T>.fastFirst(predicate: (T) -> Boolean): T {
contract { callsInPlace(predicate) }
fastForEach { if (predicate(it)) return it }
- throw NoSuchElementException("Collection contains no element matching the predicate.")
+ throwNoSuchElementException("Collection contains no element matching the predicate.")
}
/**
@@ -564,3 +589,13 @@
else -> append(element.toString())
}
}
+
+@PublishedApi
+internal fun throwNoSuchElementException(message: String): Nothing {
+ throw NoSuchElementException(message)
+}
+
+@PublishedApi
+internal fun throwUnsupportedOperationException(message: String) {
+ throw UnsupportedOperationException(message)
+}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 8ed523d..799e1ad 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2514,15 +2514,18 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScaleFactor {
+ ctor public ScaleFactor(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float scaleX, optional float scaleY);
method @androidx.compose.runtime.Stable public operator long div(float operand);
- method public float getScaleX();
- method public float getScaleY();
+ method public long getPackedValue();
+ method public inline float getScaleX();
+ method public inline float getScaleY();
method @androidx.compose.runtime.Stable public operator long times(float operand);
- property @androidx.compose.runtime.Stable public final float scaleX;
- property @androidx.compose.runtime.Stable public final float scaleY;
+ property public final long packedValue;
+ property @androidx.compose.runtime.Stable public final inline float scaleX;
+ property @androidx.compose.runtime.Stable public final inline float scaleY;
field public static final androidx.compose.ui.layout.ScaleFactor.Companion Companion;
}
@@ -2532,7 +2535,7 @@
}
public final class ScaleFactorKt {
- method @androidx.compose.runtime.Stable public static long ScaleFactor(float scaleX, float scaleY);
+ method @androidx.compose.runtime.Stable public static inline long ScaleFactor(float scaleX, float scaleY);
method @androidx.compose.runtime.Stable public static operator long div(long, long scaleFactor);
method public static inline boolean isSpecified(long);
method public static inline boolean isUnspecified(long);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 59af42d..e675339 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2521,15 +2521,18 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScaleFactor {
+ ctor public ScaleFactor(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float scaleX, optional float scaleY);
method @androidx.compose.runtime.Stable public operator long div(float operand);
- method public float getScaleX();
- method public float getScaleY();
+ method public long getPackedValue();
+ method public inline float getScaleX();
+ method public inline float getScaleY();
method @androidx.compose.runtime.Stable public operator long times(float operand);
- property @androidx.compose.runtime.Stable public final float scaleX;
- property @androidx.compose.runtime.Stable public final float scaleY;
+ property public final long packedValue;
+ property @androidx.compose.runtime.Stable public final inline float scaleX;
+ property @androidx.compose.runtime.Stable public final inline float scaleY;
field public static final androidx.compose.ui.layout.ScaleFactor.Companion Companion;
}
@@ -2539,7 +2542,7 @@
}
public final class ScaleFactorKt {
- method @androidx.compose.runtime.Stable public static long ScaleFactor(float scaleX, float scaleY);
+ method @androidx.compose.runtime.Stable public static inline long ScaleFactor(float scaleX, float scaleY);
method @androidx.compose.runtime.Stable public static operator long div(long, long scaleFactor);
method public static inline boolean isSpecified(long);
method public static inline boolean isUnspecified(long);
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
index bfeca00..b208f50 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
@@ -1548,6 +1549,52 @@
assertThat(postFlingCount).isGreaterThan(0)
}
}
+
+ @Test
+ fun modifierIsRemoved_shouldKeepInfoAboutPreviousParent() {
+ val innerDispatcher = NestedScrollDispatcher()
+ val outerDispatcher = NestedScrollDispatcher()
+ var keepAround by mutableStateOf(true)
+
+ rule.setContent {
+ Column(
+ modifier =
+ Modifier.nestedScroll(
+ dispatcher = outerDispatcher,
+ connection = object : NestedScrollConnection {}
+ )
+ ) {
+ Box(
+ Modifier.size(400.dp)
+ .then(
+ if (keepAround)
+ Modifier.nestedScroll(
+ dispatcher = innerDispatcher,
+ connection = object : NestedScrollConnection {}
+ )
+ else Modifier
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(innerDispatcher.lastKnownValidParentNode).isNull()
+ assertThat(innerDispatcher.nestedScrollNode?.parentNestedScrollNode)
+ .isEqualTo(outerDispatcher.nestedScrollNode)
+ }
+
+ rule.runOnIdle {
+ keepAround = false // remove inner node
+ }
+
+ rule.runOnIdle {
+ // the inner node's parent is the outer node
+ assertThat(innerDispatcher.lastKnownValidParentNode)
+ .isEqualTo(outerDispatcher.nestedScrollNode)
+ assertThat(innerDispatcher.nestedScrollNode?.parentNestedScrollNode).isNull()
+ }
+ }
}
private fun Offset.customEquals(other: Offset): Boolean {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 38f3d5c..7f5532a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -76,6 +76,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -3105,6 +3106,34 @@
}
@Test
+ fun testApproachUsingContentNotComposedInLookahead() {
+ var items by mutableStateOf(List(500) { it })
+ rule.setContent {
+ LookaheadScope {
+ LazyColumn(Modifier.requiredHeight(400.dp)) {
+ items(items, key = { it }) {
+ Box(
+ Modifier.animateItem(
+ fadeInSpec = null,
+ placementSpec = tween(15),
+ fadeOutSpec = tween(15)
+ )
+ ) {
+ Box { Text("Item $it") }
+ }
+ }
+ }
+ }
+ }
+
+ repeat(30) {
+ rule.waitForIdle()
+ items = items.shuffled()
+ Snapshot.sendApplyNotifications()
+ }
+ }
+
+ @Test
fun testLookaheadAndApproachCoordinatesAreConsistentOnFirstPass_usingAlign() =
with(rule.density) {
val rootSizePx = 300
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
index 28fc17c..fd37d97 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
@@ -18,17 +18,32 @@
import android.view.View
import android.view.inputmethod.BaseInputConnection
+import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.ExtractedText
import android.view.inputmethod.InputConnection
+import android.widget.EditText
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusEventModifierNode
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.input.pointer.MatrixPositionCalculator
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.PlatformTextInputModifierNode
import androidx.compose.ui.platform.establishTextInputSession
+import androidx.compose.ui.platform.platformTextInputServiceInterceptor
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
@@ -623,6 +638,79 @@
}
}
+ @Suppress("DEPRECATION")
+ @Test
+ fun focusSwitchToAndroidViewNonEditor_hidesKeyboard() {
+ lateinit var button: android.widget.Button
+ val inputMethodManager = TestInputMethodManager()
+ platformTextInputServiceInterceptor = { original ->
+ TextInputServiceAndroid(
+ view = (original as TextInputServiceAndroid).view,
+ rootPositionCalculator = FakeMatrixPositionCalculator,
+ inputMethodManager = inputMethodManager
+ )
+ }
+ rule.setContent {
+ Column {
+ Box(TestElement { node1 = it }.focusable().testTag("tag"))
+ AndroidView(
+ factory = { context ->
+ android.widget.Button(context).also {
+ it.isFocusableInTouchMode = true
+ button = it
+ }
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag("tag").requestFocus()
+ rule.waitForIdle()
+
+ inputMethodManager.reset()
+
+ // fake TextField is active, a session is started. Now let's request focus on Button
+ rule.runOnUiThread { button.requestFocus() }
+
+ rule.runOnIdle {
+ assertThat(inputMethodManager.restartCalls).isEqualTo(1)
+ assertThat(inputMethodManager.hideKeyboardCalls).isEqualTo(1)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun focusSwitchToAndroidViewEditor_doesNotHideKeyboard() {
+ lateinit var editText: EditText
+ val inputMethodManager = TestInputMethodManager()
+ platformTextInputServiceInterceptor = { original ->
+ TextInputServiceAndroid(
+ view = (original as TextInputServiceAndroid).view,
+ rootPositionCalculator = FakeMatrixPositionCalculator,
+ inputMethodManager = inputMethodManager
+ )
+ }
+ rule.setContent {
+ Column {
+ Box(TestElement { node1 = it }.focusable().testTag("tag"))
+ AndroidView(factory = { EditText(it).also { editText = it } })
+ }
+ }
+
+ rule.onNodeWithTag("tag").requestFocus()
+
+ rule.waitForIdle()
+ inputMethodManager.reset()
+
+ // fake TextField is active, a session is started. Now let's request focus on EditText
+ rule.runOnUiThread { editText.requestFocus() }
+
+ rule.runOnIdle {
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideKeyboardCalls).isEqualTo(0)
+ }
+ }
+
private fun setupContent() {
rule.setContent {
hostView = LocalView.current as AndroidComposeView
@@ -643,10 +731,89 @@
}
private class TestNode(var onNode: (PlatformTextInputModifierNode) -> Unit) :
- Modifier.Node(), PlatformTextInputModifierNode {
+ Modifier.Node(), PlatformTextInputModifierNode, FocusEventModifierNode {
+
+ private var inputSessionJob: Job? = null
+
+ private fun startInputSession() {
+ inputSessionJob =
+ coroutineScope.launch {
+ establishTextInputSession {
+ launch {
+ startInputMethod(
+ object : TestInputMethodRequest(view) {
+ override fun createInputConnection(
+ outAttributes: EditorInfo
+ ): InputConnection = BaseInputConnection(view, true)
+ }
+ )
+ }
+ awaitCancellation()
+ }
+ }
+ }
+
+ private fun disposeInputSession() {
+ inputSessionJob?.cancel()
+ inputSessionJob = null
+ }
override fun onAttach() {
onNode(this)
}
+
+ override fun onFocusEvent(focusState: FocusState) {
+ if (focusState.isFocused) {
+ startInputSession()
+ } else {
+ disposeInputSession()
+ }
+ }
}
}
+
+private object FakeMatrixPositionCalculator : MatrixPositionCalculator {
+ override fun localToScreen(localTransform: Matrix) = Unit
+
+ override fun localToScreen(localPosition: Offset) = localPosition
+
+ override fun screenToLocal(positionOnScreen: Offset) = positionOnScreen
+}
+
+@Suppress("DEPRECATION")
+private class TestInputMethodManager : InputMethodManager {
+ var restartCalls = 0
+ var showKeyboardCalls = 0
+ var hideKeyboardCalls = 0
+
+ fun reset() {
+ restartCalls = 0
+ showKeyboardCalls = 0
+ hideKeyboardCalls = 0
+ }
+
+ override fun isActive(): Boolean = true
+
+ override fun restartInput() {
+ restartCalls++
+ }
+
+ override fun showSoftInput() {
+ showKeyboardCalls++
+ }
+
+ override fun hideSoftInput() {
+ hideKeyboardCalls++
+ }
+
+ override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+ override fun updateSelection(
+ selectionStart: Int,
+ selectionEnd: Int,
+ compositionStart: Int,
+ compositionEnd: Int
+ ) {}
+
+ override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index edb02b4..14c0c52 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -270,6 +270,19 @@
}
private fun processInputCommands() {
+ // If the associated view is not focused anymore, we should check whether the focus has
+ // transitioned into another Editor.
+ if (!view.isFocused) {
+ val focusedView = view.rootView.findFocus()
+ // If a view is focused and is an editor, we can skip the queued up commands since the
+ // new editor is going to manage the keyboard and the input session. Otherwise we should
+ // process the queue since it probably contains StopInput or HideKeyboard calls to
+ // clean up after us.
+ if (focusedView?.onCheckIsTextEditor() == true) {
+ textInputCommandQueue.clear()
+ return
+ }
+ }
// Multiple commands may have been queued up in the channel while this function was
// waiting to be resumed. We don't execute the commands as they come in because making a
// bunch of calls to change the actual IME quickly can result in flickers. Instead, we
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 0e07822..6c10ef5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -60,7 +60,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastMaxBy
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
@@ -74,6 +73,7 @@
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID
+import kotlin.math.max
import kotlin.math.roundToInt
/**
@@ -562,9 +562,19 @@
@Composable
private fun DialogLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
- val placeables = measurables.fastMap { it.measure(constraints) }
- val width = placeables.fastMaxBy { it.width }?.width ?: constraints.minWidth
- val height = placeables.fastMaxBy { it.height }?.height ?: constraints.minHeight
- layout(width, height) { placeables.fastForEach { it.placeRelative(0, 0) } }
+ var maxWidth = 0
+ var maxHeight = 0
+ val placeables =
+ measurables.fastMap {
+ it.measure(constraints).apply {
+ maxWidth = max(maxWidth, width)
+ maxHeight = max(maxHeight, height)
+ }
+ }
+ if (measurables.isEmpty()) {
+ maxWidth = constraints.minWidth
+ maxHeight = constraints.minHeight
+ }
+ layout(maxWidth, maxHeight) { placeables.fastForEach { it.placeRelative(0, 0) } }
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 35431d0..cc18d62 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -85,6 +85,7 @@
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID
+import kotlin.math.max
import kotlinx.coroutines.isActive
import org.jetbrains.annotations.TestOnly
@@ -436,14 +437,15 @@
layout(p.width, p.height) { p.placeRelative(0, 0) }
}
else -> {
- val placeables = measurables.fastMap { it.measure(constraints) }
var width = 0
var height = 0
- for (i in 0..placeables.lastIndex) {
- val p = placeables[i]
- width = maxOf(width, p.width)
- height = maxOf(height, p.height)
- }
+ val placeables =
+ measurables.fastMap {
+ it.measure(constraints).apply {
+ width = max(width, this.width)
+ height = max(height, this.height)
+ }
+ }
layout(width, height) {
for (i in 0..placeables.lastIndex) {
val p = placeables[i]
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
index f27a52f..4e7c151 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
@@ -40,6 +40,7 @@
class TextInputServiceAndroidCommandDebouncingTest {
private val view = mock<View>()
+ private val parentView = mock<View>()
private val inputMethodManager = TestInputMethodManager()
private val executor = Executor { runnable -> scope.launch { runnable.run() } }
private val service =
@@ -54,8 +55,11 @@
@Before
fun setUp() {
- // Default the view to focused because when it's not focused commands should be ignored.
+ // Default the view to focused. When it is not focused the commands should be processed
+ // according to whether the new focused View is an Editor.
whenever(view.isFocused).thenReturn(true)
+ whenever(view.rootView).thenReturn(parentView)
+ whenever(parentView.findFocus()).thenReturn(null)
}
@After
@@ -249,15 +253,29 @@
}
@Test
- fun commandsAreDrained_whenProcessedWithoutFocus() {
+ fun commandsAreNotDrained_whenProcessedWithoutFocus_and_focusDidNotSwitchToAnEditor() {
whenever(view.isFocused).thenReturn(false)
+ val newFocusedView = mock<View>()
+ whenever(newFocusedView.onCheckIsTextEditor()).thenReturn(false)
+ whenever(parentView.findFocus()).thenReturn(newFocusedView)
service.showSoftwareKeyboard()
- service.hideSoftwareKeyboard()
- scope.advanceUntilIdle()
- whenever(view.isFocused).thenReturn(true)
scope.advanceUntilIdle()
- assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+ assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
+ }
+
+ @Test
+ fun commandsAreDrained_whenProcessedWithoutFocus_and_focusSwitchedToAnEditor() {
+ whenever(view.isFocused).thenReturn(false)
+ val newFocusedView = mock<View>()
+ whenever(newFocusedView.onCheckIsTextEditor()).thenReturn(true)
+ whenever(parentView.findFocus()).thenReturn(newFocusedView)
+ service.stopInput()
+ service.hideSoftwareKeyboard()
+ scope.advanceUntilIdle()
+
+ assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+ assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
}
@Test
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
index b8d1e64..5e82355 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
@@ -106,6 +106,9 @@
internal var nestedScrollNode: NestedScrollNode? = null
+ // caches last known parent for fling clean up use.
+ internal var lastKnownValidParentNode: NestedScrollNode? = null
+
// lambda to calculate the most outer nested scroll scope for this dispatcher on demand
internal var calculateNestedScrollScope: () -> CoroutineScope? = { scope }
@@ -142,7 +145,9 @@
/**
* Parent to be set when attached to nested scrolling chain. `null` is valid and means there no
- * nested scrolling parent above
+ * nested scrolling parent above. The last known attached parent might be used in case this
+ * dispatcher is not attached to any node, that is [nestedScrollNode?.parentNestedScrollNode] is
+ * null.
*/
internal val parent: NestedScrollConnection?
get() = nestedScrollNode?.parentNestedScrollNode
@@ -204,7 +209,17 @@
* @return velocity that has been consumed by all the ancestors
*/
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
- return parent?.onPostFling(consumed, available) ?: Velocity.Zero
+ // lastKnownValidParentNode can be used to send clean up signals.
+ // If this dispatcher's regular parent is not present it means either it never attached or
+ // it was detached. If it was detached we have information about its last known parent so
+ // we use it to send the post fling signal. We don't need to do the same for the other
+ // methods because the problem with parity in this API comes from a node that detaches
+ // during a fling.
+ return if (parent == null) {
+ lastKnownValidParentNode?.onPostFling(consumed, available) ?: Velocity.Zero
+ } else {
+ parent?.onPostFling(consumed, available) ?: Velocity.Zero
+ }
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt
index 432daec..680033b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt
@@ -21,6 +21,7 @@
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.findNearestAncestor
+import androidx.compose.ui.node.traverseAncestors
import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.CoroutineScope
@@ -49,6 +50,8 @@
resolvedDispatcher = dispatcher ?: NestedScrollDispatcher() // Resolve null dispatcher
}
+ internal var lastKnownValidParentNode: NestedScrollNode? = null
+
internal val parentNestedScrollNode: NestedScrollNode?
get() = if (isAttached) findNearestAncestor() else null
@@ -94,11 +97,12 @@
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-
val selfConsumed = connection.onPostFling(consumed, available)
+ // if we receive an onPostFling after detaching this node, use the last known parent
+ // if this parent is also detached it will send the signal through the detached parents
+ val parent = if (isAttached) parentConnection else lastKnownValidParentNode
val parentConsumed =
- parentConnection?.onPostFling(consumed + selfConsumed, available - selfConsumed)
- ?: Velocity.Zero
+ parent?.onPostFling(consumed + selfConsumed, available - selfConsumed) ?: Velocity.Zero
return selfConsumed + parentConsumed
}
@@ -128,6 +132,9 @@
}
override fun onDetach() {
+ // cache parent for detached clean up access in the dispatcher and in this node.
+ lastKnownValidParentNode = findNearestAttachedAncestor()
+ resolvedDispatcher.lastKnownValidParentNode = lastKnownValidParentNode
resetDispatcherFields()
}
@@ -137,6 +144,9 @@
*/
private fun updateDispatcherFields() {
resolvedDispatcher.nestedScrollNode = this
+ // reset lastKnownValidParentNodes
+ resolvedDispatcher.lastKnownValidParentNode = null
+ lastKnownValidParentNode = null
resolvedDispatcher.calculateNestedScrollScope = { nestedCoroutineScope }
resolvedDispatcher.scope = coroutineScope
}
@@ -155,3 +165,16 @@
updateDispatcher(dispatcher)
}
}
+
+private fun <T : TraversableNode> T.findNearestAttachedAncestor(): T? {
+ var node: T? = null
+ traverseAncestors {
+ if (it.node.isAttached) {
+ node = it
+ false
+ } else {
+ true
+ }
+ }
+ return node
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index e3537fd..33359fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.input.pointer
import androidx.compose.runtime.Immutable
@@ -1016,7 +1018,8 @@
val y = position.y
val width = size.width
val height = size.height
- return x < 0f || x > width || y < 0f || y > height
+ // Branch-less
+ return (x < 0f) or (x > width) or (y < 0f) or (y > height)
}
/**
@@ -1027,15 +1030,24 @@
* the pointer region.
*/
fun PointerInputChange.isOutOfBounds(size: IntSize, extendedTouchPadding: Size): Boolean {
- if (type != PointerType.Touch) {
- @Suppress("DEPRECATION") return isOutOfBounds(size)
- }
+ // Set to 1 when the pointer type is touch, 0 otherwise
+ // No-op at the CPU level
+ val isTouch = (type == PointerType.Touch).toInt()
+
val position = position
val x = position.x
val y = position.y
- val minX = -extendedTouchPadding.width
- val maxX = size.width + extendedTouchPadding.width
- val minY = -extendedTouchPadding.height
- val maxY = size.height + extendedTouchPadding.height
- return x < minX || x > maxX || y < minY || y > maxY
+
+ // Set extentX to 0 when the pointer type is *not* touch
+ val extentX = extendedTouchPadding.width * isTouch
+ val maxX = size.width + extentX
+
+ // Set extentY to 0 when the pointer type is *not* touch
+ val extentY = extendedTouchPadding.height * isTouch
+ val maxY = size.height + extentY
+
+ // Don't branch
+ return (x < -extentX) or (x > maxX) or (y < -extentY) or (y > maxY)
}
+
+private inline fun Boolean.toInt() = if (this) 1 else 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 49aa673..2ba31ca 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.input.pointer
import androidx.collection.LongSparseArray
@@ -211,7 +213,7 @@
if (it.down) {
previousPointerInputData.put(
it.id.value,
- PointerInputData(it.uptime, it.positionOnScreen, it.down, it.type)
+ PointerInputData(it.uptime, it.positionOnScreen, it.down)
)
} else {
previousPointerInputData.remove(it.id.value)
@@ -229,20 +231,18 @@
private class PointerInputData(
val uptime: Long,
val positionOnScreen: Offset,
- val down: Boolean,
- val type: PointerType
+ val down: Boolean
)
}
/** The result of a call to [PointerInputEventProcessor.process]. */
-// TODO(shepshpard): Not sure if storing these values in a int is most efficient overall.
@kotlin.jvm.JvmInline
-internal value class ProcessResult(private val value: Int) {
+internal value class ProcessResult(val value: Int) {
val dispatchedToAPointerInputModifier
- get() = (value and 1) != 0
+ inline get() = (value and 0x1) != 0
val anyMovementConsumed
- get() = (value and (1 shl 1)) != 0
+ inline get() = (value and 0x2) != 0
}
/**
@@ -256,7 +256,9 @@
dispatchedToAPointerInputModifier: Boolean,
anyMovementConsumed: Boolean
): ProcessResult {
- val val1 = if (dispatchedToAPointerInputModifier) 1 else 0
- val val2 = if (anyMovementConsumed) (1 shl 1) else 0
- return ProcessResult(val1 or val2)
+ return ProcessResult(
+ dispatchedToAPointerInputModifier.toInt() or (anyMovementConsumed.toInt() shl 1)
+ )
}
+
+private inline fun Boolean.toInt() = if (this) 1 else 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
index d664f85..d96d294 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/PointerIdArray.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.input.pointer.util
import androidx.compose.ui.input.pointer.PointerId
@@ -25,7 +27,6 @@
* (since LongArray is not itself resizable).
*/
internal class PointerIdArray {
-
/**
* The size of this [PointerIdArray], which is equal to the number of ids stored in the array.
*/
@@ -34,6 +35,10 @@
var size = 0
private set
+ /** Returns index of last item in array */
+ inline val lastIndex: Int
+ get() = size - 1
+
/**
* The ids are stored as Long values in a LongArray. LongArray is not resizable, and we may need
* to expand this array if there are many pointer ids in use at any given time, so we keep the
@@ -58,7 +63,7 @@
*
* @return true if [pointerId] was in the array, false otherwise
*/
- fun remove(pointerId: PointerId): Boolean {
+ inline fun remove(pointerId: PointerId): Boolean {
return remove(pointerId.value)
}
@@ -70,8 +75,11 @@
*/
fun remove(pointerIdValue: Long): Boolean {
for (i in 0 until size) {
- if (pointerIdValue == this[i].value) {
- removeAt(i)
+ if (pointerIdValue == internalArray[i]) {
+ for (j in i until size - 1) {
+ internalArray[j] = internalArray[j + 1]
+ }
+ size--
return true
}
}
@@ -116,7 +124,7 @@
*
* @return true if id was added, false otherwise
*/
- fun add(pointerId: PointerId): Boolean {
+ inline fun add(pointerId: PointerId): Boolean {
return add(pointerId.value)
}
@@ -127,20 +135,27 @@
* it.
*/
operator fun set(index: Int, value: Long) {
+ var internalArray = internalArray
if (index >= internalArray.size) {
// Increase the size of the backing array
- internalArray = internalArray.copyOf(maxOf(index + 1, internalArray.size * 2))
+ internalArray = resizeStorage(index + 1)
}
internalArray[index] = value
if (index >= size) size = index + 1
}
+ private fun resizeStorage(minSize: Int): LongArray {
+ return internalArray.copyOf(maxOf(minSize, internalArray.size * 2)).apply {
+ internalArray = this
+ }
+ }
+
/**
* Sets the value at the given index to [pointerId]. The index must be less than or equal to the
* current size of the array. If it is equal to the size of the array, the storage in the array
* will be expanded to ensure that the item can be added to the end of it.
*/
- operator fun set(index: Int, pointerId: PointerId) {
+ inline operator fun set(index: Int, pointerId: PointerId) {
set(index, pointerId.value)
}
@@ -151,7 +166,7 @@
}
/** Returns true if [pointerId] is in the array, false otherwise */
- fun contains(pointerId: PointerId): Boolean {
+ inline fun contains(pointerId: PointerId): Boolean {
return contains(pointerId.value)
}
@@ -162,8 +177,4 @@
}
return false
}
-
- /** Returns index of last item in array */
- inline val lastIndex: Int
- get() = size - 1
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
index cae8d987..18b188e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.internal
import kotlin.contracts.ExperimentalContracts
@@ -34,6 +36,18 @@
throw IllegalArgumentException(message)
}
+internal fun throwIllegalArgumentExceptionForNullCheck(message: String): Nothing {
+ throw IllegalArgumentException(message)
+}
+
+internal fun throwUnsupportedOperationException(message: String) {
+ throw UnsupportedOperationException(message)
+}
+
+internal fun throwIndexOutOfBoundsException(message: String) {
+ throw IndexOutOfBoundsException(message)
+}
+
// Like Kotlin's check() but without the .toString() call and
// a non-inline throw
@Suppress("BanInlineOptIn")
@@ -45,7 +59,7 @@
}
}
-@Suppress("NOTHING_TO_INLINE", "BanInlineOptIn")
+@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
internal inline fun checkPrecondition(value: Boolean) {
contract { returns() implies value }
@@ -69,7 +83,7 @@
}
// Like Kotlin's checkNotNull() but with a non-inline throw
-@Suppress("NOTHING_TO_INLINE", "BanInlineOptIn")
+@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
internal inline fun <T : Any> checkPreconditionNotNull(value: T?): T {
contract { returns() implies (value != null) }
@@ -90,3 +104,17 @@
throwIllegalArgumentException(lazyMessage())
}
}
+
+// Like Kotlin's requireNotNull() but without the .toString() call and
+// a non-inline throw
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T : Any> requirePreconditionNotNull(value: T?, lazyMessage: () -> String): T {
+ contract { returns() implies (value != null) }
+
+ if (value == null) {
+ throwIllegalArgumentExceptionForNullCheck(lazyMessage())
+ }
+
+ return value
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index d64d81b..eff14bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.layout
+import androidx.compose.ui.internal.requirePreconditionNotNull
+import androidx.compose.ui.internal.throwIllegalArgumentExceptionForNullCheck
import androidx.compose.ui.node.LayoutModifierNodeCoordinator
import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.node.checkMeasuredSize
@@ -54,7 +56,7 @@
) : ApproachMeasureScope, MeasureScope by coordinator, LookaheadScope {
override val lookaheadConstraints: Constraints
get() =
- requireNotNull(coordinator.lookaheadConstraints) {
+ requirePreconditionNotNull(coordinator.lookaheadConstraints) {
"Error: Lookahead constraints requested before lookahead measure."
}
@@ -68,13 +70,13 @@
if (this is NodeCoordinator) {
return lookaheadDelegate?.lookaheadLayoutCoordinates ?: this
}
- throw IllegalArgumentException("Unsupported LayoutCoordinates: $this")
+ throwIllegalArgumentExceptionForNullCheck("Unsupported LayoutCoordinates")
}
override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
get() {
val lookaheadRoot = coordinator.layoutNode.lookaheadRoot
- requireNotNull(lookaheadRoot) {
+ requirePreconditionNotNull(lookaheadRoot) {
"Error: Requesting LookaheadScopeCoordinates is not permitted from outside of a" +
" LookaheadScope."
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
index d6ce074..bb72e66 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.layout
import androidx.compose.runtime.Immutable
@@ -149,6 +151,8 @@
return min(widthScale, heightScale)
}
-private fun computeFillWidth(srcSize: Size, dstSize: Size): Float = dstSize.width / srcSize.width
+private inline fun computeFillWidth(srcSize: Size, dstSize: Size): Float =
+ dstSize.width / srcSize.width
-private fun computeFillHeight(srcSize: Size, dstSize: Size): Float = dstSize.height / srcSize.height
+private inline fun computeFillHeight(srcSize: Size, dstSize: Size): Float =
+ dstSize.height / srcSize.height
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index 1bdba48..7291168 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastForEach
import kotlin.jvm.JvmName
@@ -355,8 +356,8 @@
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
- val w = width.coerceAtLeast(0)
- val h = height.coerceAtLeast(0)
+ val w = width.fastCoerceAtLeast(0)
+ val h = height.fastCoerceAtLeast(0)
checkMeasuredSize(w, h)
return object : MeasureResult {
override val width: Int
@@ -389,8 +390,8 @@
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
- val w = width.coerceAtLeast(0)
- val h = height.coerceAtLeast(0)
+ val w = width.fastCoerceAtLeast(0)
+ val h = height.fastCoerceAtLeast(0)
checkMeasuredSize(w, h)
return object : MeasureResult {
override val width: Int
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 0fec80b..bee65dd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.internal.throwUnsupportedOperationException
import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastCoerceIn
@@ -155,7 +156,7 @@
*/
@Suppress("DocumentExceptions")
fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
- throw UnsupportedOperationException(
+ throwUnsupportedOperationException(
"transformFrom is not implemented on this LayoutCoordinates"
)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasurePolicy.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasurePolicy.kt
index 5752aa8b..c2df497 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasurePolicy.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasurePolicy.kt
@@ -29,11 +29,11 @@
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
- return when {
- measurables.isEmpty() -> {
+ return when (measurables.size) {
+ 0 -> {
layout(constraints.minWidth, constraints.minHeight) {}
}
- measurables.size == 1 -> {
+ 1 -> {
val placeable = measurables[0].measure(constraints)
layout(
constraints.constrainWidth(placeable.width),
@@ -43,13 +43,15 @@
}
}
else -> {
- val placeables = measurables.fastMap { it.measure(constraints) }
var maxWidth = 0
var maxHeight = 0
- placeables.fastForEach { placeable ->
- maxWidth = maxOf(placeable.width, maxWidth)
- maxHeight = maxOf(placeable.height, maxHeight)
- }
+ val placeables =
+ measurables.fastMap {
+ it.measure(constraints).apply {
+ maxWidth = maxOf(width, maxWidth)
+ maxHeight = maxOf(height, maxHeight)
+ }
+ }
layout(
constraints.constrainWidth(maxWidth),
constraints.constrainHeight(maxHeight)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt
index 60704c6..d02222d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt
@@ -14,52 +14,40 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.layout
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.isSpecified
-import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2
/** Constructs a [ScaleFactor] from the given x and y scale values */
-@Stable fun ScaleFactor(scaleX: Float, scaleY: Float) = ScaleFactor(packFloats(scaleX, scaleY))
+@Stable
+inline fun ScaleFactor(scaleX: Float, scaleY: Float) = ScaleFactor(packFloats(scaleX, scaleY))
/** Holds 2 dimensional scaling factors for horizontal and vertical axes */
@Immutable
@kotlin.jvm.JvmInline
-value class ScaleFactor internal constructor(@PublishedApi internal val packedValue: Long) {
+value class ScaleFactor(val packedValue: Long) {
/** Returns the scale factor to apply along the horizontal axis */
@Stable
- val scaleX: Float
- get() {
- // Explicitly compare against packed values to avoid
- // auto-boxing of ScaleFactor.Unspecified
- checkPrecondition(this.packedValue != Unspecified.packedValue) {
- "ScaleFactor is unspecified"
- }
- return unpackFloat1(packedValue)
- }
+ inline val scaleX: Float
+ get() = unpackFloat1(packedValue)
/** Returns the scale factor to apply along the vertical axis */
@Stable
- val scaleY: Float
- get() {
- // Explicitly compare against packed values to avoid
- // auto-boxing of Size.Unspecified
- checkPrecondition(this.packedValue != Unspecified.packedValue) {
- "ScaleFactor is unspecified"
- }
- return unpackFloat2(packedValue)
- }
+ inline val scaleY: Float
+ get() = unpackFloat2(packedValue)
- @Suppress("NOTHING_TO_INLINE") @Stable inline operator fun component1(): Float = scaleX
+ @Stable inline operator fun component1(): Float = scaleX
- @Suppress("NOTHING_TO_INLINE") @Stable inline operator fun component2(): Float = scaleY
+ @Stable inline operator fun component2(): Float = scaleY
/**
* Returns a copy of this ScaleFactor instance optionally overriding the scaleX or scaleY
@@ -84,7 +72,6 @@
override fun toString() = "ScaleFactor(${scaleX}, ${scaleY})"
companion object {
-
/**
* A ScaleFactor whose [scaleX] and [scaleY] parameters are unspecified. This is a sentinel
* value used to initialize a non-null parameter. Access to scaleX or scaleY on an
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 08a5f0c..2edc023 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -18,6 +18,7 @@
import androidx.collection.MutableOrderedScatterSet
import androidx.collection.mutableOrderedScatterSetOf
+import androidx.collection.mutableScatterMapOf
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNodeLifecycleCallback
@@ -36,6 +37,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.internal.checkPrecondition
+import androidx.compose.ui.internal.requirePrecondition
+import androidx.compose.ui.internal.throwIllegalStateExceptionForNullCheck
+import androidx.compose.ui.internal.throwIndexOutOfBoundsException
import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.materialize
import androidx.compose.ui.node.ComposeUiNode.Companion.SetCompositeKeyHash
@@ -405,17 +409,19 @@
private var currentIndex = 0
private var currentPostLookaheadIndex = 0
- private val nodeToNodeState = hashMapOf<LayoutNode, NodeState>()
+ private val nodeToNodeState = mutableScatterMapOf<LayoutNode, NodeState>()
// this map contains active slotIds (without precomposed or reusable nodes)
- private val slotIdToNode = hashMapOf<Any?, LayoutNode>()
+ private val slotIdToNode = mutableScatterMapOf<Any?, LayoutNode>()
private val scope = Scope()
private val postLookaheadMeasureScope = PostLookaheadMeasureScopeImpl()
- private val precomposeMap = hashMapOf<Any?, LayoutNode>()
+ private val precomposeMap = mutableScatterMapOf<Any?, LayoutNode>()
private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
+
// SlotHandles precomposed in the post-lookahead pass.
- private val postLookaheadPrecomposeSlotHandleMap = mutableMapOf<Any?, PrecomposedSlotHandle>()
+ private val postLookaheadPrecomposeSlotHandleMap =
+ mutableScatterMapOf<Any?, PrecomposedSlotHandle>()
// Slot ids _composed_ in post-lookahead. The valid slot ids are stored between 0 and
// currentPostLookaheadIndex - 1, beyond index currentPostLookaheadIndex are obsolete ids.
private val postLookaheadComposedSlotIds = mutableVectorOf<Any?>()
@@ -471,7 +477,7 @@
if (root.foldedChildren.getOrNull(currentIndex) !== node) {
// the node has a new index in the list
val itemIndex = root.foldedChildren.indexOf(node)
- require(itemIndex >= currentIndex) {
+ requirePrecondition(itemIndex >= currentIndex) {
"Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
"sure you provide a unique key for each item."
}
@@ -509,7 +515,10 @@
existing = nodeState.composition,
container = node,
parent =
- compositionContext ?: error("parent composition reference not set"),
+ compositionContext
+ ?: throwIllegalStateExceptionForNullCheck(
+ "parent composition reference not set"
+ ),
reuseContent = nodeState.forceReuse,
composable = { ReusableContentHost(nodeState.active, content) }
)
@@ -539,20 +548,21 @@
}
}
- private fun getSlotIdAtIndex(index: Int): Any? {
- val node = root.foldedChildren[index]
+ private fun getSlotIdAtIndex(foldedChildren: List<LayoutNode>, index: Int): Any? {
+ val node = foldedChildren[index]
return nodeToNodeState[node]!!.slotId
}
fun disposeOrReuseStartingFromIndex(startIndex: Int) {
reusableCount = 0
- val lastReusableIndex = root.foldedChildren.size - precomposedCount - 1
+ val foldedChildren = root.foldedChildren
+ val lastReusableIndex = foldedChildren.size - precomposedCount - 1
var needApplyNotification = false
if (startIndex <= lastReusableIndex) {
// construct the set of available slot ids
reusableSlotIdsSet.clear()
for (i in startIndex..lastReusableIndex) {
- val slotId = getSlotIdAtIndex(i)
+ val slotId = getSlotIdAtIndex(foldedChildren, i)
reusableSlotIdsSet.add(slotId)
}
@@ -561,7 +571,7 @@
var i = lastReusableIndex
Snapshot.withoutReadObservation {
while (i >= startIndex) {
- val node = root.foldedChildren[i]
+ val node = foldedChildren[i]
val nodeState = nodeToNodeState[node]!!
val slotId = nodeState.slotId
if (slotId in reusableSlotIdsSet) {
@@ -596,12 +606,13 @@
precomposedCount = 0
precomposeMap.clear()
- val childCount = root.foldedChildren.size
+ val foldedChildren = root.foldedChildren
+ val childCount = foldedChildren.size
if (reusableCount != childCount) {
reusableCount = childCount
Snapshot.withoutReadObservation {
for (i in 0 until childCount) {
- val node = root.foldedChildren[i]
+ val node = foldedChildren[i]
val nodeState = nodeToNodeState[node]
if (nodeState != null && nodeState.active) {
node.resetLayoutState()
@@ -624,7 +635,7 @@
private fun disposeCurrentNodes() {
root.ignoreRemeasureRequests {
- nodeToNodeState.values.forEach { it.composition?.dispose() }
+ nodeToNodeState.forEachValue { it.composition?.dispose() }
root.removeAll()
}
@@ -639,17 +650,17 @@
fun makeSureStateIsConsistent() {
val childrenCount = root.foldedChildren.size
- require(nodeToNodeState.size == childrenCount) {
+ requirePrecondition(nodeToNodeState.size == childrenCount) {
"Inconsistency between the count of nodes tracked by the state " +
"(${nodeToNodeState.size}) and the children count on the SubcomposeLayout" +
" ($childrenCount). Are you trying to use the state of the" +
" disposed SubcomposeLayout?"
}
- require(childrenCount - reusableCount - precomposedCount >= 0) {
+ requirePrecondition(childrenCount - reusableCount - precomposedCount >= 0) {
"Incorrect state. Total children $childrenCount. Reusable children " +
"$reusableCount. Precomposed children $precomposedCount"
}
- require(precomposeMap.size == precomposedCount) {
+ requirePrecondition(precomposeMap.size == precomposedCount) {
"Incorrect state. Precomposed children $precomposedCount. Map size " +
"${precomposeMap.size}"
}
@@ -664,13 +675,14 @@
if (reusableCount == 0) {
return null
}
- val reusableNodesSectionEnd = root.foldedChildren.size - precomposedCount
+ val foldedChildren = root.foldedChildren
+ val reusableNodesSectionEnd = foldedChildren.size - precomposedCount
val reusableNodesSectionStart = reusableNodesSectionEnd - reusableCount
var index = reusableNodesSectionEnd - 1
var chosenIndex = -1
// first try to find a node with exactly the same slotId
while (index >= reusableNodesSectionStart) {
- if (getSlotIdAtIndex(index) == slotId) {
+ if (getSlotIdAtIndex(foldedChildren, index) == slotId) {
// we have a node with the same slotId
chosenIndex = index
break
@@ -682,7 +694,7 @@
// try to find a first compatible slotId from the end of the section
index = reusableNodesSectionEnd - 1
while (index >= reusableNodesSectionStart) {
- val node = root.foldedChildren[index]
+ val node = foldedChildren[index]
val nodeState = nodeToNodeState[node]!!
if (
nodeState.slotId === ReusedSlotId ||
@@ -704,7 +716,7 @@
move(index, reusableNodesSectionStart, 1)
}
reusableCount--
- val node = root.foldedChildren[reusableNodesSectionStart]
+ val node = foldedChildren[reusableNodesSectionStart]
val nodeState = nodeToNodeState[node]!!
// create a new instance to avoid change notifications
nodeState.activeState = mutableStateOf(true)
@@ -726,6 +738,7 @@
scope.density = density
scope.fontScale = fontScale
if (!isLookingAhead && root.lookaheadRoot != null) {
+ // Approach pass
currentPostLookaheadIndex = 0
val result = postLookaheadMeasureScope.block(constraints)
val indexAfterMeasure = currentPostLookaheadIndex
@@ -736,6 +749,7 @@
disposeUnusedSlotsInPostLookahead()
}
} else {
+ // Lookahead pass, or the main pass if not in a lookahead scope.
currentIndex = 0
val result = scope.block(constraints)
val indexAfterMeasure = currentIndex
@@ -750,7 +764,7 @@
}
private fun disposeUnusedSlotsInPostLookahead() {
- postLookaheadPrecomposeSlotHandleMap.entries.removeAll { (slotId, handle) ->
+ postLookaheadPrecomposeSlotHandleMap.removeIf { slotId, handle ->
val id = postLookaheadComposedSlotIds.indexOf(slotId)
if (id < 0 || id >= currentPostLookaheadIndex) {
// Slot was not used in the latest pass of post-lookahead.
@@ -813,9 +827,9 @@
makeSureStateIsConsistent()
val node = precomposeMap.remove(slotId)
if (node != null) {
- check(precomposedCount > 0) { "No pre-composed items to dispose" }
+ checkPrecondition(precomposedCount > 0) { "No pre-composed items to dispose" }
val itemIndex = root.foldedChildren.indexOf(node)
- check(itemIndex >= root.foldedChildren.size - precomposedCount) {
+ checkPrecondition(itemIndex >= root.foldedChildren.size - precomposedCount) {
"Item is not in pre-composed item range"
}
// move this item into the reusable section
@@ -835,11 +849,13 @@
if (node != null && node.isAttached) {
val size = node.children.size
if (index < 0 || index >= size) {
- throw IndexOutOfBoundsException(
+ throwIndexOutOfBoundsException(
"Index ($index) is out of bound of [0, $size)"
)
}
- require(!node.isPlaced) { "Pre-measure called on node that is not placed" }
+ requirePrecondition(!node.isPlaced) {
+ "Pre-measure called on node that is not placed"
+ }
root.ignoreRemeasureRequests {
node.requireOwner().measureAndLayout(node.children[index], constraints)
}
@@ -860,7 +876,7 @@
if (reusableCount != childCount) {
// only invalidate children if there are any non-reused ones
// in other cases, all of them are going to be invalidated later anyways
- nodeToNodeState.forEach { (_, nodeState) -> nodeState.forceRecompose = true }
+ nodeToNodeState.forEachValue { nodeState -> nodeState.forceRecompose = true }
if (!root.measurePending) {
root.requestRemeasure()
@@ -951,11 +967,14 @@
* pass, [subcompose] will return an [emptyList].
*/
override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
- val measurables = slotIdToNode[slotId]?.childMeasurables
- if (measurables != null) {
- return measurables
+ val nodeInSlot = slotIdToNode[slotId]
+ if (nodeInSlot != null && root.foldedChildren.indexOf(nodeInSlot) < currentIndex) {
+ // Check that the node has been composed in lookahead. Otherwise, we need to
+ // compose the node in approach pass via postLookaheadSubcompose.
+ return nodeInSlot.childMeasurables
+ } else {
+ return postLookaheadSubcompose(slotId, content)
}
- return postLookaheadSubcompose(slotId, content)
}
}
@@ -963,7 +982,7 @@
slotId: Any?,
content: @Composable () -> Unit
): List<Measurable> {
- require(postLookaheadComposedSlotIds.size >= currentPostLookaheadIndex) {
+ requirePrecondition(postLookaheadComposedSlotIds.size >= currentPostLookaheadIndex) {
"Error: currentPostLookaheadIndex cannot be greater than the size of the" +
"postLookaheadComposedSlotIds list."
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
index 0e8e58f..e32ceea 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.node
+import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
@@ -81,7 +82,9 @@
parent.measurePending ||
parent.layoutPending ||
parentLayoutState == LayoutNode.LayoutState.Measuring ||
- parentLayoutState == LayoutNode.LayoutState.LayingOut
+ parentLayoutState == LayoutNode.LayoutState.LayingOut ||
+ postponedMeasureRequests.fastAny { it.node == this } ||
+ layoutState == LayoutNode.LayoutState.Measuring
}
}
if (isPlacedInLookahead == true) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index 8bf8d59..708fec5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -19,8 +19,10 @@
import androidx.collection.MutableObjectFloatMap
import androidx.collection.MutableScatterMap
import androidx.collection.MutableScatterSet
+import androidx.collection.mutableObjectIntMapOf
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.internal.checkPrecondition
+import androidx.compose.ui.internal.throwIllegalStateExceptionForNullCheck
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadLayoutCoordinates
@@ -358,7 +360,7 @@
override val measureResult: MeasureResult
get() =
_measureResult
- ?: error(
+ ?: throwIllegalStateExceptionForNullCheck(
"LookaheadDelegate has not been measured yet when measureResult is requested."
)
@@ -417,10 +419,10 @@
field = result
}
- protected val cachedAlignmentLinesMap = mutableMapOf<AlignmentLine, Int>()
+ protected val cachedAlignmentLinesMap = mutableObjectIntMapOf<AlignmentLine>()
internal fun getCachedAlignmentLine(alignmentLine: AlignmentLine): Int =
- cachedAlignmentLinesMap[alignmentLine] ?: AlignmentLine.Unspecified
+ cachedAlignmentLinesMap.getOrDefault(alignmentLine, AlignmentLine.Unspecified)
override fun replace() {
placeAt(position, 0f, null)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
index fc0a078..acc7620 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MyersDiff.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("NOTHING_TO_INLINE")
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.ui.node
@@ -180,7 +180,7 @@
): Boolean {
val oldSize = oldEnd - oldStart
val newSize = newEnd - newStart
- val checkForSnake = abs(oldSize - newSize) % 2 == 1
+ val checkForSnake = (abs(oldSize - newSize) and 1) == 1
val delta = oldSize - newSize
var k = -d
while (k <= d) {
@@ -200,7 +200,7 @@
x = startX + 1
}
var y: Int = newStart + (x - oldStart) - k
- startY = if (d == 0 || x != startX) y else y - 1
+ startY = y - ((d != 0) and (x == startX)).toInt()
// now find snake size
while ((x < oldEnd) && y < newEnd && cb.areItemsTheSame(x, y)) {
x++
@@ -244,7 +244,7 @@
): Boolean {
val oldSize = oldEnd - oldStart
val newSize = newEnd - newStart
- val checkForSnake = (oldSize - newSize) % 2 == 0
+ val checkForSnake = ((oldSize - newSize) and 1) == 0
val delta = oldSize - newSize
// same as androidx.compose.ui.node.forward but we go backwards from end of the lists to be
// beginning this also means we'll try to optimize for minimizing x instead of maximizing it
@@ -267,7 +267,7 @@
x = startX - 1
}
var y = newEnd - (oldEnd - x - k)
- val startY = if (d == 0 || x != startX) y else y + 1
+ val startY = y + ((d != 0) and (x == startX)).toInt()
// now find snake size
while ((x > oldStart) && y > newStart && cb.areItemsTheSame(x - 1, y - 1)) {
x--
@@ -306,26 +306,26 @@
@JvmInline
private value class Snake(val data: IntArray) {
/** Position in the old list */
- val startX: Int
+ inline val startX: Int
get() = data[0]
/** Position in the new list */
- val startY: Int
+ inline val startY: Int
get() = data[1]
/** End position in the old list, exclusive */
- val endX: Int
+ inline val endX: Int
get() = data[2]
/** End position in the new list, exclusive */
- val endY: Int
+ inline val endY: Int
get() = data[3]
/** True if this snake was created in the reverse search, false otherwise. */
- val reverse: Boolean
+ inline val reverse: Boolean
get() = data[4] != 0
- val diagonalSize: Int
+ inline val diagonalSize: Int
get() = min(endX - startX, endY - startY)
private val hasAdditionOrRemoval: Boolean
@@ -339,27 +339,41 @@
* where we try to produce a path and also find moves.
*/
fun addDiagonalToStack(diagonals: IntStack) {
+ // if (hasAdditionOrRemoval) {
+ // if (reverse) {
+ // x = startX
+ // y = startY
+ // } else {
+ // if (isAddition) {
+ // x = startX
+ // y = startY + 1
+ // } else {
+ // x = startX + 1
+ // y = startY
+ // }
+ // }
+ // } else {
+ // x = startX
+ // y = startY
+ // }
+ val size: Int
+ var x = startX
+ var y = startY
if (hasAdditionOrRemoval) {
- if (reverse) {
- // snake edge it at the end
- diagonals.pushDiagonal(startX, startY, diagonalSize)
- } else {
- // snake edge it at the beginning
- if (isAddition) {
- diagonals.pushDiagonal(startX, startY + 1, diagonalSize)
- } else {
- diagonals.pushDiagonal(startX + 1, startY, diagonalSize)
- }
- }
+ size = diagonalSize
+ x += (!(reverse or isAddition)).toInt()
+ y += (!(reverse or !isAddition)).toInt()
} else {
- // we are a pure diagonal
- diagonals.pushDiagonal(startX, startY, endX - startX)
+ size = endX - startX
}
+ diagonals.pushDiagonal(x, y, size)
}
override fun toString() = "Snake($startX,$startY,$endX,$endY,$reverse)"
}
+private inline fun Boolean.toInt() = if (this) 1 else 0
+
internal fun fillSnake(
startX: Int,
startY: Int,
@@ -403,6 +417,12 @@
val size: Int
get() = lastIndex
+ private fun resizeStack(stack: IntArray): IntArray {
+ val copy = stack.copyOf(stack.size * 2)
+ this.stack = copy
+ return copy
+ }
+
fun pushRange(
oldStart: Int,
oldEnd: Int,
@@ -410,10 +430,10 @@
newEnd: Int,
) {
val i = lastIndex
+ var stack = stack
if (i + 4 >= stack.size) {
- stack = stack.copyOf(stack.size * 2)
+ stack = resizeStack(stack)
}
- val stack = stack
stack[i + 0] = oldStart
stack[i + 1] = oldEnd
stack[i + 2] = newStart
@@ -427,10 +447,10 @@
size: Int,
) {
val i = lastIndex
+ var stack = stack
if (i + 3 >= stack.size) {
- stack = stack.copyOf(stack.size * 2)
+ stack = resizeStack(stack)
}
- val stack = stack
stack[i + 0] = x + size
stack[i + 1] = y + size
stack[i + 2] = size
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java
deleted file mode 100644
index e43da90..0000000
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- *******************************************************************************
- * Copyright (C) 2008-2010, International Business Machines Corporation and *
- * others. All Rights Reserved. *
- *******************************************************************************
- */
-package androidx.core.i18n.messageformat_icu.impl;
-
-import androidx.annotation.RestrictTo;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.AccessControlException;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.util.MissingResourceException;
-import java.util.Properties;
-
-/**
- * ICUConfig is a class used for accessing ICU4J runtime configuration.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class ICUConfig {
- public static final String CONFIG_PROPS_FILE = "/com/ibm/icu/ICUConfig.properties";
- private static final Properties CONFIG_PROPS;
-
- static {
- CONFIG_PROPS = new Properties();
- try {
- InputStream is = ICUData.getStream(CONFIG_PROPS_FILE);
- if (is != null) {
- CONFIG_PROPS.load(is);
- }
- } catch (MissingResourceException mre) {
- // If it does not exist, ignore.
- } catch (IOException ioe) {
- // Any IO errors, ignore
- }
- }
-
- /**
- * Get ICU configuration property value for the given name.
- * @param name The configuration property name
- * @return The configuration property value, or null if it does not exist.
- */
- public static String get(String name) {
- return get(name, null);
- }
-
- /**
- * Get ICU configuration property value for the given name.
- * @param name The configuration property name
- * @param def The default value
- * @return The configuration property value. If the property does not
- * exist, <code>def</code> is returned.
- */
- public static String get(String name, String def) {
- String val = null;
- final String fname = name;
- if (System.getSecurityManager() != null) {
- try {
- val = AccessController.doPrivileged(new PrivilegedAction<String>() {
- @Override
- public String run() {
- return System.getProperty(fname);
- }
- });
- } catch (AccessControlException e) {
- // ignore
- // TODO log this message
- }
- } else {
- val = System.getProperty(name);
- }
-
- if (val == null) {
- val = CONFIG_PROPS.getProperty(name, def);
- }
- return val;
- }
-}
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java
deleted file mode 100644
index a20acca..0000000
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- *******************************************************************************
- * Copyright (C) 2004-2009, International Business Machines Corporation and *
- * others. All Rights Reserved. *
- *******************************************************************************
- *
- * Created on Feb 4, 2004
- *
- */
-package androidx.core.i18n.messageformat_icu.impl;
-
-import androidx.annotation.RestrictTo;
-
-import java.io.InputStream;
-import java.net.URL;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.util.MissingResourceException;
-
-/**
- * Provides access to ICU data files as InputStreams. Implements security checking.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public final class ICUData {
- /*
- * Return a URL to the ICU resource names resourceName. The
- * resource name should either be an absolute path, or a path relative to
- * com.ibm.icu.impl (e.g., most likely it is 'data/foo'). If required
- * is true, throw an MissingResourceException instead of returning a null result.
- */
- public static boolean exists(final String resourceName) {
- URL i = null;
- if (System.getSecurityManager() != null) {
- i = AccessController.doPrivileged(new PrivilegedAction<URL>() {
- @Override
- public URL run() {
- return ICUData.class.getResource(resourceName);
- }
- });
- } else {
- i = ICUData.class.getResource(resourceName);
- }
- return i != null;
- }
-
- private static InputStream getStream(final Class<?> root, final String resourceName, boolean required) {
- InputStream i = null;
-
- if (System.getSecurityManager() != null) {
- i = AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
- @Override
- public InputStream run() {
- return root.getResourceAsStream(resourceName);
- }
- });
- } else {
- i = root.getResourceAsStream(resourceName);
- }
-
- if (i == null && required) {
- throw new MissingResourceException("could not locate data " +resourceName, root.getPackage().getName(), resourceName);
- }
- return i;
- }
-
- private static InputStream getStream(final ClassLoader loader, final String resourceName, boolean required) {
- InputStream i = null;
- if (System.getSecurityManager() != null) {
- i = AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
- @Override
- public InputStream run() {
- return loader.getResourceAsStream(resourceName);
- }
- });
- } else {
- i = loader.getResourceAsStream(resourceName);
- }
- if (i == null && required) {
- throw new MissingResourceException("could not locate data", loader.toString(), resourceName);
- }
- return i;
- }
-
- public static InputStream getStream(ClassLoader loader, String resourceName){
- return getStream(loader,resourceName, false);
- }
-
- public static InputStream getRequiredStream(ClassLoader loader, String resourceName){
- return getStream(loader, resourceName, true);
- }
-
- /*
- * Convenience override that calls getStream(ICUData.class, resourceName, false);
- */
- public static InputStream getStream(String resourceName) {
- return getStream(ICUData.class, resourceName, false);
- }
-
- /*
- * Convenience method that calls getStream(ICUData.class, resourceName, true).
- */
- public static InputStream getRequiredStream(String resourceName) {
- return getStream(ICUData.class, resourceName, true);
- }
-
- /*
- * Convenience override that calls getStream(root, resourceName, false);
- */
- public static InputStream getStream(Class<?> root, String resourceName) {
- return getStream(root, resourceName, false);
- }
-
- /*
- * Convenience method that calls getStream(root, resourceName, true).
- */
- public static InputStream getRequiredStream(Class<?> root, String resourceName) {
- return getStream(root, resourceName, true);
- }
-}
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
index b0b190d7..d494d46 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
@@ -17,8 +17,6 @@
import java.util.ArrayList;
import java.util.Locale;
-import androidx.core.i18n.messageformat_icu.impl.ICUConfig;
-
//Note: Minimize ICU dependencies, only use a very small part of the ICU core.
//In particular, do not depend on *Format classes.
@@ -82,8 +80,7 @@
public final class MessagePattern implements Cloneable, Freezable<MessagePattern> {
/**
* Mode for when an apostrophe starts quoted literal text for MessageFormat output.
- * The default is DOUBLE_OPTIONAL unless overridden via ICUConfig
- * (/com/ibm/icu/ICUConfig.properties).
+ * The default is {@link ApostropheMode#DOUBLE_OPTIONAL}.
* <p>
* A pair of adjacent apostrophes always results in a single apostrophe in the output,
* even when the pair is between two single, text-quoting apostrophes.
@@ -893,7 +890,7 @@
/**
* Freezes this object, making it immutable and thread-safe.
- * @return this
+ * @return this
* icu_annot::stable ICU 4.8
*/
@Override
@@ -941,7 +938,7 @@
char c=msg.charAt(index++);
if(c=='\'') {
if(index==msg.length()) {
- // The apostrophe is the last character in the pattern.
+ // The apostrophe is the last character in the pattern.
// Add a Part for auto-quoting.
addPart(Part.Type.INSERT_CHAR, index, 0, '\''); // value=char to be inserted
needsAutoQuoting=true;
@@ -1612,8 +1609,7 @@
private boolean frozen;
private static final ApostropheMode defaultAposMode=
- ApostropheMode.valueOf(
- ICUConfig.get("com.ibm.icu.text.MessagePattern.ApostropheMode", "DOUBLE_OPTIONAL"));
+ ApostropheMode.DOUBLE_OPTIONAL;
private static final ArgType[] argTypes=ArgType.values();
}
diff --git a/core/core-telecom/api/current.txt b/core/core-telecom/api/current.txt
index 40a8a93..8468a41 100644
--- a/core/core-telecom/api/current.txt
+++ b/core/core-telecom/api/current.txt
@@ -137,7 +137,7 @@
}
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
- method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalCallSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
}
diff --git a/core/core-telecom/api/restricted_current.txt b/core/core-telecom/api/restricted_current.txt
index 40a8a93..8468a41 100644
--- a/core/core-telecom/api/restricted_current.txt
+++ b/core/core-telecom/api/restricted_current.txt
@@ -137,7 +137,7 @@
}
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
- method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalCallSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
index e97f365..a3353b7 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
@@ -84,7 +84,7 @@
}
Extensions.LOCAL_CALL_SILENCE -> {
localCallSilenceUpdater =
- addLocalSilenceExtension(false) {
+ addLocalCallSilenceExtension(false) {
Log.i(TAG, "addLocalSilenceExtension: callId=[$callId], it=[$it]")
callback?.setLocalCallSilenceState(callId, it)
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
index e21ad2d..4ff6f6a 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
@@ -117,7 +117,7 @@
* @return The interface used by this application to further update the local call silence
* extension state to remote surfaces
*/
- public fun addLocalSilenceExtension(
+ public fun addLocalCallSilenceExtension(
initialCallSilenceState: Boolean,
onLocalSilenceUpdate: (suspend (Boolean) -> Unit),
): LocalCallSilenceExtension
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
index 20b900f..7d923d4 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
@@ -64,7 +64,7 @@
return participant
}
- override fun addLocalSilenceExtension(
+ override fun addLocalCallSilenceExtension(
initialCallSilenceState: Boolean,
onLocalSilenceUpdate: (suspend (Boolean) -> Unit)
): LocalCallSilenceExtension {
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt
index 96e8b75..4b43ad3 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt
@@ -28,7 +28,7 @@
* not transmitting audio input data to remote users. This allows applications to do stuff like
* nudge the user when they are silenced but talking into the microphone.
*
- * @see ExtensionInitializationScope.addLocalSilenceExtension
+ * @see ExtensionInitializationScope.addLocalCallSilenceExtension
*/
@ExperimentalAppActions
public interface LocalCallSilenceExtension {
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index ce89b90..2683d54 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -31,7 +31,7 @@
dependencies {
api("androidx.annotation:annotation:1.8.1")
- api("androidx.biometric:biometric-ktx:1.4.0-alpha02")
+ api("androidx.biometric:biometric:1.1.0")
api(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
implementation("androidx.core:core:1.15.0-alpha01")
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
index ef61508..b9a7f1b 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
@@ -104,7 +104,7 @@
}
@Test
- public void testGetCredentialAsyc_successCallbackThrows() throws InterruptedException {
+ public void testGetCredentialAsync_successCallbackThrows() throws InterruptedException {
if (Looper.myLooper() == null) {
Looper.prepare();
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
index 0a2d095..2c89de4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
@@ -150,7 +150,8 @@
@Test
public void fromBundle_validAllowedAuthenticatorAboveApi35_success() {
- long expectedOpId = TEST_CRYPTO_OBJECT.getOperationHandle();
+ long expectedOpId = BiometricTestUtils.INSTANCE
+ .getTestCryptoObjectOpId$credentials_releaseAndroidTest(TEST_CRYPTO_OBJECT);
Bundle inputBundle = new Bundle();
inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
inputBundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
@@ -160,9 +161,8 @@
assertThat(actualBiometricPromptData).isNotNull();
assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
TEST_ALLOWED_AUTHENTICATOR);
- assertThat(actualBiometricPromptData.getCryptoObject()).isNotNull();
- assertThat(actualBiometricPromptData.getCryptoObject().getOperationHandle())
- .isEqualTo(TEST_CRYPTO_OBJECT.getOperationHandle());
+ assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
+ // TODO(b/368395001) : Add CryptoObject test back when library dependency updates
}
@Test
@@ -211,7 +211,8 @@
public void toBundle_api35AndAboveWithOpId_success() {
BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,
TEST_ALLOWED_AUTHENTICATOR);
- long expectedOpId = TEST_CRYPTO_OBJECT.getOperationHandle();
+ long expectedOpId = BiometricTestUtils.INSTANCE
+ .getTestCryptoObjectOpId$credentials_releaseAndroidTest(TEST_CRYPTO_OBJECT);
Bundle actualBundle = BiometricPromptData.toBundle(
testBiometricPromptData);
@@ -231,5 +232,4 @@
() -> new BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
);
}
-
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
index bdc1ea0..2f1bfc4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
@@ -18,6 +18,7 @@
import android.os.Bundle
import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt.CryptoObject
import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID
import androidx.credentials.provider.utils.BiometricTestUtils
@@ -143,7 +144,7 @@
@Test
fun fromBundle_validAllowedAuthenticatorAboveApi35_success() {
- val expectedOpId = TEST_CRYPTO_OBJECT.operationHandle
+ val expectedOpId = getTestCryptoObjectOpId()
val inputBundle = Bundle()
inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
inputBundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
@@ -153,11 +154,13 @@
assertThat(actualBiometricPromptData).isNotNull()
assertThat(actualBiometricPromptData!!.allowedAuthenticators)
.isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
- assertThat(actualBiometricPromptData.cryptoObject).isNotNull()
- assertThat(actualBiometricPromptData.cryptoObject!!.operationHandle)
- .isEqualTo(TEST_CRYPTO_OBJECT.operationHandle)
+ assertThat(actualBiometricPromptData.cryptoObject).isNull()
+ // TODO(b/368395001) : Add CryptoObject test back when library dependency updates
}
+ private fun getTestCryptoObjectOpId(cryptoObject: CryptoObject = TEST_CRYPTO_OBJECT) =
+ BiometricTestUtils.getTestCryptoObjectOpId(cryptoObject)
+
@Test
fun fromBundle_unrecognizedAllowedAuthenticator_success() {
val inputBundle = Bundle()
@@ -202,7 +205,7 @@
fun toBundle_api35AndAboveWithOpId_success() {
val testBiometricPromptData =
BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
- val expectedOpId = TEST_CRYPTO_OBJECT.operationHandle
+ val expectedOpId = getTestCryptoObjectOpId()
val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt
index b9ed941..0df3636 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt
@@ -20,6 +20,7 @@
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
@@ -35,6 +36,13 @@
private const val KEYSTORE_INSTANCE = "AndroidKeyStore"
/**
+ * Retrieve the operationHandle for a CryptoObject by converting it to the framework equivalent
+ * type.
+ */
+ internal fun getTestCryptoObjectOpId(cryptoObject: CryptoObject) =
+ CryptoObjectUtils.getOperationHandle(cryptoObject = cryptoObject)
+
+ /**
* Returns a [BiometricPrompt.CryptoObject] for crypto-based authentication. Adapted from:
* [package androidx.biometric.samples.auth].
*/
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
index c279cca..8ae0d32 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
@@ -28,6 +28,7 @@
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.CryptoObject
import androidx.core.os.BuildCompat
+import androidx.credentials.provider.utils.CryptoObjectUtils
/**
* Biometric prompt data that can be optionally used to provide information needed for the system to
@@ -44,11 +45,19 @@
* determines whether a biometric auth or a device credential mechanism will / can be shown. The
* value for this property is found in [Authenticators].
*
+ * Finally, note that the CryptoObject does not presently support presentationSession and
+ * operationHandle constructors, and intends to support the CryptoObject built from the stable
+ * biometric jetpack library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0).
+ *
* @property allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
* the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
* @property cryptoObject a crypto object to be unlocked after successful authentication; When set,
* the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
- * [IllegalArgumentException] is thrown
+ * [IllegalArgumentException] is thrown, note that the CryptoObject does not presently support
+ * presentationSession and operationHandle constructors, and intends to support the CryptoObject
+ * built from the stable biometric jetpack library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0)
* @throws IllegalArgumentException if [cryptoObject] is not null, and the [allowedAuthenticators]
* is not set to [BIOMETRIC_STRONG]
* @see Authenticators
@@ -76,11 +85,19 @@
* determines whether a biometric auth or a device credential mechanism will / can be shown. The
* value for this property is found in [Authenticators].
*
+ * Finally, note that the [CryptoObject] does not presently support presentationSession and
+ * operationHandle constructors, and intends to support the CryptoObject built from the stable
+ * biometric jetpack library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0).
+ *
* @param allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
* the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
* @param cryptoObject a crypto object to be unlocked after successful authentication; When set,
* the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
- * [IllegalArgumentException] is thrown
+ * [IllegalArgumentException] is thrown, note that the CryptoObject does not presently support
+ * presentationSession and operationHandle constructors, and intends to support the
+ * CryptoObject built from the stable biometric jetpack library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0)
* @throws IllegalArgumentException if [cryptoObject] is not null, and the
* [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
* @see Authenticators
@@ -180,7 +197,10 @@
/**
* Sets whether this [BiometricPromptData] should have a crypto object associated with this
* authentication. If opting to pass in a value, the [allowedAuthenticators] must be
- * [BIOMETRIC_STRONG].
+ * [BIOMETRIC_STRONG]. The CryptoObject does not presently support presentationSession and
+ * operationHandle constructors and intends to support the CryptoObject built from the
+ * stable biometric jetpack library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0).
*
* @param cryptoObject the [CryptoObject] to be associated with this biometric
* authentication flow
@@ -224,6 +244,14 @@
}
private object ApiMinImpl {
+
+ /**
+ * Encapsulates the allowedAuthenticators from this [biometricPromptData] into a parcelable
+ * bundle.
+ *
+ * @param biometricPromptData the [BiometricPromptData] to convert into a bundle
+ * @return a bundle containing the representative bits of the [biometricPromptData]
+ */
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
@@ -235,6 +263,12 @@
return bundle
}
+ /**
+ * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.
+ *
+ * @param bundle the bundle with which to recover an instance of [BiometricPromptData] from
+ * @return an instance of [BiometricPromptData] recovered from the [bundle]
+ */
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun fromBundle(bundle: Bundle): BiometricPromptData {
@@ -249,6 +283,13 @@
private object Api35Impl {
+ /**
+ * Encapsulates the operationHandle and the allowed authenticator from this
+ * [biometricPromptData] into a parcelable bundle.
+ *
+ * @param biometricPromptData the [BiometricPromptData] to convert into a bundle
+ * @return a bundle containing the representative bits of the [biometricPromptData]
+ */
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
@@ -258,24 +299,36 @@
biometricPromptData.allowedAuthenticators
)
biometricPromptData.cryptoObject?.let {
- bundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, it.operationHandle)
+ bundle.putLong(
+ BUNDLE_HINT_CRYPTO_OP_ID,
+ CryptoObjectUtils.getOperationHandle(
+ cryptoObject = biometricPromptData.cryptoObject
+ )
+ )
}
return bundle
}
+ /**
+ * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.
+ *
+ * At present, the library owners do not support re-forming the [CryptoObject] from the
+ * bundle, as the library presently supports the stable AndroidX biometric library described
+ * [here](https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0).
+ *
+ * @param bundle the bundle with which to recover an instance of [BiometricPromptData] from
+ * @return an instance of [BiometricPromptData] recovered from the [bundle]
+ */
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun fromBundle(bundle: Bundle): BiometricPromptData {
- var cryptoObject: CryptoObject? = null
- if (bundle.containsKey(BUNDLE_HINT_CRYPTO_OP_ID)) {
- val opId = bundle.getLong(BUNDLE_HINT_CRYPTO_OP_ID)
- cryptoObject = CryptoObject(opId)
- }
+ // TODO(b/368395001) : Once the AndroidX biometric library updates the stable
+ // release, re-form the cryptoObject.
val biometricPromptData =
BiometricPromptData(
allowedAuthenticators = bundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS),
- cryptoObject = cryptoObject,
+ cryptoObject = null,
isCreatedFromBundle = true,
)
return biometricPromptData
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
index 83401bf..241a156 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
@@ -32,6 +32,7 @@
import androidx.credentials.PasswordCredential
import androidx.credentials.PublicKeyCredential
import androidx.credentials.provider.CreateEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.CryptoObjectUtils.getOperationHandle
import java.time.Instant
import java.util.Collections
@@ -369,7 +370,7 @@
)
biometricPromptData.cryptoObject?.let {
sliceBuilder.addLong(
- biometricPromptData.cryptoObject.operationHandle,
+ getOperationHandle(biometricPromptData.cryptoObject),
/*subType=*/ null,
listOf(SLICE_HINT_CRYPTO_OP_ID)
)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
index 342bb81..a53b84b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
@@ -32,6 +32,7 @@
import androidx.credentials.CredentialOption
import androidx.credentials.R
import androidx.credentials.provider.CustomCredentialEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.CryptoObjectUtils.getOperationHandle
import java.time.Instant
import java.util.Collections
@@ -123,6 +124,11 @@
}
/**
+ * Custom credential entry for a custom credential type that is displayed on the account
+ * selector UI.
+ *
+ * Each entry corresponds to an account that can provide a credential.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param title the title shown with this entry on the selector UI
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -179,6 +185,11 @@
)
/**
+ * Custom credential entry for a custom credential type that is displayed on the account
+ * selector UI.
+ *
+ * Each entry corresponds to an account that can provide a credential.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param title the title shown with this entry on the selector UI
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -238,6 +249,11 @@
)
/**
+ * Custom credential entry for a custom credential type that is displayed on the account
+ * selector UI.
+ *
+ * Each entry corresponds to an account that can provide a credential.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param title the title shown with this entry on the selector UI
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -326,7 +342,7 @@
)
biometricPromptData.cryptoObject?.let {
sliceBuilder.addLong(
- biometricPromptData.cryptoObject.operationHandle,
+ getOperationHandle(biometricPromptData.cryptoObject),
/*subType=*/ null,
listOf(SLICE_HINT_CRYPTO_OP_ID)
)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
index 6f41b70..12e84c6 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
@@ -34,6 +34,7 @@
import androidx.credentials.R
import androidx.credentials.provider.PasswordCredentialEntry.Api28Impl.toSlice
import androidx.credentials.provider.PasswordCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.CryptoObjectUtils.getOperationHandle
import java.time.Instant
import java.util.Collections
@@ -116,6 +117,15 @@
}
/**
+ * A password credential entry that is displayed on the account selector UI. This entry denotes
+ * that a credential of type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] is available for the
+ * user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the password credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -174,6 +184,15 @@
)
/**
+ * A password credential entry that is displayed on the account selector UI. This entry denotes
+ * that a credential of type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] is available for the
+ * user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the password credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -238,6 +257,15 @@
)
/**
+ * A password credential entry that is displayed on the account selector UI. This entry denotes
+ * that a credential of type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] is available for the
+ * user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the password credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -327,7 +355,7 @@
)
biometricPromptData.cryptoObject?.let {
sliceBuilder.addLong(
- biometricPromptData.cryptoObject.operationHandle,
+ getOperationHandle(biometricPromptData.cryptoObject),
/*subType=*/ null,
listOf(SLICE_HINT_CRYPTO_OP_ID)
)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
index 9aefe535..b236cdd 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
@@ -34,6 +34,7 @@
import androidx.credentials.R
import androidx.credentials.provider.PublicKeyCredentialEntry.Api28Impl.toSlice
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.CryptoObjectUtils.getOperationHandle
import java.time.Instant
import java.util.Collections
@@ -122,6 +123,15 @@
}
/**
+ * A public key credential entry that is displayed on the account selector UI. This entry
+ * denotes that a credential of type [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] is
+ * available for the user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the public key credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -170,6 +180,15 @@
)
/**
+ * A public key credential entry that is displayed on the account selector UI. This entry
+ * denotes that a credential of type [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] is
+ * available for the user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the public key credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -224,6 +243,15 @@
)
/**
+ * A public key credential entry that is displayed on the account selector UI. This entry
+ * denotes that a credential of type [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] is
+ * available for the user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must set the
+ * final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
* @param context the context of the calling app, required to retrieve fallback resources
* @param username the username of the account holding the public key credential
* @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
@@ -314,7 +342,7 @@
)
biometricPromptData.cryptoObject?.let {
sliceBuilder.addLong(
- biometricPromptData.cryptoObject.operationHandle,
+ getOperationHandle(biometricPromptData.cryptoObject),
/*subType=*/ null,
listOf(SLICE_HINT_CRYPTO_OP_ID)
)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/CryptoObjectUtils.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/CryptoObjectUtils.kt
new file mode 100644
index 0000000..01bd74d
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/CryptoObjectUtils.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2024 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.credentials.provider.utils
+
+import android.hardware.biometrics.BiometricPrompt
+import android.os.Build
+import android.security.identity.IdentityCredential
+import androidx.annotation.RequiresApi
+import java.security.Signature
+import javax.crypto.Cipher
+import javax.crypto.Mac
+
+/**
+ * Utility class for creating and converting between different types of crypto objects that may be
+ * used internally by [androidx.biometric.BiometricPrompt] or the
+ * [androidx.biometric.BiometricManager].
+ *
+ * Borrowed from [package androidx.biometric] to support credential manager operations where both
+ * framework and jetpack data types are relevant.
+ *
+ * TODO(b/369394452) : Remove once biometrics new stable library reached.
+ */
+internal object CryptoObjectUtils {
+
+ /**
+ * Wraps a crypto object to be passed to [android.hardware.biometrics.BiometricPrompt].
+ *
+ * @param cryptoObject An instance of [androidx.biometric.BiometricPrompt.CryptoObject].
+ * @return An equivalent crypto object that is compatible with
+ * [android.hardware.biometrics.BiometricPrompt].
+ */
+ @RequiresApi(Build.VERSION_CODES.P)
+ fun wrapForBiometricPrompt(
+ cryptoObject: androidx.biometric.BiometricPrompt.CryptoObject?
+ ): BiometricPrompt.CryptoObject? {
+ if (cryptoObject == null) {
+ return null
+ }
+
+ val cipher = cryptoObject.cipher
+ if (cipher != null) {
+ return Api28Impl.create(cipher)
+ }
+
+ val signature = cryptoObject.signature
+ if (signature != null) {
+ return Api28Impl.create(signature)
+ }
+
+ val mac = cryptoObject.mac
+ if (mac != null) {
+ return Api28Impl.create(mac)
+ }
+
+ // Identity credential is only supported on API 30 and above.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val identityCredential = cryptoObject.identityCredential
+ if (identityCredential != null) {
+ return Api30Impl.create(identityCredential)
+ }
+ }
+
+ // Presentation session is only supported on API 33 and above, and thus not available in
+ // the supported stable library (biometrics 1.1.0).
+
+ // Operation handle is only supported on API 35 and above, and thus not available in
+ // the supported stable library (biometrics 1.1.0).
+ return null
+ }
+
+ /**
+ * Get the `operationHandle` associated with this object or 0 if none. This needs to be achieved
+ * by getting the corresponding [android.hardware.biometrics.BiometricPrompt.CryptoObject] and
+ * then get its operation handle.
+ *
+ * @param cryptoObject An instance of [androidx.biometric.BiometricPrompt.CryptoObject].
+ * @return The `operationHandle` associated with this object or 0 if none.
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun getOperationHandle(cryptoObject: androidx.biometric.BiometricPrompt.CryptoObject?): Long {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ val wrappedCryptoObject = wrapForBiometricPrompt(cryptoObject)
+ if (wrappedCryptoObject != null) {
+ return Api35Impl.getOperationHandle(wrappedCryptoObject)
+ }
+ }
+ return 0
+ }
+
+ /**
+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private object Api35Impl {
+ /**
+ * Gets the operation handle associated with the given crypto object, if any.
+ *
+ * @param crypto An instance of [android.hardware.biometrics.BiometricPrompt.CryptoObject].
+ * @return The wrapped operation handle object, or `null`.
+ */
+ fun getOperationHandle(crypto: BiometricPrompt.CryptoObject): Long {
+ return crypto.operationHandle
+ }
+ }
+
+ /**
+ * Nested class to avoid verification errors for methods introduced in Android 11.0 (API 30).
+ */
+ @RequiresApi(Build.VERSION_CODES.R)
+ private object Api30Impl {
+ /**
+ * Creates an instance of the framework class
+ * [android.hardware.biometrics.BiometricPrompt.CryptoObject] from the given identity
+ * credential.
+ *
+ * @param identityCredential The identity credential object to be wrapped.
+ * @return An instance of [android.hardware.biometrics.BiometricPrompt.CryptoObject].
+ */
+ @Suppress("deprecation")
+ fun create(identityCredential: IdentityCredential): BiometricPrompt.CryptoObject {
+ return BiometricPrompt.CryptoObject(identityCredential)
+ }
+ }
+
+ /** Nested class to avoid verification errors for methods introduced in Android 9.0 (API 28). */
+ @RequiresApi(Build.VERSION_CODES.P)
+ private object Api28Impl {
+ /**
+ * Creates an instance of the framework class
+ * [android.hardware.biometrics.BiometricPrompt.CryptoObject] from the given cipher.
+ *
+ * @param cipher The cipher object to be wrapped.
+ * @return An instance of [android.hardware.biometrics.BiometricPrompt.CryptoObject].
+ */
+ fun create(cipher: Cipher): BiometricPrompt.CryptoObject {
+ return BiometricPrompt.CryptoObject(cipher)
+ }
+
+ /**
+ * Creates an instance of the framework class
+ * [android.hardware.biometrics.BiometricPrompt.CryptoObject] from the given signature.
+ *
+ * @param signature The signature object to be wrapped.
+ * @return An instance of [android.hardware.biometrics.BiometricPrompt.CryptoObject].
+ */
+ fun create(signature: Signature): BiometricPrompt.CryptoObject {
+ return BiometricPrompt.CryptoObject(signature)
+ }
+
+ /**
+ * Creates an instance of the framework class
+ * [android.hardware.biometrics.BiometricPrompt.CryptoObject] from the given MAC.
+ *
+ * @param mac The MAC object to be wrapped.
+ * @return An instance of [android.hardware.biometrics.BiometricPrompt.CryptoObject].
+ */
+ fun create(mac: Mac): BiometricPrompt.CryptoObject {
+ return BiometricPrompt.CryptoObject(mac)
+ }
+ }
+}
diff --git a/development/jspecify_update.py b/development/jspecify_update.py
new file mode 100755
index 0000000..06e8fe5
--- /dev/null
+++ b/development/jspecify_update.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+
+import os
+import subprocess
+import sys
+from subprocess import CompletedProcess
+
+
+def usage():
+ print("""Usage: jspecify_update.py <dir>
+This script updates all projects in a directory to use JSpecify annotations instead of AndroidX
+nullness annotations. If no directory is provided, uses the root frameworks/support dir.
+""")
+ sys.exit(1)
+
+
+def run_gradle_task(gradle_path: str, task: str, args: list[str] | None = None) -> \
+ CompletedProcess[bytes]:
+ """Runs a Gradle task ans returns the result."""
+ cmd = [gradle_path, task] + (args or [])
+ return subprocess.run(cmd, capture_output=True)
+
+
+def update_annotations(dir_path: str, gradle_path: str) -> bool:
+ """If the project in dir_path has a lint task, runs lintFix to shift all AndroidX nullness
+ annotations to type-use position. Returns whether any updates were applied."""
+ tasks_result = run_gradle_task(gradle_path, "tasks")
+ if "lintFix" not in str(tasks_result.stdout):
+ print(f"Lint task does not exist for {dir_path}")
+ return False
+
+ fix_result = run_gradle_task(gradle_path, "lintFix", ["-Pandroidx.useJSpecifyAnnotations=true"])
+ return fix_result.returncode != 0
+
+
+def update_imports_for_file(filepath: str) -> bool:
+ """Replaces AndroidX nullness annotation imports with JSpecify imports. Returns whether any
+ updates were needed."""
+ with open(filepath, "r") as f:
+ lines = f.readlines()
+
+ replacements = {
+ "import androidx.annotation.NonNull": "import org.jspecify.annotations.NonNull;\n",
+ "import androidx.annotation.Nullable": "import org.jspecify.annotations.Nullable;\n"
+ }
+ updated_count = 0
+ for i, line in enumerate(lines):
+ for target, replacement in replacements.items():
+ if target in line:
+ lines[i] = replacement
+ updated_count += 1
+ break
+
+ if updated_count == len(replacements):
+ break
+
+ replaced = updated_count > 0
+ if replaced:
+ with open(filepath, "w") as f:
+ f.writelines(lines)
+ return replaced
+
+
+def reformat_files(gradle_path: str) -> None:
+ """Runs the java formatter to fix imports for the specified files."""
+ run_gradle_task(gradle_path, "javaFormat", ["--fix-imports-only"]),
+
+
+def update_imports(dir_path: str, gradle_path: str) -> bool:
+ """For each java file in the directory, replaces the AndroidX nullness imports with JSpecify
+ imports and runs the java formatter to correct the new import order."""
+ java_files = []
+ for sub_dir_path, _, filenames in os.walk(dir_path):
+ for filename in filenames:
+ (_, ext) = os.path.splitext(filename)
+ if ext == ".java":
+ file_path = os.path.join(sub_dir_path, filename)
+ if update_imports_for_file(file_path):
+ java_files.append(file_path)
+
+ if java_files:
+ reformat_files(gradle_path)
+ return java_files
+
+
+def add_jspecify_dependency(build_gradle_path: str) -> None:
+ """Adds a JSpecify dependency to the build file."""
+ with open(build_gradle_path, "r") as f:
+ lines = f.readlines()
+
+ jspecify_dependency = " api(libs.jspecify)\n"
+ if jspecify_dependency in lines:
+ print(f"JSpecify dependency already present for {build_gradle_path}")
+ return
+
+ dependencies_start = None
+ for i in range(len(lines)):
+ line = lines[i]
+ if line.startswith("dependencies {"):
+ dependencies_start = i
+ break
+
+ if not dependencies_start:
+ print(f"No dependencies block found for {build_gradle_path}")
+ return
+
+ lines.insert(dependencies_start + 1, " api(libs.jspecify)\n")
+ with open(build_gradle_path, "w") as f:
+ f.writelines(lines)
+
+
+def process_dir(dir_path: str, root_dir_path: str) -> None:
+ """Updates the directory to use JSpecify annotations."""
+ print(f"Processing {dir_path}")
+ os.chdir(dir_path)
+ gradle_path = os.path.join(root_dir_path, "gradlew")
+ if update_annotations(dir_path, gradle_path):
+ print(f"Lint fixes applied in {dir_path}")
+ if update_imports(dir_path, gradle_path):
+ add_jspecify_dependency(os.path.join(dir_path, "build.gradle"))
+
+
+def main(start_dir: str | None) -> None:
+ # Location of this script: under support/development
+ script_path = os.path.realpath(__file__)
+ # Move up to the support dir
+ support_dir = os.path.dirname(os.path.dirname(script_path))
+
+ # Search the specified directory, or the support dir if there wasn't one
+ if not start_dir:
+ start_dir = support_dir
+ else:
+ start_dir = os.path.abspath(start_dir)
+ for dir_path, _, filenames in os.walk(start_dir):
+ if dir_path == support_dir:
+ continue
+ if "build.gradle" in filenames:
+ process_dir(dir_path, support_dir)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) == 1:
+ main(None)
+ elif len(sys.argv) == 2:
+ main(sys.argv[1])
+ else:
+ usage()
diff --git a/development/update_gradle.sh b/development/update_gradle.sh
new file mode 100755
index 0000000..b400ac4
--- /dev/null
+++ b/development/update_gradle.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+set -e
+
+# Check if the user has provided the version as an argument
+if [ -z "$1" ]; then
+ echo "Error: No version provided. Usage: $0 <gradle-version>"
+ exit 1
+fi
+
+VERSION="$1"
+DEST_DIR="../../tools/external/gradle"
+WRAPPER_FILES=("gradle/wrapper/gradle-wrapper.properties" "playground-common/gradle/wrapper/gradle-wrapper.properties")
+
+BASE_URL="https://services.gradle.org/distributions"
+ZIP_FILE="gradle-${VERSION}-bin.zip"
+SHA_FILE="${ZIP_FILE}.sha256"
+
+# Function to check if a URL is valid by checking the HTTP status code
+check_url() {
+ local url="$1"
+
+ echo "Checking URL: $url"
+
+ http_status=$(curl -L --silent --head --write-out "%{http_code}" --output /dev/null "$url")
+
+ if [ "$http_status" -ne 200 ]; then
+ echo "Error: URL returned status code $http_status. The file doesn't exist at: $url"
+ exit 1
+ else
+ echo "URL is valid: $url"
+ fi
+}
+
+check_url "$BASE_URL/$ZIP_FILE"
+check_url "$BASE_URL/$SHA_FILE"
+
+echo "Cleaning destination directory: $DEST_DIR"
+rm -rf "$DEST_DIR"/*
+mkdir -p "$DEST_DIR"
+
+echo "Downloading Gradle ${VERSION}..."
+curl -Lo "$DEST_DIR/$ZIP_FILE" "$BASE_URL/$ZIP_FILE"
+curl -Lo "$DEST_DIR/$SHA_FILE" "$BASE_URL/$SHA_FILE"
+
+GRADLE_SHA256SUM=$(cat "$DEST_DIR/$SHA_FILE")
+
+echo "Downloaded Gradle ${VERSION} with SHA256: $GRADLE_SHA256SUM"
+
+update_gradle_wrapper_properties() {
+ local file="$1"
+ echo "Updating $file..."
+
+ if [ "$(uname)" = "Darwin" ]; then
+ sed -i '' "
+ s|distributionUrl=.*tools/external/gradle/.*|distributionUrl=../../../../tools/external/gradle/${ZIP_FILE}|;
+ s|distributionUrl=https\\\://services.gradle.org/distributions/.*|distributionUrl=https\\\://services.gradle.org/distributions/${ZIP_FILE}|;
+ s|distributionSha256Sum=.*|distributionSha256Sum=${GRADLE_SHA256SUM}|
+ " "$file"
+ else
+ sed -i "
+ s|distributionUrl=.*tools/external/gradle/.*|distributionUrl=../../../../tools/external/gradle/${ZIP_FILE}|;
+ s|distributionUrl=https\\\://services.gradle.org/distributions/.*|distributionUrl=https\\\://services.gradle.org/distributions/${ZIP_FILE}|;
+ s|distributionSha256Sum=.*|distributionSha256Sum=${GRADLE_SHA256SUM}|
+ " "$file"
+ fi
+
+ echo "Updated $file."
+}
+
+for file in "${WRAPPER_FILES[@]}"; do
+ update_gradle_wrapper_properties "$file"
+done
+
+echo "Gradle binary downloaded, and the wrapper properties updated successfully!"
+
+echo "Testing the setup with './gradlew bOS --dry-run'..."
+if ./gradlew bOS --dry-run; then
+ echo "Download and setup successful!"
+ echo "You can now upload changes in $(pwd) and $DEST_DIR to Gerrit!"
+fi
diff --git a/glance/glance-appwidget-multiprocess/api/current.txt b/glance/glance-appwidget-multiprocess/api/current.txt
index 6714fbe..6252f24 100644
--- a/glance/glance-appwidget-multiprocess/api/current.txt
+++ b/glance/glance-appwidget-multiprocess/api/current.txt
@@ -23,8 +23,7 @@
public abstract class MultiProcessGlanceAppWidget extends androidx.glance.appwidget.GlanceAppWidget {
ctor public MultiProcessGlanceAppWidget();
ctor public MultiProcessGlanceAppWidget(optional @LayoutRes int errorUiLayout);
- method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig();
- property public androidx.glance.appwidget.multiprocess.MultiProcessConfig? multiProcessConfig;
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig(android.content.Context context);
}
}
diff --git a/glance/glance-appwidget-multiprocess/api/restricted_current.txt b/glance/glance-appwidget-multiprocess/api/restricted_current.txt
index 6714fbe..6252f24 100644
--- a/glance/glance-appwidget-multiprocess/api/restricted_current.txt
+++ b/glance/glance-appwidget-multiprocess/api/restricted_current.txt
@@ -23,8 +23,7 @@
public abstract class MultiProcessGlanceAppWidget extends androidx.glance.appwidget.GlanceAppWidget {
ctor public MultiProcessGlanceAppWidget();
ctor public MultiProcessGlanceAppWidget(optional @LayoutRes int errorUiLayout);
- method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig();
- property public androidx.glance.appwidget.multiprocess.MultiProcessConfig? multiProcessConfig;
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig(android.content.Context context);
}
}
diff --git a/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt
index 415fb48..1afa3a9 100644
--- a/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt
+++ b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt
@@ -40,29 +40,34 @@
@LayoutRes internal open val errorUiLayout: Int = R.layout.glance_error_layout,
) : GlanceAppWidget(errorUiLayout) {
/**
- * Override [multiProcessConfig] to provide a [androidx.work.multiprocess.RemoteWorkerService]
- * that runs in the same process as the [androidx.glance.appwidget.GlanceAppWidgetReceiver] that
- * this is attached to.
+ * Override [getMultiProcessConfig] to provide a
+ * [androidx.work.multiprocess.RemoteWorkerService] that runs in the same process as the
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] that this is attached to.
*
* If null, then this widget will be run with normal WorkManager, i.e. the same behavior as
* GlanceAppWidget.
*/
- public open val multiProcessConfig: MultiProcessConfig? = null
+ public open fun getMultiProcessConfig(context: Context): MultiProcessConfig? = null
- @get:RestrictTo(Scope.LIBRARY_GROUP)
- final override val components: GlanceComponents?
- get() = multiProcessConfig?.toGlanceComponents()
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ final override fun getComponents(context: Context): GlanceComponents? =
+ getMultiProcessConfig(context)?.toGlanceComponents()
- @get:RestrictTo(Scope.LIBRARY_GROUP)
- protected final override val sessionManager: SessionManager
- get() = if (multiProcessConfig != null) RemoteSessionManager else GlanceSessionManager
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected final override fun getSessionManager(context: Context): SessionManager =
+ if (getMultiProcessConfig(context) != null) {
+ RemoteSessionManager
+ } else {
+ GlanceSessionManager
+ }
@RestrictTo(Scope.LIBRARY_GROUP)
protected final override fun createAppWidgetSession(
+ context: Context,
id: AppWidgetId,
options: Bundle?
): AppWidgetSession {
- return multiProcessConfig?.let { config ->
+ return getMultiProcessConfig(context)?.let { config ->
RemoteAppWidgetSession(this, config.remoteWorkerService, id, options)
} ?: AppWidgetSession(this, id, options)
}
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
index a9c4fc1..4f228ac 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
@@ -187,7 +187,7 @@
layoutConfig.addLayout(root),
DpSize.Unspecified,
receiver,
- widget.components ?: GlanceComponents.getDefault(context),
+ widget.getComponents(context) ?: GlanceComponents.getDefault(context),
)
if (shouldPublish) {
appWidgetManager.updateAppWidget(id.appWidgetId, rv)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index 2f2358e..6dc3f1e 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -58,8 +58,8 @@
abstract class GlanceAppWidget(
@LayoutRes internal open val errorUiLayout: Int = R.layout.glance_error_layout,
) {
- @get:RestrictTo(Scope.LIBRARY_GROUP)
- protected open val sessionManager: SessionManager = GlanceSessionManager
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected open fun getSessionManager(context: Context): SessionManager = GlanceSessionManager
/**
* Override this function to provide the Glance Composable.
@@ -140,7 +140,7 @@
*/
internal suspend fun deleted(context: Context, appWidgetId: Int) {
val glanceId = AppWidgetId(appWidgetId)
- sessionManager.runWithLock { closeSession(glanceId.toSessionKey()) }
+ getSessionManager(context).runWithLock { closeSession(glanceId.toSessionKey()) }
try {
onDelete(context, glanceId)
} catch (cancelled: CancellationException) {
@@ -163,9 +163,9 @@
) {
Tracing.beginGlanceAppWidgetUpdate()
val glanceId = AppWidgetId(appWidgetId)
- sessionManager.runWithLock {
+ getSessionManager(context).runWithLock {
if (!isSessionRunning(context, glanceId.toSessionKey())) {
- startSession(context, createAppWidgetSession(glanceId, options))
+ startSession(context, createAppWidgetSession(context, glanceId, options))
return@runWithLock
}
val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
@@ -184,7 +184,8 @@
options: Bundle? = null,
) {
val glanceId = AppWidgetId(appWidgetId)
- sessionManager.getOrCreateAppWidgetSession(context, glanceId, options) { session ->
+ getSessionManager(context).getOrCreateAppWidgetSession(context, glanceId, options) { session
+ ->
session.runLambda(actionKey)
}
}
@@ -200,7 +201,8 @@
return
}
val glanceId = AppWidgetId(appWidgetId)
- sessionManager.getOrCreateAppWidgetSession(context, glanceId, options) { session ->
+ getSessionManager(context).getOrCreateAppWidgetSession(context, glanceId, options) { session
+ ->
session.updateAppWidgetOptions(options)
}
}
@@ -247,7 +249,7 @@
block: suspend SessionManagerScope.(AppWidgetSession) -> Unit
) = runWithLock {
if (!isSessionRunning(context, glanceId.toSessionKey())) {
- startSession(context, createAppWidgetSession(glanceId, options))
+ startSession(context, createAppWidgetSession(context, glanceId, options))
}
val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
block(session)
@@ -259,11 +261,15 @@
*
* If null, then the default components will be used.
*/
- @get:RestrictTo(Scope.LIBRARY_GROUP) open val components: GlanceComponents? = null
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ open fun getComponents(context: Context): GlanceComponents? = null
@RestrictTo(Scope.LIBRARY_GROUP)
- protected open fun createAppWidgetSession(id: AppWidgetId, options: Bundle? = null) =
- AppWidgetSession(this@GlanceAppWidget, id, options)
+ protected open fun createAppWidgetSession(
+ context: Context,
+ id: AppWidgetId,
+ options: Bundle? = null
+ ) = AppWidgetSession(this@GlanceAppWidget, id, options)
}
@RestrictTo(Scope.LIBRARY_GROUP) data class AppWidgetId(val appWidgetId: Int) : GlanceId
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3e2af46..1739056 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,6 +150,7 @@
findbugs = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" }
firebaseAppindexing = { module = "com.google.firebase:firebase-appindexing", version = "19.2.0" }
freemarker = { module = "org.freemarker:freemarker", version = "2.3.31"}
+googlejavaformat = { module = "com.google.googlejavaformat:google-java-format", version = "1.22.0" }
googletest = { module = "com.android.ndk.thirdparty:googletest", version = "1.11.0-beta-1" }
hamcrestCore = { module = "org.hamcrest:hamcrest-core", version.ref = "hamcrestCore" }
hiltAndroid = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
@@ -165,6 +166,7 @@
jcodecJavaSe = { module = "org.jcodec:jcodec-javase", version.ref = "jcodec" }
json = { module = "org.json:json", version = "20180813" }
jsoup = { module = "org.jsoup:jsoup", version = "1.16.2" }
+jspecify = { module = "org.jspecify:jspecify", version = "1.0.0" }
jsqlparser = { module = "com.github.jsqlparser:jsqlparser", version = "3.1" }
jsr250 = { module = "javax.annotation:javax.annotation-api", version = "1.2" }
junit = { module = "junit:junit", version = "4.13.2" }
@@ -205,7 +207,7 @@
kotlinCoroutinesRx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinCoroutines" }
kotlinDaemonEmbeddable = { module = "org.jetbrains.kotlin:kotlin-daemon-embeddable", version.ref = "kotlin" }
kotlinKlibCommonizer = { module = "org.jetbrains.kotlin:kotlin-klib-commonizer", version.ref = "kotlin" }
-kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.9.0" }
+kotlinMetadataJvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" }
kotlinSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinSerialization" }
kotlinSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
kotlinSerializationJsonOkio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinSerialization" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 5d0eb4f..9922876 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -58,7 +58,7 @@
<trust group="androidx[.]annotation" version="1\.9\.0-beta[0-9][1-9]" regex="true" reason="New versions, not yet signed"/>
<trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
<trust group="androidx[.]collection" version="1\.5\.0-alpha[0-9][1-9]" regex="true" reason="Old versions, before signing"/>
- <trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
+ <trust group="androidx[.]collection" version="1\.[0-4]\..*" regex="true" reason="Old versions, before signing"/>
</trusted-artifacts>
<trusted-keys>
<trusted-key id="00089EE8C3AFA95A854D0F1DF800DD0933ECF7F7" group="com.google.guava"/>
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9bd15e6..b5f8900 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-8.10-bin.zip
-distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a
+distributionUrl=../../../../tools/external/gradle/gradle-8.10.2-bin.zip
+distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonValidationTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonValidationTest.kt
new file mode 100644
index 0000000..6b6973d
--- /dev/null
+++ b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonValidationTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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.graphics.shapes
+
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class PolygonValidationTest {
+ private val pentagonPoints =
+ floatArrayOf(
+ 0.2f,
+ 0.0f,
+ 0.8f,
+ 0.0f,
+ 1.0f,
+ 0.6f,
+ 0.5f,
+ 1.0f,
+ 0.0f,
+ 0.6f,
+ )
+
+ private val reverseOrientedPentagonPoints =
+ floatArrayOf(
+ 0.2f,
+ 0.0f,
+ 0.0f,
+ 0.6f,
+ 0.5f,
+ 1.0f,
+ 1.0f,
+ 0.6f,
+ 0.8f,
+ 0.0f,
+ )
+
+ @Test fun doesNotFixValidSharpPolygon() = staysUnchanged(RoundedPolygon(5))
+
+ @Test
+ fun doesNotFixValidRoundPolygon() =
+ staysUnchanged(RoundedPolygon(5, rounding = CornerRounding(0.5f)))
+
+ @Test
+ fun fixesAntiClockwiseOrientedPolygon() {
+ val valid = RoundedPolygon(pentagonPoints)
+
+ val broken = RoundedPolygon(reverseOrientedPentagonPoints)
+
+ fixes(broken, valid)
+ }
+
+ @Test
+ fun fixesAntiClockwiseOrientedRoundedPolygon() {
+ val valid = RoundedPolygon(pentagonPoints, rounding = CornerRounding(0.5f))
+
+ val broken = RoundedPolygon(reverseOrientedPentagonPoints, rounding = CornerRounding(0.5f))
+
+ fixes(broken, valid)
+ }
+
+ private fun staysUnchanged(polygon: RoundedPolygon) {
+ val copy = RoundedPolygon(polygon)
+ val fixedPolygon = PolygonValidator.fix(polygon)
+
+ assertTrue(polygon === fixedPolygon)
+ assertEquals(copy, polygon)
+ }
+
+ private fun fixes(broken: RoundedPolygon, expected: RoundedPolygon) {
+ val fixed = PolygonValidator.fix(broken)
+
+ assertFalse(broken == fixed)
+ assertPolygonsEqualish(expected, fixed)
+ }
+}
diff --git a/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt
index 8c66d27..f39f85e 100644
--- a/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt
+++ b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/TestUtils.kt
@@ -72,6 +72,26 @@
}
}
+internal fun assertFeaturesEqualish(expected: Feature, actual: Feature) {
+ assertCubicListsEqualish(expected.cubics, actual.cubics)
+ assertEquals(expected::class, actual::class)
+
+ if (expected is Feature.Corner && actual is Feature.Corner) {
+ pointsEqualish(expected.vertex, actual.vertex)
+ pointsEqualish(expected.roundedCenter, actual.roundedCenter)
+ assertEquals(expected.convex, actual.convex)
+ }
+}
+
+internal fun assertPolygonsEqualish(expected: RoundedPolygon, actual: RoundedPolygon) {
+ assertCubicListsEqualish(expected.cubics, actual.cubics)
+
+ assertEquals(expected.features.size, actual.features.size)
+ for (i in expected.features.indices) {
+ assertFeaturesEqualish(expected.features[i], actual.features[i])
+ }
+}
+
internal fun assertPointGreaterish(expected: Point, actual: Point) {
assertTrue(actual.x >= expected.x - Epsilon)
assertTrue(actual.y >= expected.y - Epsilon)
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt
index 8a2135d..501e598 100644
--- a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt
@@ -24,6 +24,8 @@
internal abstract class Feature(val cubics: List<Cubic>) {
internal abstract fun transformed(f: PointTransformer): Feature
+ internal abstract fun reversed(): Feature
+
/**
* Edges have only a list of the cubic curves which make up the edge. Edges lie between corners
* and have no vertex or concavity; the curves are simply straight lines (represented by Cubic
@@ -41,6 +43,16 @@
}
)
+ override fun reversed(): Edge {
+ val reversedCubics = mutableListOf<Cubic>()
+
+ for (i in cubics.lastIndex downTo 0) {
+ reversedCubics.add(cubics[i].reverse())
+ }
+
+ return Edge(reversedCubics)
+ }
+
override fun toString(): String = "Edge"
}
@@ -72,6 +84,18 @@
)
}
+ override fun reversed(): Corner {
+ val reversedCubics = mutableListOf<Cubic>()
+
+ for (i in cubics.lastIndex downTo 0) {
+ reversedCubics.add(cubics[i].reverse())
+ }
+
+ // TODO: b/369320447 - Revert flag negation when [RoundedPolygon] ignores orientation
+ // for setting the flag
+ return Corner(reversedCubics, vertex, roundedCenter, !convex)
+ }
+
override fun toString(): String {
return "Corner: vertex=$vertex, center=$roundedCenter, convex=$convex"
}
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonValidation.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonValidation.kt
new file mode 100644
index 0000000..607a0b1
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonValidation.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 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.graphics.shapes
+
+// @TODO: Make class public as soon as all validations are implemented and mention in
+// [RoundedPolygon] constructor
+
+/**
+ * Utility class to fix invalid [RoundedPolygon]s that will otherwise break [Morph]s in one way or
+ * another, as [RoundedPolygon] assumes correct input. Correct input meaning:
+ * - Closed geometry
+ * - Clockwise orientation of points
+ * - No self-intersections
+ * - No holes
+ */
+internal class PolygonValidator() {
+
+ companion object {
+
+ // @TODO: Update docs when other validations are implemented
+ /**
+ * Validates whether this [RoundedPolygon]'s orientation is clockwise and fixes it if
+ * necessary.
+ *
+ * @param polygon The [RoundedPolygon] to validate
+ * @return A new [RoundedPolygon] with fixed orientation, or the same [RoundedPolygon] as
+ * given when it was already valid
+ */
+ fun fix(polygon: RoundedPolygon): RoundedPolygon {
+ var result = polygon
+
+ debugLog(LOG_TAG) { "Validating polygon..." }
+
+ if (isCWOriented(polygon)) {
+ debugLog(LOG_TAG) { "Passed clockwise validation!" }
+ } else {
+ debugLog(LOG_TAG) { "Polygon is oriented anti-clockwise, fixing orientation..." }
+ result = fixCWOrientation(polygon)
+ }
+
+ return result
+ }
+
+ private fun isCWOriented(polygon: RoundedPolygon): Boolean {
+ var signedArea = 0.0f
+
+ for (i in polygon.cubics.indices) {
+ val cubic = polygon.cubics[i]
+ signedArea += (cubic.anchor1X - cubic.anchor0X) * (cubic.anchor1Y + cubic.anchor0Y)
+ }
+
+ return signedArea < 0
+ }
+
+ private fun fixCWOrientation(polygon: RoundedPolygon): RoundedPolygon {
+ val reversedFeatures = buildList {
+ // Persist first feature to stay a Corner
+ add(polygon.features.first().reversed())
+
+ for (i in polygon.features.lastIndex downTo 1) {
+ add(polygon.features[i].reversed())
+ }
+ }
+
+ return RoundedPolygon(reversedFeatures, polygon.centerX, polygon.centerY)
+ }
+ }
+}
+
+private const val LOG_TAG = "PolygonValidation"
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
index 7181326..e93e881 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/ReadRecordsSamples.kt
@@ -21,19 +21,57 @@
import androidx.activity.result.ActivityResultCaller
import androidx.annotation.Sampled
import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.contracts.ExerciseRouteRequestContract
+import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
+import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
import androidx.health.connect.client.readRecord
import androidx.health.connect.client.records.ExerciseRoute
import androidx.health.connect.client.records.ExerciseRouteResult
import androidx.health.connect.client.records.ExerciseSessionRecord
import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.SkinTemperatureRecord
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.Instant
+@OptIn(ExperimentalFeatureAvailabilityApi::class)
+@Sampled
+suspend fun ReadSkinTemperatureRecord(
+ healthConnectClient: HealthConnectClient,
+ startTime: Instant,
+ endTime: Instant
+) {
+ if (
+ healthConnectClient.features.getFeatureStatus(
+ HealthConnectFeatures.FEATURE_SKIN_TEMPERATURE
+ ) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
+ ) {
+ if (
+ healthConnectClient.permissionController
+ .getGrantedPermissions()
+ .contains(HealthPermission.getReadPermission(SkinTemperatureRecord::class))
+ ) {
+ val response =
+ healthConnectClient.readRecords(
+ ReadRecordsRequest<SkinTemperatureRecord>(
+ timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
+ )
+ )
+ for (skinTemperatureRecord in response.records) {
+ // Process each skin temperature record
+ }
+ } else {
+ // Permission hasn't been granted. Request permission to read skin temperature.
+ }
+ } else {
+ // Feature is not available. It is not possible to read skin temperature.
+ }
+}
+
@Sampled
suspend fun ReadStepsRange(
healthConnectClient: HealthConnectClient,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
index bef39e4..1c3d1ae 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
@@ -17,6 +17,7 @@
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
+import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.units.Temperature
import androidx.health.connect.client.units.TemperatureDelta
@@ -28,6 +29,11 @@
* Captures the skin temperature of a user. Each record can represent a series of measurements of
* temperature differences.
*
+ * The ability to insert or read this record type is dependent on the version of HealthConnect
+ * installed on the device. To check if available: call [HealthConnectFeatures.getFeatureStatus] and
+ * pass [HealthConnectFeatures.FEATURE_SKIN_TEMPERATURE] as an argument. See further down for an
+ * example on how to read skin temperature.
+ *
* @param startTime Start time of the record.
* @param startZoneOffset User experienced zone offset at [startTime], or null if unknown. Providing
* these will help history aggregations results stay consistent should user travel. Queries with
@@ -48,6 +54,7 @@
* @param metadata set of common metadata associated with the written record.
* @throws IllegalArgumentException if [startTime] > [endTime] or [deltas] are not within the record
* time range or baseline is not within [MIN_TEMPERATURE], [MAX_TEMPERATURE].
+ * @sample androidx.health.connect.client.samples.ReadSkinTemperatureRecord
*/
class SkinTemperatureRecord(
override val startTime: Instant,
diff --git a/ink/ink-authoring/api/current.txt b/ink/ink-authoring/api/current.txt
index 8bee50d..df33139 100644
--- a/ink/ink-authoring/api/current.txt
+++ b/ink/ink-authoring/api/current.txt
@@ -14,11 +14,14 @@
ctor public InProgressStrokesView(android.content.Context context, optional android.util.AttributeSet? attrs);
ctor public InProgressStrokesView(android.content.Context context, optional android.util.AttributeSet? attrs, optional @AttrRes int defStyleAttr);
method public void addFinishedStrokesListener(androidx.ink.authoring.InProgressStrokesFinishedListener listener);
+ method public void addToStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId);
+ method public void addToStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId, optional android.view.MotionEvent? prediction);
method public void addToStroke(androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.authoring.InProgressStrokeId strokeId);
method public void addToStroke(androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.authoring.InProgressStrokeId strokeId, optional androidx.ink.strokes.StrokeInputBatch prediction);
method public void cancelStroke(androidx.ink.authoring.InProgressStrokeId strokeId);
method public void cancelStroke(androidx.ink.authoring.InProgressStrokeId strokeId, optional android.view.MotionEvent? event);
method public void eagerInit();
+ method public void finishStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId);
method public void finishStroke(androidx.ink.strokes.StrokeInput input, androidx.ink.authoring.InProgressStrokeId strokeId);
method public java.util.Map<androidx.ink.authoring.InProgressStrokeId,androidx.ink.strokes.Stroke> getFinishedStrokes();
method public androidx.test.espresso.idling.CountingIdlingResource? getInProgressStrokeCounter();
@@ -31,6 +34,9 @@
method public void setMaskPath(android.graphics.Path?);
method public void setMotionEventToViewTransform(android.graphics.Matrix);
method public void setRendererFactory(kotlin.jvm.functions.Function0<? extends androidx.ink.rendering.android.canvas.CanvasStrokeRenderer>);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush, optional android.graphics.Matrix motionEventToWorldTransform);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush, optional android.graphics.Matrix motionEventToWorldTransform, optional android.graphics.Matrix strokeToWorldTransform);
method public androidx.ink.authoring.InProgressStrokeId startStroke(androidx.ink.strokes.StrokeInput input, androidx.ink.brush.Brush brush);
property public final androidx.test.espresso.idling.CountingIdlingResource? inProgressStrokeCounter;
property public final android.graphics.Path? maskPath;
diff --git a/ink/ink-authoring/api/restricted_current.txt b/ink/ink-authoring/api/restricted_current.txt
index 8bee50d..df33139 100644
--- a/ink/ink-authoring/api/restricted_current.txt
+++ b/ink/ink-authoring/api/restricted_current.txt
@@ -14,11 +14,14 @@
ctor public InProgressStrokesView(android.content.Context context, optional android.util.AttributeSet? attrs);
ctor public InProgressStrokesView(android.content.Context context, optional android.util.AttributeSet? attrs, optional @AttrRes int defStyleAttr);
method public void addFinishedStrokesListener(androidx.ink.authoring.InProgressStrokesFinishedListener listener);
+ method public void addToStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId);
+ method public void addToStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId, optional android.view.MotionEvent? prediction);
method public void addToStroke(androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.authoring.InProgressStrokeId strokeId);
method public void addToStroke(androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.authoring.InProgressStrokeId strokeId, optional androidx.ink.strokes.StrokeInputBatch prediction);
method public void cancelStroke(androidx.ink.authoring.InProgressStrokeId strokeId);
method public void cancelStroke(androidx.ink.authoring.InProgressStrokeId strokeId, optional android.view.MotionEvent? event);
method public void eagerInit();
+ method public void finishStroke(android.view.MotionEvent event, int pointerId, androidx.ink.authoring.InProgressStrokeId strokeId);
method public void finishStroke(androidx.ink.strokes.StrokeInput input, androidx.ink.authoring.InProgressStrokeId strokeId);
method public java.util.Map<androidx.ink.authoring.InProgressStrokeId,androidx.ink.strokes.Stroke> getFinishedStrokes();
method public androidx.test.espresso.idling.CountingIdlingResource? getInProgressStrokeCounter();
@@ -31,6 +34,9 @@
method public void setMaskPath(android.graphics.Path?);
method public void setMotionEventToViewTransform(android.graphics.Matrix);
method public void setRendererFactory(kotlin.jvm.functions.Function0<? extends androidx.ink.rendering.android.canvas.CanvasStrokeRenderer>);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush, optional android.graphics.Matrix motionEventToWorldTransform);
+ method public androidx.ink.authoring.InProgressStrokeId startStroke(android.view.MotionEvent event, int pointerId, androidx.ink.brush.Brush brush, optional android.graphics.Matrix motionEventToWorldTransform, optional android.graphics.Matrix strokeToWorldTransform);
method public androidx.ink.authoring.InProgressStrokeId startStroke(androidx.ink.strokes.StrokeInput input, androidx.ink.brush.Brush brush);
property public final androidx.test.espresso.idling.CountingIdlingResource? inProgressStrokeCounter;
property public final android.graphics.Path? maskPath;
diff --git a/ink/ink-authoring/build.gradle b/ink/ink-authoring/build.gradle
index 4180c02..3b003528 100644
--- a/ink/ink-authoring/build.gradle
+++ b/ink/ink-authoring/build.gradle
@@ -32,6 +32,7 @@
androidMain {
dependencies {
implementation("androidx.collection:collection:1.4.3")
+ implementation("androidx.graphics:graphics-core:1.0.0")
implementation("androidx.fragment:fragment-ktx:1.3.0")
implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
implementation(project(":core:core"))
@@ -41,7 +42,6 @@
implementation(project(":ink:ink-brush"))
implementation(project(":ink:ink-strokes"))
implementation(project(":ink:ink-rendering"))
- implementation(project(":graphics:graphics-core"))
}
}
diff --git a/ink/ink-authoring/src/androidInstrumentedTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt b/ink/ink-authoring/src/androidInstrumentedTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt
index 07894b6..39211bc 100644
--- a/ink/ink-authoring/src/androidInstrumentedTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt
+++ b/ink/ink-authoring/src/androidInstrumentedTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt
@@ -300,10 +300,10 @@
osDetectsEvent = 321_000_000
// The clock ticked once to record this time.
strokesViewGetsAction = 777_001
+ // And twice more for this one.
+ canvasFrontBufferStrokesRenderHelperData.finishesDrawCalls = 777_003
// And once more for this one.
- canvasFrontBufferStrokesRenderHelperData.finishesDrawCalls = 777_002
- // And once more for this one.
- estimatedPixelPresentationTime = 777_003
+ estimatedPixelPresentationTime = 777_004
}
)
)
@@ -815,11 +815,11 @@
osDetectsEvent = 333_000_000
// The clock ticked once to get this time.
strokesViewGetsAction = 334_000_001
- // And twice for this one - once on the UI thread and once on the render
+ // And thrice for this one - twice on the UI thread and once on the render
// thread.
- canvasFrontBufferStrokesRenderHelperData.finishesDrawCalls = 334_000_003
- // And once more for this one.
- estimatedPixelPresentationTime = 334_000_004
+ canvasFrontBufferStrokesRenderHelperData.finishesDrawCalls = 334_000_004
+ // And once more to get the estimated time.
+ estimatedPixelPresentationTime = 334_000_005
}
)
)
diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt
index 4ae50d0..5996a1b 100644
--- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt
+++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt
@@ -75,8 +75,8 @@
* This must be set to its desired value before the first call to [startStroke] or [eagerInit].
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@Deprecated("Prefer to allow the underlying implementation details to be chosen automatically.")
public var useHighLatencyRenderHelper: Boolean = false
@@ -89,8 +89,8 @@
* This must be set to its desired value before the first call to [startStroke] or [eagerInit].
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@Deprecated("Prefer to allow the underlying implementation details to be chosen automatically.")
public var useNewTPlusRenderHelper: Boolean = false
@@ -104,8 +104,8 @@
* If handoff is ever needed as soon as safely possible, call [requestHandoff].
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public var handoffDebounceTimeMs: Long = 0L
@UiThread
set(value) {
@@ -130,13 +130,13 @@
*/
// Needed on both property and on getter for AndroidX build, but the Kotlin compiler doesn't
// like it on the getter so suppress its complaint.
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@ExperimentalInkCustomBrushApi
@get:ExperimentalInkCustomBrushApi
@set:ExperimentalInkCustomBrushApi
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public var textureBitmapStore: TextureBitmapStore = TextureBitmapStore { null }
set(value) {
check(!isInitialized()) { "Cannot set textureBitmapStore after initialization." }
@@ -224,15 +224,15 @@
* minimize the amount of computation in this callback, and should also avoid allocations (since
* allocation may trigger the garbage collector).
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
// Needed on both property and on getter for AndroidX build, but the Kotlin compiler doesn't
// like it on the getter so suppress its complaint.
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@ExperimentalLatencyDataApi
@get:ExperimentalLatencyDataApi
@set:ExperimentalLatencyDataApi
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public var latencyDataCallback: LatencyDataCallback? = null
private val renderHelperCallback =
@@ -399,11 +399,30 @@
}
/**
- * Start building a stroke with the [event] data for [pointerId].
+ * Start building a stroke using a particular pointer within a [MotionEvent]. This would
+ * typically be followed by many calls to [addToStroke], and the sequence would end with a call
+ * to either [finishStroke] or [cancelStroke].
*
- * @param event The first [MotionEvent] as part of a Stroke's input data, typically an
- * ACTION_DOWN.
- * @param pointerId The id of the relevant pointer in the [event].
+ * In most circumstances, prefer to use this function over [startStroke] that accepts a
+ * [StrokeInput].
+ *
+ * For optimum performance, it is strongly recommended to call [View.requestUnbufferedDispatch]
+ * using [event] and the [View] that generated [event] alongside calling this function. When
+ * requested this way, unbuffered dispatch mode will automatically end when the gesture is
+ * complete.
+ *
+ * @param event The first [MotionEvent] as part of a Stroke's input data, typically one with a
+ * [MotionEvent.getActionMasked] value of [MotionEvent.ACTION_DOWN] or
+ * [MotionEvent.ACTION_POINTER_DOWN], but not restricted to those action types.
+ * @param pointerId The identifier of the pointer within [event] to be used for inking, as
+ * determined by [MotionEvent.getPointerId] and used as an input to
+ * [MotionEvent.findPointerIndex]. Note that this is the ID of the pointer, not its index.
+ * @param brush Brush specification for the stroke being started. Note that the overall scaling
+ * factor of [motionEventToWorldTransform] and [strokeToWorldTransform] combined should be
+ * related to the value of [Brush.epsilon] - in general, the larger the combined
+ * `motionEventToStrokeTransform` scaling factor is, the smaller on screen the stroke units
+ * are, so [Brush.epsilon] should be a larger quantity of stroke units to maintain a similar
+ * screen size.
* @param motionEventToWorldTransform The matrix that transforms [event] coordinates into the
* client app's "world" coordinates, which typically is defined by how a client app's document
* is panned/zoomed/rotated. This defaults to the identity matrix, in which case the world
@@ -412,22 +431,18 @@
* density (e.g. scaled by 1 / [android.util.DisplayMetrics.density]) and any pan/zoom/rotate
* gestures that have been applied to the "camera" which portrays the "world" on the device
* screen. This matrix must be invertible.
- * @param strokeToWorldTransform An optional matrix that transforms this stroke into the client
- * app's "world" coordinates, which allows the coordinates of the stroke to be defined in
- * something other than world coordinates. Defaults to the identity matrix, in which case the
- * stroke coordinate space is the same as world coordinate space. This matrix must be
- * invertible.
- * @param brush Brush specification for the stroke being started. Note that if
- * [motionEventToWorldTransform] and [strokeToWorldTransform] combine to a [MotionEvent] to
- * stroke coordinates transform that scales stroke coordinate units to be very different in
- * size than screen pixels, then it is recommended to update the value of [Brush.epsilon] to
- * reflect that.
- * @return The Stroke ID of the stroke being built, later used to identify which stroke is being
- * added to, finished, or canceled.
+ * @param strokeToWorldTransform Allows an object-specific (stroke-specific) coordinate space to
+ * be defined in relation to the caller's "world" coordinate space. This defaults to the
+ * identity matrix, which is typical for many use cases at the time of stroke construction. In
+ * typical use cases, stroke coordinates and world coordinates may start to differ from one
+ * another after stroke creation as a particular stroke is manipulated within the world, e.g.
+ * it may be moved, scaled, or rotated relative to other content within an app's document.
+ * This matrix must be invertible.
+ * @return The [InProgressStrokeId] of the stroke being built, later used to identify which
+ * stroke is being updated with [addToStroke] or ended with [finishStroke] or [cancelStroke].
* @throws IllegalArgumentException if [motionEventToWorldTransform] or [strokeToWorldTransform]
* is not invertible.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@JvmOverloads
public fun startStroke(
event: MotionEvent,
@@ -475,31 +490,44 @@
}
/**
- * Start building a stroke with the provided [input].
+ * Start building a stroke with the provided [input]. This would typically be followed by many
+ * calls to [addToStroke], and the sequence would end with a call to either [finishStroke] or
+ * [cancelStroke].
+ *
+ * In most circumstances, the [startStroke] overload that accepts a [MotionEvent] is more
+ * convenient. However, this overload using a [StrokeInput] is available for cases where the
+ * input data may not come directly from a [MotionEvent], such as receiving events over a
+ * network connection.
+ *
+ * If there is a way to request unbuffered dispatch from the source of the input data used here,
+ * equivalent to [View.requestUnbufferedDispatch] for unbuffered [MotionEvent] data, then be
+ * sure to request it for optimal performance.
*
* @param input The [StrokeInput] that started a stroke.
* @param brush Brush specification for the stroke being started. Note that if stroke coordinate
- * units (the [StrokeInput.x] and [StrokeInput.y] fields of [input] are scaled to be very
+ * units (the [StrokeInput.x] and [StrokeInput.y] fields of [input]) are scaled to be very
* different in size than screen pixels, then it is recommended to update the value of
* [Brush.epsilon] to reflect that.
- * @return The Stroke ID of the stroke being built, later used to identify which stroke is being
- * added to, finished, or canceled.
+ * @return The [InProgressStrokeId] of the stroke being built, later used to identify which
+ * stroke is being updated with [addToStroke] or ended with [finishStroke] or [cancelStroke].
*/
public fun startStroke(input: StrokeInput, brush: Brush): InProgressStrokeId =
inProgressStrokesManager.startStroke(input, brush)
/**
- * Add [event] data for [pointerId] to already started stroke with [strokeId].
+ * Add input data, from a particular pointer within a [MotionEvent], to an existing stroke.
*
- * @param event the next [MotionEvent] as part of a Stroke's input data, typically an
- * ACTION_MOVE.
- * @param pointerId the id of the relevant pointer in the [event].
- * @param strokeId the Stroke that is to be built upon with [event].
- * @param prediction optional predicted [MotionEvent] containing predicted inputs between event
- * and the time of the next frame, as generated by
- * [androidx.input.motionprediction.MotionEventPredictor.predict].
+ * @param event The next [MotionEvent] as part of a stroke's input data, typically one with
+ * [MotionEvent.getActionMasked] of [MotionEvent.ACTION_MOVE].
+ * @param pointerId The identifier of the pointer within [event] to be used for inking, as
+ * determined by [MotionEvent.getPointerId] and used as an input to
+ * [MotionEvent.findPointerIndex]. Note that this is the ID of the pointer, not its index.
+ * @param strokeId The [InProgressStrokeId] of the stroke to be built upon.
+ * @param prediction Predicted [MotionEvent] containing predicted inputs between [event] and the
+ * time of the next frame. This value typically comes from
+ * [androidx.input.motionprediction.MotionEventPredictor.predict]. It is technically optional,
+ * but it is strongly recommended to achieve the best performance.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@JvmOverloads
public fun addToStroke(
event: MotionEvent,
@@ -515,12 +543,13 @@
)
/**
- * Add [inputs] to already started stroke with [strokeId].
+ * Add input data from a [StrokeInputBatch] to an existing stroke.
*
- * @param inputs the next [StrokeInputBatch] to be added to the stroke.
- * @param strokeId the Stroke that is to be built upon with [inputs].
- * @param prediction optional [StrokeInputBatch] containing predicted inputs after this portion
- * of the stroke.
+ * @param inputs The next [StrokeInputBatch] to be added to the stroke.
+ * @param strokeId The [InProgressStrokeId] of the stroke to be built upon.
+ * @param prediction Predicted [StrokeInputBatch] containing predicted inputs between [inputs]
+ * and the time of the next frame. This can technically be empty, but it is strongly
+ * recommended for it to be non-empty to achieve the best performance.
*/
@JvmOverloads
public fun addToStroke(
@@ -558,13 +587,22 @@
}
/**
- * Complete the building of a stroke.
+ * Complete the building of a stroke, with the last input data coming from a particular pointer
+ * of a [MotionEvent].
*
- * @param event the last [MotionEvent] as part of a stroke, typically an ACTION_UP.
- * @param pointerId the id of the relevant pointer in the [event].
- * @param strokeId the stroke that is to be finished with the latest event.
+ * When the stroke no longer needs to be rendered by this [InProgressStrokesView] and can
+ * instead be rendered anywhere in the [View] hierarchy using [CanvasStrokeRenderer], the
+ * resulting [Stroke] object will be passed to the [InProgressStrokesFinishedListener] instances
+ * registered with this [InProgressStrokesView] using [addFinishedStrokesListener].
+ *
+ * @param event The last [MotionEvent] as part of a stroke's input data, typically one with
+ * [MotionEvent.getActionMasked] of [MotionEvent.ACTION_UP] or
+ * [MotionEvent.ACTION_POINTER_UP], but can also be other actions.
+ * @param pointerId The identifier of the pointer within [event] to be used for inking, as
+ * determined by [MotionEvent.getPointerId] and used as an input to
+ * [MotionEvent.findPointerIndex]. Note that this is the ID of the pointer, not its index.
+ * @param strokeId The [InProgressStrokeId] of the stroke to be finished.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun finishStroke(
event: MotionEvent,
pointerId: Int,
@@ -572,18 +610,36 @@
): Unit = inProgressStrokesManager.finishStroke(event, pointerId, strokeId)
/**
- * Complete the building of a stroke.
+ * Complete the building of a stroke, with the last input data coming from a [StrokeInput].
*
- * @param input the last [StrokeInput] in the stroke.
- * @param strokeId the stroke that is to be finished with the latest event.
+ * @param input The last [StrokeInput] in the stroke.
+ * @param strokeId The [InProgressStrokeId] of the stroke to be finished.
*/
public fun finishStroke(input: StrokeInput, strokeId: InProgressStrokeId): Unit =
inProgressStrokesManager.finishStroke(input, strokeId)
/**
- * Cancel the building of a stroke.
+ * Cancel the building of a stroke. It will no longer be visible within this
+ * [InProgressStrokesView], and no completed [Stroke] object will come through
+ * [InProgressStrokesFinishedListener].
*
- * @param strokeId the stroke to cancel.
+ * This is typically done for one of three reasons:
+ * 1. A [MotionEvent] with [MotionEvent.getActionMasked] of [MotionEvent.ACTION_CANCEL]. This
+ * tends to be when an entire gesture has been canceled, for example when a parent [View]
+ * uses [android.view.ViewGroup.onInterceptTouchEvent] to intercept and handle the gesture
+ * itself.
+ * 2. A [MotionEvent] with [MotionEvent.getFlags] containing [MotionEvent.FLAG_CANCELED]. This
+ * tends to be when the system has detected an unintentional touch, such as from the user
+ * resting their palm on the screen while writing or drawing, after some events from that
+ * unintentional pointer have already been delivered.
+ * 3. An app's business logic reinterprets a gesture previously used for inking as something
+ * else, and the earlier inking may be seen as unintentional. For example, an app that uses
+ * single-pointer gestures for inking and dual-pointer gestures for pan/zoom/rotate will
+ * start inking when the first pointer goes down, but when the second pointer goes down it
+ * may want to cancel the stroke from the first pointer rather than leave the small ink marks
+ * on the screen.
+ *
+ * @param strokeId The [InProgressStrokeId] of the stroke to be canceled.
* @param event The [MotionEvent] that led to this cancellation, if applicable.
*/
@JvmOverloads
diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/internal/InProgressStrokesManager.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/internal/InProgressStrokesManager.kt
index b06a42e..06633b8 100644
--- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/internal/InProgressStrokesManager.kt
+++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/internal/InProgressStrokesManager.kt
@@ -933,16 +933,10 @@
it,
)
}
- // TODO: b/287041801 - Don't necessarily always immediately [updateShape] after
- // [enqueueInputs].
- updateShape(getNanoTime() / 1_000_000L - strokeState.startEventTimeMillis).onFailure {
- // TODO(b/306361370): Throw here once input is more sanitized.
- Log.w(
- InProgressStrokesManager::class.simpleName,
- "Error during InProgressStroke.updateShape",
- it,
- )
- }
+ // Rather than updating the shape immediately, we enqueue the inputs and wait to update
+ // the
+ // shape until we have handled all the inputs in threadSharedState.inputActions. This is
+ // being done to reduce that amount of updateShape calls.
}
action.realInputs.clear()
action.predictedInputs.clear()
@@ -985,8 +979,8 @@
it,
)
}
- // TODO: b/287041801 - Don't necessarily always immediately [updateShape] after
- // [enqueueInputs].
+ // We update the finished stroke immediately after enqueueing because we know we are not
+ // going to be receiving any other inputs.
strokeState.inProgressStroke
.updateShape(getNanoTime() / 1_000_000L - strokeState.startEventTimeMillis)
.onFailure {
@@ -1210,7 +1204,22 @@
handleAction(nextInputAction)
renderThreadState.handledActions.add(nextInputAction)
}
-
+ val nowMillis = getNanoTime() / 1_000_000L
+ for (strokeState in renderThreadState.toDrawStrokes.values) {
+ val inProgressStroke = strokeState.inProgressStroke
+ if (inProgressStroke.getNeedsUpdate()) {
+ inProgressStroke
+ .updateShape(nowMillis - strokeState.startEventTimeMillis)
+ .onFailure {
+ // TODO(b/306361370): Throw here once input is more sanitized.
+ Log.w(
+ InProgressStrokesManager::class.simpleName,
+ "Error during InProgressStroke.updateShape after handleAction",
+ it,
+ )
+ }
+ }
+ }
if (inProgressStrokesRenderHelper.contentsPreservedBetweenDraws) {
// The updated region for each stroke must be drawn into for all strokes, not just
// itself, to
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7599e2b..8b67e7a 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -70,6 +70,17 @@
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
+ public final class InputToolType {
+ field public static final androidx.ink.brush.InputToolType.Companion Companion;
+ field public static final androidx.ink.brush.InputToolType MOUSE;
+ field public static final androidx.ink.brush.InputToolType STYLUS;
+ field public static final androidx.ink.brush.InputToolType TOUCH;
+ field public static final androidx.ink.brush.InputToolType UNKNOWN;
+ }
+
+ public static final class InputToolType.Companion {
+ }
+
public final class StockBrushes {
method public static androidx.ink.brush.BrushFamily getHighlighterLatest();
method public static androidx.ink.brush.BrushFamily getHighlighterV1();
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7599e2b..8b67e7a 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -70,6 +70,17 @@
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
+ public final class InputToolType {
+ field public static final androidx.ink.brush.InputToolType.Companion Companion;
+ field public static final androidx.ink.brush.InputToolType MOUSE;
+ field public static final androidx.ink.brush.InputToolType STYLUS;
+ field public static final androidx.ink.brush.InputToolType TOUCH;
+ field public static final androidx.ink.brush.InputToolType UNKNOWN;
+ }
+
+ public static final class InputToolType.Companion {
+ }
+
public final class StockBrushes {
method public static androidx.ink.brush.BrushFamily getHighlighterLatest();
method public static androidx.ink.brush.BrushFamily getHighlighterV1();
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
index d0c8086..467ed12 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
@@ -25,7 +25,6 @@
* The type of input tool used in producing [androidx.ink.strokes.StrokeInput], used by
* [BrushBehavior] to define when a behavior is applicable.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@UsedByNative
public class InputToolType
private constructor(
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt
index 27523e0..cef896e 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt
@@ -119,19 +119,49 @@
listOf(
predictionFadeOutBehavior,
BrushBehavior(
- source = BrushBehavior.Source.NORMALIZED_PRESSURE,
- target = BrushBehavior.Target.SIZE_MULTIPLIER,
- sourceValueRangeLowerBound = 0f,
+ BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ sourceValueRangeLowerBound = 3f,
+ sourceValueRangeUpperBound = 0f,
+ targetModifierRangeLowerBound = 1f,
+ targetModifierRangeUpperBound = 0.75f,
+ BrushBehavior.OutOfRange.CLAMP,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source.NORMALIZED_DIRECTION_Y,
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.45f,
+ sourceValueRangeUpperBound = 0.65f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.17f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 25L,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source
+ .INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ sourceValueRangeLowerBound = -80f,
+ sourceValueRangeUpperBound = -230f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.25f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 25L,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source.NORMALIZED_PRESSURE,
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.8f,
sourceValueRangeUpperBound = 1f,
- targetModifierRangeLowerBound = 0.05f,
- targetModifierRangeUpperBound = 1f,
- sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
- responseCurve = EasingFunction.Predefined.LINEAR,
- responseTimeMillis = 40L,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.5f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 30L,
enabledToolTypes = setOf(InputToolType.STYLUS),
),
)
- )
+ ),
+ inputModel = BrushFamily.SPRING_MODEL,
)
/**
@@ -156,17 +186,61 @@
BrushFamily(
tip =
BrushTip(
- scaleX = 0.05f,
+ scaleX = 0.25f,
scaleY = 1f,
- cornerRounding = 0.11f,
+ cornerRounding = 0.3f,
rotation = Angle.degreesToRadians(150f),
- behaviors = listOf(predictionFadeOutBehavior),
- )
+ behaviors =
+ listOf(
+ predictionFadeOutBehavior,
+ BrushBehavior(
+ BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.CORNER_ROUNDING_OFFSET,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 1f,
+ targetModifierRangeLowerBound = 0.3f,
+ targetModifierRangeUpperBound = 1f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 15L,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.CORNER_ROUNDING_OFFSET,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 1f,
+ targetModifierRangeLowerBound = 0.3f,
+ targetModifierRangeUpperBound = 1f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 15L,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.OPACITY_MULTIPLIER,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 3f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 15L,
+ ),
+ BrushBehavior(
+ BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.OPACITY_MULTIPLIER,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 3f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1f,
+ BrushBehavior.OutOfRange.CLAMP,
+ responseTimeMillis = 15L,
+ ),
+ ),
+ ),
+ inputModel = BrushFamily.SPRING_MODEL,
)
/**
- * Version 1 of a chisel-tip brush that is intended for highlighting text in a document (when
- * used with a translucent brush color).
+ * The latest version of a chisel-tip brush that is intended for highlighting text in a document
+ * (when used with a translucent brush color).
*
* The behavior of this [BrushFamily] may change in future releases, as it always points to the
* latest version of the pressure pen.
diff --git a/ink/ink-nativeloader/src/commonMain/kotlin/androidx/ink/nativeloader/UsedByNative.kt b/ink/ink-nativeloader/src/commonMain/kotlin/androidx/ink/nativeloader/UsedByNative.kt
index 7531e6b..cdd4106 100644
--- a/ink/ink-nativeloader/src/commonMain/kotlin/androidx/ink/nativeloader/UsedByNative.kt
+++ b/ink/ink-nativeloader/src/commonMain/kotlin/androidx/ink/nativeloader/UsedByNative.kt
@@ -24,7 +24,7 @@
* Use this to annotate methods, fields, and types that are referenced by name from native code to
* prevent them from being removed as unused.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
diff --git a/ink/ink-rendering/api/current.txt b/ink/ink-rendering/api/current.txt
index adc06c7..9635393 100644
--- a/ink/ink-rendering/api/current.txt
+++ b/ink/ink-rendering/api/current.txt
@@ -3,6 +3,11 @@
public interface CanvasStrokeRenderer {
method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, android.graphics.Matrix strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.Stroke stroke, android.graphics.Matrix strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.Stroke stroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform);
+ method @Px public default int strokeModifiedRegionOutsetPx();
field public static final androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion Companion;
}
diff --git a/ink/ink-rendering/api/restricted_current.txt b/ink/ink-rendering/api/restricted_current.txt
index e06360d..fa05dd5 100644
--- a/ink/ink-rendering/api/restricted_current.txt
+++ b/ink/ink-rendering/api/restricted_current.txt
@@ -3,6 +3,11 @@
public interface CanvasStrokeRenderer {
method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, android.graphics.Matrix strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.Stroke stroke, android.graphics.Matrix strokeToScreenTransform);
+ method public void draw(android.graphics.Canvas canvas, androidx.ink.strokes.Stroke stroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform);
+ method @Px public default int strokeModifiedRegionOutsetPx();
field public static final androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion Companion;
}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt
index 14c0c57..5bda1ea 100644
--- a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt
@@ -77,7 +77,8 @@
BrushFamily(BrushTip(opacityMultiplier = 1.0F)),
TestColors.COBALT_BLUE.withAlpha(0.4),
),
- INPUTS_TWIST,
+ // TODO: b/369408056 - change this back to INPUTS_TWIST
+ INPUTS_ZIGZAG,
),
),
Pair(
@@ -109,7 +110,8 @@
),
TestColors.RED,
),
- INPUTS_TWIST,
+ // TODO: b/369408056 - change this back to INPUTS_TWIST
+ INPUTS_ZIGZAG,
),
),
// TODO: b/330528190 - Add row for atlased textures
@@ -157,7 +159,8 @@
),
TestColors.AVOCADO_GREEN,
),
- INPUTS_TWIST,
+ // TODO: b/369408056 - change this back to INPUTS_TWIST
+ INPUTS_ZIGZAG,
),
),
// TODO: b/274461578 - Add row for winding textures
@@ -475,14 +478,6 @@
.addOrThrow(InputToolType.UNKNOWN, x = 5F, y = 90F, elapsedTimeMillis = 250)
.asImmutable()
- val INPUTS_TWIST =
- MutableStrokeInputBatch()
- .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 0F, elapsedTimeMillis = 100)
- .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 100F, elapsedTimeMillis = 150)
- .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 100F, elapsedTimeMillis = 200)
- .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 0F, elapsedTimeMillis = 250)
- .asImmutable()
-
fun brush(
family: BrushFamily = StockBrushes.markerLatest,
@ColorInt color: Int = TestColors.BLACK,
@@ -564,7 +559,8 @@
)
val paint = BrushPaint(listOf(textureLayer))
val brush = brush(BrushFamily(paint = paint), color, size = 30f)
- return finishedInProgressStroke(brush, INPUTS_TWIST)
+ // TODO: b/369408056 - change this back to INPUTS_TWIST
+ return finishedInProgressStroke(brush, INPUTS_ZIGZAG)
}
fun textureBlendedStroke(blendMode: BlendMode): InProgressStroke {
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt
index 3592f4b..d4647a3 100644
--- a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt
@@ -48,12 +48,13 @@
private inner class StrokeView(context: Context) : View(context) {
+ // TODO: b/369408056 - Change back to twist-style input points
private val inputs =
MutableStrokeInputBatch()
.addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 0F, elapsedTimeMillis = 100)
- .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 100F, elapsedTimeMillis = 150)
- .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 100F, elapsedTimeMillis = 200)
- .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 0F, elapsedTimeMillis = 250)
+ .addOrThrow(InputToolType.UNKNOWN, x = 40F, y = 40F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 70F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 30F, y = 100F, elapsedTimeMillis = 250)
.asImmutable()
// Pink twist stroke.
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt
index 80d188e..bcb4e99 100644
--- a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt
@@ -32,6 +32,39 @@
/**
* Renders strokes to a [Canvas].
*
+ * Instead of calling the [draw] methods here directly, it may be simpler to pass an instance of
+ * [CanvasStrokeRenderer] to [androidx.ink.rendering.android.view.ViewStrokeRenderer] and use it to
+ * calculate transform matrix values.
+ *
+ * An example of how to use [CanvasStrokeRenderer.draw] directly:
+ * ```
+ * class MyView {
+ * // Update these according to app business logic, and call `MyView.invalidate()`
+ * val worldToViewTransform = Matrix() // Call e.g. `setScale(2F)` to zoom in 2x
+ * val strokesWithTransforms = mutableMapOf<Stroke, Matrix>()
+ *
+ * private val strokeToViewTransform = Matrix() // reusable scratch object
+ * private val renderer = CanvasStrokeRenderer.create()
+ *
+ * fun onDraw(canvas: Canvas) {
+ * for ((stroke, strokeToWorldTransform) in strokesWithTransforms) {
+ * // Combine worldToViewTransform (drawing surface being panned/zoomed/rotated) with
+ * // strokeToWorldTransform (stroke itself being moved/scaled/rotated within the drawing
+ * // surface) to get the overall transform of this stroke.
+ * strokeToViewTransform.set(strokeToWorldTransform)
+ * strokeToViewTransform.postConcat(worldToViewTransform)
+ *
+ * canvas.withMatrix(strokeToViewTransform) {
+ * // If coordinates of MyView are scaled/rotated from screen coordinates, then those
+ * // scale/rotation values should be multiplied into the strokeToScreenTransform
+ * // argument to renderer.draw.
+ * renderer.draw(canvas, stroke, strokeToViewTransform)
+ * }
+ * }
+ * }
+ * }
+ * ```
+ *
* In almost all cases, a developer should use an implementation of this interface obtained from
* [CanvasStrokeRenderer.create].
*
@@ -64,7 +97,6 @@
* blurry or aliased.
*/
// TODO: b/353561141 - Reference ComposeStrokeRenderer above once implemented.
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun draw(canvas: Canvas, stroke: Stroke, strokeToScreenTransform: AffineTransform)
/**
@@ -80,7 +112,6 @@
* appear blurry or aliased.
*/
// TODO: b/353561141 - Reference ComposeStrokeRenderer above once implemented.
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun draw(canvas: Canvas, stroke: Stroke, strokeToScreenTransform: Matrix)
/**
@@ -92,7 +123,6 @@
* [canvas] during an app’s drawing logic. If this transform is inaccurate, strokes may appear
* blurry or aliased.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun draw(
canvas: Canvas,
inProgressStroke: InProgressStroke,
@@ -108,7 +138,6 @@
* the [canvas] during an app’s drawing logic. If this transform is inaccurate, strokes may
* appear blurry or aliased.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun draw(
canvas: Canvas,
inProgressStroke: InProgressStroke,
@@ -125,9 +154,7 @@
* lowest value that avoids the artifacts, as larger values will be less performant, and effects
* that rely on larger values will be less compatible with stroke geometry operations.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- @Px
- public fun strokeModifiedRegionOutsetPx(): Int = 3
+ @Px public fun strokeModifiedRegionOutsetPx(): Int = 3
public companion object {
diff --git a/input/input-motionprediction/api/1.0.0-beta05.txt b/input/input-motionprediction/api/1.0.0-beta05.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/1.0.0-beta05.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/api/res-1.0.0-beta05.txt b/input/input-motionprediction/api/res-1.0.0-beta05.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/input/input-motionprediction/api/res-1.0.0-beta05.txt
diff --git a/input/input-motionprediction/api/restricted_1.0.0-beta05.txt b/input/input-motionprediction/api/restricted_1.0.0-beta05.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/restricted_1.0.0-beta05.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
index 5ad7111..0d7627e 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
@@ -72,11 +72,11 @@
private final DVector2 mLastPosition = new DVector2();
private long mLastSeenEventTime;
- private long mLastPredictEventTime;
+ private double mLastPredictEventTime;
private long mDownEventTime;
- private List<Float> mReportRates = new LinkedList<>();
+ private List<Double> mReportRates = new LinkedList<>();
private int mExpectedPredictionSampleSize = -1;
- private float mReportRateMs = 0;
+ private double mReportRateMs = 0;
private final DVector2 mPosition = new DVector2();
private final DVector2 mVelocity = new DVector2();
@@ -141,10 +141,10 @@
// to be used as an estimate.
if (mReportRates != null && mReportRates.size() < 20) {
if (mLastSeenEventTime > 0) {
- float dt = eventTime - mLastSeenEventTime;
+ double dt = eventTime - mLastSeenEventTime;
mReportRates.add(dt);
- float sum = 0;
- for (float rate : mReportRates) {
+ double sum = 0;
+ for (double rate : mReportRates) {
sum += rate;
}
mReportRateMs = sum / mReportRates.size();
@@ -250,16 +250,6 @@
int predictionTargetInSamples =
(int) Math.ceil(predictionTargetMs / mReportRateMs * confidenceFactor);
- // Predict at least as far in time as the previous prediction.
- // Otherwise, it may appear that the coordinates are going backwards.
- if (mLastPredictEventTime > mLastSeenEventTime) {
- int minimumPredictionSampleSize = (int) Math.floor(
- (mLastPredictEventTime - mLastSeenEventTime) / mReportRateMs
- );
- if (predictionTargetInSamples < minimumPredictionSampleSize) {
- predictionTargetInSamples = minimumPredictionSampleSize;
- }
- }
if (mExpectedPredictionSampleSize != -1) {
// Normally this should always be false as confidenceFactor should be less than 1.0
if (predictionTargetInSamples > mExpectedPredictionSampleSize) {
@@ -272,9 +262,11 @@
predictionTargetInSamples = Math.max(predictionTargetInSamples, 1);
}
- long predictedEventTime = mLastSeenEventTime;
- int i = 0;
- for (; i < predictionTargetInSamples; i++) {
+ double predictedEventTime = mLastSeenEventTime;
+ double nextPredictedEventTime = mLastSeenEventTime + mReportRateMs;
+ for (int i = 0;
+ i < predictionTargetInSamples || (nextPredictedEventTime <= mLastPredictEventTime);
+ i++) {
mAcceleration.a1 += mJank.a1 * JANK_INFLUENCE;
mAcceleration.a2 += mJank.a2 * JANK_INFLUENCE;
mVelocity.a1 += mAcceleration.a1 * ACCELERATION_INFLUENCE;
@@ -290,8 +282,6 @@
mPressure = 1;
}
- long nextPredictedEventTime = predictedEventTime + Math.round(mReportRateMs);
-
// Abort prediction if the pen is to be lifted.
if (mPredictLift
&& mPressure < 0.1
@@ -310,7 +300,7 @@
predictedEvent =
MotionEvent.obtain(
mDownEventTime /* downTime */,
- nextPredictedEventTime /* eventTime */,
+ (long) nextPredictedEventTime /* eventTime */,
MotionEvent.ACTION_MOVE /* action */,
1 /* pointerCount */,
pointerProperties /* pointer properties */,
@@ -324,9 +314,13 @@
0 /* source */,
0 /* flags */);
} else {
- predictedEvent.addBatch(nextPredictedEventTime, coords, 0);
+ predictedEvent.addBatch((long) nextPredictedEventTime, coords, 0);
}
+ // Keep track of the last predicted time
predictedEventTime = nextPredictedEventTime;
+
+ // Prepare for next iteration
+ nextPredictedEventTime += mReportRateMs;
}
// Store the last predicted time
diff --git a/libraryversions.toml b/libraryversions.toml
index 62d780a..b46cc85 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -15,6 +15,7 @@
CAMERA = "1.5.0-alpha02"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
+CAMERA_MEDIA3 = "1.0.0-alpha01"
CAMERA_VIEWFINDER = "1.4.0-alpha09"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-beta02"
@@ -70,7 +71,7 @@
GRAPHICS_PATH = "1.0.0-rc01"
GRAPHICS_SHAPES = "1.0.0-rc01"
GRIDLAYOUT = "1.1.0-beta02"
-HEALTH_CONNECT = "1.1.0-alpha09"
+HEALTH_CONNECT = "1.1.0-alpha10"
HEALTH_CONNECT_TESTING_QUARANTINE = "1.0.0-alpha01"
HEALTH_SERVICES_CLIENT = "1.1.0-alpha03"
HEIFWRITER = "1.1.0-alpha03"
@@ -78,7 +79,7 @@
HILT_NAVIGATION = "1.2.0-rc01"
HILT_NAVIGATION_COMPOSE = "1.2.0-rc01"
INK = "1.0.0-alpha01"
-INPUT_MOTIONPREDICTION = "1.0.0-beta04"
+INPUT_MOTIONPREDICTION = "1.0.0-beta05"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
JAVASCRIPTENGINE = "1.0.0-beta01"
@@ -154,8 +155,8 @@
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.2.0-alpha01"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.5.0-alpha02"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha25"
+WEAR_COMPOSE = "1.5.0-alpha03"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha26"
WEAR_CORE = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
@@ -163,7 +164,7 @@
WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
WEAR_PROTOLAYOUT = "1.3.0-alpha01"
WEAR_PROTOLAYOUT_MATERIAL3 = "1.0.0-alpha01"
-WEAR_REMOTE_INTERACTIONS = "1.1.0-beta01"
+WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
WEAR_TILES = "1.5.0-alpha01"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
WEAR_WATCHFACE = "1.3.0-alpha04"
@@ -193,6 +194,7 @@
CAMERA = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA" }
CAMERA_PIPE = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA_PIPE", overrideInclude = [ ":camera:camera-camera2-pipe", ":camera:camera-camera2-pipe-integration" ] }
CAMERA_TESTING = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA_TESTING", overrideInclude = [ ":camera:camera-testing" ] }
+CAMERA_MEDIA3 = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA_MEDIA3", overrideInclude = [ ":camera:camera-media3-effect" ] }
CAMERA_VIEWFINDER = { group = "androidx.camera.viewfinder", atomicGroupVersion = "versions.CAMERA_VIEWFINDER" }
CARDVIEW = { group = "androidx.cardview", atomicGroupVersion = "versions.CARDVIEW" }
CAR_APP = { group = "androidx.car.app", atomicGroupVersion = "versions.CAR_APP" }
diff --git a/navigation/integration-tests/testapp/build.gradle b/navigation/integration-tests/testapp/build.gradle
index 030aecf..a3b7ee4 100644
--- a/navigation/integration-tests/testapp/build.gradle
+++ b/navigation/integration-tests/testapp/build.gradle
@@ -22,11 +22,22 @@
dependencies {
implementation(libs.kotlinStdlib)
+ implementation(project(":navigation:navigation-common"))
+ implementation(project(":navigation:navigation-fragment"))
+ implementation(project(":navigation:navigation-runtime"))
+ implementation(project(":navigation:navigation-ui"))
implementation("androidx.appcompat:appcompat:1.1.0")
- api(project(":fragment:fragment-ktx"))
- api(project(":transition:transition-ktx"))
- implementation(project(":navigation:navigation-fragment-ktx"))
- implementation(project(":navigation:navigation-ui-ktx"))
+ implementation("androidx.cardview:cardview:1.0.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.0.1")
+ implementation("androidx.core:core:1.13.0")
+ implementation("androidx.customview:customview:1.1.0")
+ implementation("androidx.drawerlayout:drawerlayout:1.1.1")
+ implementation("androidx.fragment:fragment:1.8.3")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
+ implementation("androidx.recyclerview:recyclerview:1.1.0")
+ implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
+ implementation("androidx.transition:transition:1.5.1")
+ implementation("com.google.android.material:material:1.4.0")
implementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
diff --git a/navigation/navigation-benchmark/build.gradle b/navigation/navigation-benchmark/build.gradle
index ef9ee24..a5a0894 100644
--- a/navigation/navigation-benchmark/build.gradle
+++ b/navigation/navigation-benchmark/build.gradle
@@ -31,14 +31,15 @@
}
dependencies {
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":navigation:navigation-runtime"))
+ androidTestImplementation(project(":benchmark:benchmark-common"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":internal-testutils-navigation"))
+ androidTestImplementation(project(":navigation:navigation-common"))
+ androidTestImplementation(project(":navigation:navigation-runtime"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/navigation/navigation-common-lint/build.gradle b/navigation/navigation-common-lint/build.gradle
index b913ca6..46b47fd 100644
--- a/navigation/navigation-common-lint/build.gradle
+++ b/navigation/navigation-common-lint/build.gradle
@@ -33,11 +33,14 @@
BundleInsideHelper.forInsideLintJar(project)
dependencies {
+ compileOnly(libs.kotlinCompiler)
compileOnly(libs.kotlinStdlib)
compileOnly(libs.androidLintPrevApi)
+ compileOnly(libs.intellijCore)
+ compileOnly(libs.uast)
+
bundleInside(project(":navigation:navigation-lint-common"))
- testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLintPrev)
testImplementation(libs.androidLintPrevTests)
testImplementation(libs.junit)
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/TypeSafeDestinationMissingAnnotationDetector.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/TypeSafeDestinationMissingAnnotationDetector.kt
index 5a2f53c..9afee7f 100644
--- a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/TypeSafeDestinationMissingAnnotationDetector.kt
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/TypeSafeDestinationMissingAnnotationDetector.kt
@@ -29,7 +29,7 @@
*/
class TypeSafeDestinationMissingAnnotationDetector :
BaseTypeSafeDestinationMissingAnnotationDetector(
- methodNames = listOf("navigation", "deepLink"),
+ methodNames = listOf("navigation", "deepLink", "setUriPattern", "navDeepLink"),
constructorNames =
listOf(
"androidx.navigation.NavDestinationBuilder",
diff --git a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingKeepAnnotationDetectorTest.kt b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingKeepAnnotationDetectorTest.kt
index cc7d1b4..792e312 100644
--- a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingKeepAnnotationDetectorTest.kt
+++ b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingKeepAnnotationDetectorTest.kt
@@ -18,6 +18,7 @@
import androidx.navigation.lint.common.KEEP_ANNOTATION
import androidx.navigation.lint.common.NAVIGATION_STUBS
+import androidx.navigation.lint.common.NAV_DEEP_LINK
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
@@ -339,5 +340,132 @@
)
}
+ @Test
+ fun testDeeplinkBuilderSetUriPattern_noError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+ import androidx.annotation.Keep
+
+ @Keep enum class TestEnum { ONE, TWO }
+ class DeepLink(val arg: TestEnum)
+
+ fun navigation() {
+ val builder = NavDeepLink.Builder()
+ builder.setUriPattern<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testDeeplinkBuilderSetUriPattern_hasError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ enum class TestEnum { ONE, TWO }
+ class DeepLink(val arg: TestEnum)
+
+ fun navigation() {
+ val builder = NavDeepLink.Builder()
+ builder.setUriPattern<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expect(
+ """
+src/com/example/TestEnum.kt:5: Warning: To prevent this Enum's serializer from being obfuscated in minified builds, annotate it with @androidx.annotation.Keep [MissingKeepAnnotation]
+enum class TestEnum { ONE, TWO }
+ ~~~~~~~~
+0 errors, 1 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testNavDeepLink_noError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+ import androidx.annotation.Keep
+
+ @Keep enum class TestEnum { ONE, TWO }
+ class DeepLink(val arg: TestEnum)
+
+ class DeepLink
+
+ fun navigation() {
+ navDeepLink<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testNavDeepLink_hasError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ enum class TestEnum { ONE, TWO }
+ class DeepLink(val arg: TestEnum)
+
+ fun navigation() {
+ navDeepLink<DeepLink>()
+
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expect(
+ """
+src/com/example/TestEnum.kt:5: Warning: To prevent this Enum's serializer from being obfuscated in minified builds, annotate it with @androidx.annotation.Keep [MissingKeepAnnotation]
+enum class TestEnum { ONE, TWO }
+ ~~~~~~~~
+0 errors, 1 warnings
+ """
+ .trimIndent()
+ )
+ }
+
val STUBS = arrayOf(*NAVIGATION_STUBS, KEEP_ANNOTATION)
}
diff --git a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingSerializableAnnotationDetectorTest.kt b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingSerializableAnnotationDetectorTest.kt
index 32071da..8166828 100644
--- a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingSerializableAnnotationDetectorTest.kt
+++ b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/MissingSerializableAnnotationDetectorTest.kt
@@ -18,14 +18,19 @@
import androidx.navigation.lint.common.K_SERIALIZER
import androidx.navigation.lint.common.NAVIGATION_STUBS
+import androidx.navigation.lint.common.NAV_DEEP_LINK
import androidx.navigation.lint.common.SERIALIZABLE_ANNOTATION
import androidx.navigation.lint.common.SERIALIZABLE_TEST_CLASS
import androidx.navigation.lint.common.TEST_CLASS
+import androidx.navigation.lint.common.bytecodeStub
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+@RunWith(JUnit4::class)
class MissingSerializableAnnotationDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = TypeSafeDestinationMissingAnnotationDetector()
@@ -580,5 +585,193 @@
)
}
+ @Test
+ fun testDeeplinkBuilderSetUriPattern_noError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+ import kotlinx.serialization.Serializable
+
+ @Serializable class DeepLink
+
+ fun navigation() {
+ val builder = NavDeepLink.Builder()
+ builder.setUriPattern<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testDeeplinkBuilderSetUriPattern_hasError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ class DeepLink
+
+ fun navigation() {
+ val builder = NavDeepLink.Builder()
+ builder.setUriPattern<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expect(
+ """
+src/com/example/DeepLink.kt:5: Error: To use this class or object as a type-safe destination, annotate it with @Serializable [MissingSerializableAnnotation]
+class DeepLink
+ ~~~~~~~~
+1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testNavDeepLink_noError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+ import kotlinx.serialization.Serializable
+
+ @Serializable class DeepLink
+
+ fun navigation() {
+ navDeepLink<DeepLink>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testNavDeepLink_hasError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.*
+
+ class DeepLink
+
+ fun navigation() {
+ navDeepLink<DeepLink>()
+
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ NAV_DEEP_LINK
+ )
+ .run()
+ .expect(
+ """
+src/com/example/DeepLink.kt:5: Error: To use this class or object as a type-safe destination, annotate it with @Serializable [MissingSerializableAnnotation]
+class DeepLink
+ ~~~~~~~~
+1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testWrongPackage_noError() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.navigation.NavGraphBuilder
+ import com.test.navigation
+ import kotlinx.serialization.*
+
+ @Serializable object TestGraph
+ object TestClass
+
+ fun navigation() {
+ val builder = NavGraphBuilder(route = TestGraph::class)
+
+ builder.navigation<TestClass>()
+ }
+ """
+ )
+ .indented(),
+ *STUBS,
+ CUSTOM_NAV_GRAPH_BUILDER_EXTENSIONS
+ )
+ .run()
+ .expectClean()
+ }
+
+ private val CUSTOM_NAV_GRAPH_BUILDER_EXTENSIONS =
+ bytecodeStub(
+ "NavGraphBuilderNavigation.kt",
+ "com/test",
+ 0x8c23ef1e,
+ """
+package com.test
+
+import androidx.navigation.NavGraphBuilder
+
+// NavGraphBuilder
+inline fun <reified T : Any> NavGraphBuilder.navigation() { }
+ """,
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgsuUSTsxLKcrPTKnQy0ssy0xPLMnM
+ zxPicsyrLMnIzEv3LhHi90ssc87PKynKz8lJLQIKcAIFPPKLS7xLuKS4uJPz
+ c/VSKxJzC3JShbhDUotL3IsSCzKAcupcHCC5EqCQkDRQC1jcqTQzJyW1yA9u
+ lXeJEoMWAwA5gn4YnAAAAA==
+ """,
+ """
+ com/test/NavGraphBuilderNavigationKt.class:
+ H4sIAAAAAAAA/41RTW/TQBB966SJYwpNU1qSUgoUl6Y94BT11IZIgARYpAGR
+ KJecNrZJNrHXyN5EPfbE/+GGOKCKIz8KMWsqKEVIlbwzb94+z9d+//HlK4AD
+ 7DLYXhw5KkiV0+Hzlwn/MH42E6EfJBSKEVcilq9VEYyhPOFz7oRcjpw3w0ng
+ EZtjsORvHUO93ubST2Lhnzh/+MuZj3b7DK1m77B9OeNR6+oJCk01FmnLhMmw
+ OY1VKKQzmUeOkCpIJA8dV6pEyFR4aREWw6o3DrxpJ1adWRi+5QmPAhIy7NT/
+ beMC09VJRlRxEYu4buEabjAs2cJ+b1+cnLm0IFt39Be9faVxGJbb5xMcB4r7
+ XHHijGieo1di2pS0AZWZamDQ5YnQqEHI32c4ODstW2enllE2fjltqsZ6jcC6
+ 0WBblkkKo8r2jEaOTv7Vt4+m/vcxy9L2GDb++/6Ppooh/zz2A5q8LWTQmUXD
+ IOnxYUhMpR17POzzROj4nCx1xUhyNUsIW914lnjBC6Evau9mUoko6ItUkPKp
+ lLHKiqTYh4E8sjnLNSygQPEDip6QN/QO9iqlz1jKNT/pFcAmW6DeTRSxTXiN
+ OJPiMpbJkhwVrJB/mKmLdHYytIU6+UPS3KQiqwPkXKy5uOWiipqLddx2sYE7
+ A7AUm7g7wEKqv3sp7md25Se3iEt3PgMAAA==
+ """
+ )
+
val STUBS = arrayOf(*NAVIGATION_STUBS, SERIALIZABLE_ANNOTATION, K_SERIALIZER)
}
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index e76f7a7..52382c7 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -46,32 +46,34 @@
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
api("androidx.savedstate:savedstate-ktx:1.2.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
+ api(libs.kotlinStdlib)
+
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.collection:collection-ktx:1.4.2")
implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation(libs.kotlinSerializationCore)
- api(libs.kotlinStdlib)
testImplementation(project(":navigation:navigation-testing"))
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
testImplementation(libs.truth)
- testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.kotlinCoroutinesCore)
testImplementation(libs.kotlinCoroutinesTest)
testImplementation(libs.kotlinTest)
- androidTestImplementation(libs.kotlinTestJunit)
+ androidTestRuntimeOnly(libs.kotlinTestJunit)
+ androidTestRuntimeOnly(libs.testCore)
+
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.kotlinStdlib)
- lintPublish(project(':navigation:navigation-common-lint'))
+ lintPublish(project(":navigation:navigation-common-lint"))
}
androidx {
diff --git a/navigation/navigation-compose-lint/build.gradle b/navigation/navigation-compose-lint/build.gradle
index 6c48c02..d321692 100644
--- a/navigation/navigation-compose-lint/build.gradle
+++ b/navigation/navigation-compose-lint/build.gradle
@@ -34,17 +34,19 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
+ compileOnly(libs.intellijCore)
+ compileOnly(libs.uast)
bundleInside(project(":compose:lint:common"))
bundleInside(project(":navigation:navigation-lint-common"))
+ testRuntimeOnly(libs.kotlinReflect)
+
testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
- testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
- testImplementation(libs.androidLintPrev)
+ testImplementation(libs.androidLintPrevApi)
testImplementation(libs.androidLintPrevTests)
testImplementation(libs.junit)
- testImplementation(libs.truth)
}
androidx {
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index af843ef..1d733ed 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -27,37 +27,62 @@
}
dependencies {
-
- implementation(libs.kotlinStdlib)
+ api(libs.kotlinStdlib)
api("androidx.activity:activity-compose:1.8.0")
- api("androidx.compose.animation:animation:1.7.0-rc01")
- implementation("androidx.compose.foundation:foundation-layout:1.7.0-rc01")
- api("androidx.compose.runtime:runtime:1.7.0-rc01")
- api("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
- api("androidx.compose.ui:ui:1.7.0-rc01")
- api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
- api(project(":navigation:navigation-runtime-ktx"))
+ api("androidx.compose.animation:animation:1.7.2")
+ api("androidx.compose.runtime:runtime:1.7.2")
+ api("androidx.compose.runtime:runtime-saveable:1.7.2")
+ api("androidx.compose.ui:ui:1.7.2")
+ api("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
+ api(project(":navigation:navigation-runtime"))
+ api(project(":navigation:navigation-common"))
+
+ implementation("androidx.activity:activity:1.8.0")
+ implementation("androidx.annotation:annotation:1.8.0")
+ implementation("androidx.compose.animation:animation-core:1.7.2")
+ implementation("androidx.compose.foundation:foundation-layout:1.7.2")
+ implementation("androidx.lifecycle:lifecycle-common:2.8.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.2")
+ implementation(libs.kotlinCoroutinesCore)
implementation(libs.kotlinSerializationCore)
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation project(":compose:test-utils")
- androidTestImplementation project(":compose:ui:ui-tooling")
+ androidTestImplementation("androidx.activity:activity:1.9.2")
+ androidTestImplementation("androidx.collection:collection-ktx:1.4.2")
+ androidTestImplementation("androidx.core:core-ktx:1.13.0")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel:2.8.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2")
+ androidTestImplementation("androidx.savedstate:savedstate:1.2.1")
androidTestImplementation(project(":navigation:navigation-testing"))
androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":lifecycle:lifecycle-common"))
- androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
- androidTestImplementation(project(":lifecycle:lifecycle-livedata-core"))
- androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
- androidTestImplementation(project(":activity:activity-ktx"))
- androidTestImplementation("androidx.collection:collection-ktx:1.4.2")
+ androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
+ // Compose test dependencies
+ androidTestImplementation(project(":compose:animation:animation"))
+ androidTestImplementation(project(":compose:animation:animation-core"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:runtime:runtime-saveable"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-graphics"))
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-text"))
+ androidTestImplementation(project(":compose:ui:ui-tooling-preview"))
+ androidTestImplementation(project(":compose:ui:ui-unit"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-tooling"))
+
lintChecks(project(":navigation:navigation-compose-lint"))
lintPublish(project(":navigation:navigation-compose-lint"))
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 41d1a1a..839752a 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -36,11 +36,21 @@
implementation(libs.kotlinSerializationCore)
implementation(project(":compose:integration-tests:demos:common"))
+ implementation(project(":compose:animation:animation"))
implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:material:material"))
- implementation("androidx.compose.material:material-icons-core:1.6.7")
+ implementation(project(":compose:runtime:runtime-saveable"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":navigation:navigation-common"))
+ implementation(project(":navigation:navigation-runtime"))
implementation(project(":navigation:navigation-compose"))
implementation(project(":navigation:navigation-compose:navigation-compose-samples"))
+ implementation("androidx.compose.material:material-icons-core:1.7.2")
}
androidx {
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index 7cdaa05..94e0951 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -36,17 +36,28 @@
}
dependencies {
- implementation(libs.kotlinStdlib)
+ api(libs.kotlinStdlib)
compileOnly(project(":annotation:annotation-sampled"))
implementation(project(":compose:animation:animation"))
- implementation("androidx.compose.foundation:foundation:1.0.1")
- implementation("androidx.compose.ui:ui-tooling:1.4.0")
+ implementation(project(":compose:animation:animation-core"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime-saveable"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":compose:ui:ui-tooling-preview"))
+ implementation(project(":compose:ui:ui-unit"))
implementation(project(":navigation:navigation-compose"))
+ implementation(project(":navigation:navigation-common"))
+ implementation(project(":navigation:navigation-runtime"))
implementation("androidx.compose.material:material:1.0.1")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
- implementation("androidx.savedstate:savedstate-ktx:1.2.1")
+ implementation("androidx.compose.ui:ui-tooling:1.4.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
+ implementation(libs.kotlinSerializationCore)
+ implementation(libs.kotlinSerializationJson)
}
androidx {
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index d294b3c..5882bd7 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -34,26 +34,30 @@
}
dependencies {
+ api("androidx.fragment:fragment:1.6.2")
api(project(":navigation:navigation-dynamic-features-runtime"))
+ api(project(":navigation:navigation-common"))
api(project(":navigation:navigation-fragment"))
+ api(project(":navigation:navigation-runtime"))
+ api(libs.playFeatureDelivery)
api(libs.kotlinStdlib)
+
implementation(libs.kotlinSerializationCore)
+ implementation("androidx.activity:activity:1.7.2")
+ implementation("androidx.core:core-ktx:1.2.0")
+ implementation("androidx.lifecycle:lifecycle-common:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-livedata-core:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
- testImplementation(libs.testCore)
- testImplementation(libs.testExtJunit)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.mockitoCore4)
- testImplementation(libs.robolectric)
- testImplementation(libs.truth)
+ testRuntimeOnly(libs.testCore)
+ testRuntimeOnly(libs.testRunner)
+ testRuntimeOnly(libs.robolectric)
+ androidTestImplementation(libs.junit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index ba25181..d5f44ab 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -35,28 +35,33 @@
}
dependencies {
- api(project(":navigation:navigation-runtime"))
api(libs.playFeatureDelivery)
+ api("androidx.lifecycle:lifecycle-livedata-core:2.6.2")
+ api(project(":navigation:navigation-common"))
+ api(project(":navigation:navigation-runtime"))
+
+ implementation("androidx.core:core-ktx:1.2.0")
implementation(libs.kotlinSerializationCore)
+ testRuntimeOnly(libs.testCore)
+ testRuntimeOnly(libs.testRunner)
+ testRuntimeOnly(libs.robolectric)
+
testImplementation(project(":navigation:navigation-testing"))
testImplementation("androidx.arch.core:core-testing:2.2.0")
- testImplementation(libs.testCore)
- testImplementation(libs.testExtJunit)
- testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
- testImplementation(libs.robolectric)
testImplementation(libs.truth)
+ testImplementation(libs.kotlinCoroutinesCore)
testImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation("androidx.annotation:annotation:1.7.0")
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-fragment-compose/build.gradle b/navigation/navigation-fragment-compose/build.gradle
index 415e042..bbf7940 100644
--- a/navigation/navigation-fragment-compose/build.gradle
+++ b/navigation/navigation-fragment-compose/build.gradle
@@ -33,16 +33,31 @@
}
dependencies {
- implementation(libs.kotlinStdlib)
- api(project(":navigation:navigation-fragment"))
api("androidx.compose.runtime:runtime:1.5.4")
api("androidx.compose.ui:ui:1.5.4")
+ api("androidx.fragment:fragment:1.6.2")
+ api(project(":navigation:navigation-common"))
+ api(project(":navigation:navigation-fragment"))
+ api(project(":navigation:navigation-runtime"))
- androidTestImplementation project(":compose:ui:ui-test-junit4")
- androidTestImplementation project(":compose:material:material")
- androidTestImplementation project(":compose:test-utils")
- androidTestImplementation(libs.testRules)
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.annotation:annotation:1.5.0")
+ implementation("androidx.core:core-ktx:1.2.0")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
+
+ androidTestRuntimeOnly(project(":compose:test-utils"))
+
+ androidTestImplementation("androidx.activity:activity:1.7.2")
+ androidTestImplementation("androidx.fragment:fragment-ktx:1.6.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel:2.8.2")
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-text"))
androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
}
diff --git a/navigation/navigation-fragment-ktx/build.gradle b/navigation/navigation-fragment-ktx/build.gradle
index b1199e7..db88f98 100644
--- a/navigation/navigation-fragment-ktx/build.gradle
+++ b/navigation/navigation-fragment-ktx/build.gradle
@@ -31,9 +31,6 @@
dependencies {
api(project(":navigation:navigation-fragment"))
- api(project(":navigation:navigation-runtime-ktx")) {
- because 'Mirror navigation-fragment dependency graph for -ktx artifacts'
- }
}
androidx {
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 6c5a89a..0f7a785 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -34,14 +34,29 @@
dependencies {
api("androidx.fragment:fragment-ktx:1.6.2")
- api(project(":navigation:navigation-runtime"))
api("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
+ api(project(":navigation:navigation-common"))
+ api(project(":navigation:navigation-runtime"))
api(libs.kotlinStdlib)
+
implementation(libs.kotlinSerializationCore)
+ implementation(libs.kotlinCoroutinesCore)
+ implementation("androidx.activity:activity:1.7.2")
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.lifecycle:lifecycle-common:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-livedata-core:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
+ implementation("androidx.savedstate:savedstate:1.2.1")
+
androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation("androidx.annotation:annotation:1.7.0")
androidTestImplementation("androidx.fragment:fragment-testing:1.6.2")
+ androidTestImplementation("androidx.fragment:fragment-testing-manifest:1.6.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
+ androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testMonitor)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/BaseTypeSafeDestinationMissingAnnotationDetector.kt b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/BaseTypeSafeDestinationMissingAnnotationDetector.kt
index bd4765c..ba90077 100644
--- a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/BaseTypeSafeDestinationMissingAnnotationDetector.kt
+++ b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/BaseTypeSafeDestinationMissingAnnotationDetector.kt
@@ -25,6 +25,7 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.source.PsiClassReferenceType
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
@@ -57,15 +58,27 @@
node: UCallExpression,
method: PsiMethod
) {
- val receiver = node.receiver?.getExpressionType()?.canonicalText ?: return
- // get the destination Type
+ val packageName = (method.containingFile as? PsiClassOwner)?.packageName
+ if (
+ packageName != NAVIGATION_PACKAGE_NAME && packageName != NAVIGATION_COMPOSE_PACKAGE_NAME
+ )
+ return
+
+ val receiver = node.receiver?.getExpressionType()?.canonicalText
+
+ // Get the destination Type, but we only want to lint routes from safe args apis.
+ // Such routes come in two possible forms:
+ // 1. route as regular parameter specifically for NavigatorProvider.navigation
+ // 2. reified type parameter for all other destination builder apis
val kClazzType =
when {
// route as parameter
node.methodName == "navigation" &&
receiver == "androidx.navigation.NavigatorProvider" -> node.getRouteKClassType()
// route as reified Type
- else -> (node.typeArguments.first() as? PsiClassReferenceType)?.resolve()
+ node.typeArguments.isNotEmpty() ->
+ (node.typeArguments.first() as? PsiClassReferenceType)?.resolve()
+ else -> null
} ?: return
checkMissingSerializableAnnotation(kClazzType, context)
@@ -135,6 +148,10 @@
}
}
+val NAVIGATION_PACKAGE_NAME = "androidx.navigation"
+
+val NAVIGATION_COMPOSE_PACKAGE_NAME = "androidx.navigation.compose"
+
fun createMissingSerializableAnnotationIssue(
detectorClass: Class<out BaseTypeSafeDestinationMissingAnnotationDetector>
) =
diff --git a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/TestStub.kt b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/TestStub.kt
index ae79751..3ae4392 100644
--- a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/TestStub.kt
+++ b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/TestStub.kt
@@ -410,6 +410,72 @@
"""
)
+val NAV_DEEP_LINK =
+ bytecodeStub(
+ "NavDeeplink.kt",
+ "androidx/navigation",
+ 0x73e1868d,
+ """
+package androidx.navigation
+
+class NavDeepLink {
+ class Builder {
+ inline fun <reified T : Any> setUriPattern() {}
+ }
+}
+
+inline fun <reified T : Any> navDeepLink(): NavDeepLink = NavDeepLink()
+""",
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijg8uESTsxLKcrPTKnQy0ssy0xPLMnM
+ zxPicsyrLMnIzEv3LhHi90ssc87PKynKz8lJLQIK8AIFXFJTC3Iy87KBXE4g
+ 1yO/uMS7hEuSizs5P1cvtSIxtyAnVYgLpMoHrEqJQYsBAHVeaUuBAAAA
+ """,
+ """
+ androidx/navigation/NavDeepLink$Builder.class:
+ H4sIAAAAAAAA/41RTW/TQBB9u86nm7ZOoJCUj0IJlPaA0woJiZYiWoRwFQqi
+ IZecNvEStknWyN5EPebED+EfcELigKIe+VGIWTcXegEfZt58vDc7nl+/f/wE
+ 8BgbDBtCh3GkwjNfi4nqC6Mi7R+LyUspPzeVHtQPxmoYyjgPxuCdionwh0L3
+ /bfdU9kzeTgMuT2lldlncB5utkvIIucigzxDxnxSCcNm8z9n7DIsJtJ8iNU7
+ YYyMNUNtr/W0eXnq7j4NYqjUVf1j/RKBBQzl5iAyQ6X9N9KIUBhBwnw0cWhn
+ Zk3RGlDvgPJnykYNQuE2w/PZtOLyKne5N5u6vGABd2fT6my6wxvsIHv+NUeZ
+ oxXPWeWNzLpbmE29bJVtUfH1+ZeCldlhqXiLYe0fi+dxkyE/355haV6ipw8e
+ DQz9wMMolAzL1CuPx6OujFuiO6RMpRn1xLAtYmXjebJ4ovpamHFMuBRoLePD
+ oUgSSSdwT6Jx3JOvlO2rvR9ro0ayrRJFxBdaRyZ9WoJtcDqd/TitQJcke4ci
+ 3y5EPrv1HYVvafku2dxFEutkS3NchAt4ZSxQlafkJ1Th5B1n728qJ2up1wjZ
+ eYtYSkWcCxHcS7tLFNcJuSnjFm6jhvtpZQ0PyD+j/DIN9jpwApQDVAJcwVWC
+ WAlI+3oHLEEVtQ5yCdwEq4kFCym48QehdXScDAMAAA==
+ """,
+ """
+ androidx/navigation/NavDeepLink.class:
+ H4sIAAAAAAAA/4VQTU/bQBB9s05sYlwItIXQLz5UCdpDHVAlJIqQgKqSpZRK
+ Lcolp028oouddeXdRBzzW/oPeqrUQxX1yI9CHYfcubyZ9/bN6s3c3v35C+A9
+ tgmb0qRlodOb2MixvpJOFya+kOOPSv3oaJMFIELzWo5lnEtzFX/pX6uBC+AR
+ /GNttDsheHtvuhHq8EPUEBBq7ru2hO3OA39/IKx0ssLl2sSflZOpdJI1MRx7
+ HI8qaFQAAmWs3+iKtblL9wk700kUipYIRXM6CcWCaE0nB6JNR+Sd1f/99EVT
+ VM4DquaDs5HOU1USdh8I9XruDNAiLM0fOGH2LnO82nmRKsIyO9XFaNhX5aXs
+ 56ysdoqBzLuy1BWfi1FijCrPc2mt4oOE34pROVCfdPW28XVknB6qrraazafG
+ FG4WxmIfgg85X7y6K+NzZvGMA/W3v7HwixuBF4z+TKzhJWN0b0ADIVcPrxhD
+ 1jbYu864OZt6hi2uh6wvsjfqwUvwKMFSgmU0ucVKglU87oEsnuBpDzWL0GLN
+ wrdY/w9v8crnPwIAAA==
+ """,
+ """
+ androidx/navigation/NavDeeplinkKt.class:
+ H4sIAAAAAAAA/41SW2sTQRT+zmyayza222o1qZfaNpVWxE1FEE0piCIubivY
+ EJA8TbLbME0yK7uT0Mc8+Xt8ExQk+OiPEs+sPqgP2oc5851zvnOd+fb90xcA
+ D3GHsCl1lCYqOve1nKqBNCrR/rGcPo/jdyOlh69MCUTwzuRU+iOpB/7r3lnc
+ Z6tDWNQ/iSETCdu7e+E/sllSi/D4oP0k/Dtb6/BCsRv/oZRQJhQPlFbmkODs
+ 7nWqcLHoooIqz9BQjdPGHy1TQFgJh4nhUf2j2MhIGsmFxHjq8IbIiooVYO7Q
+ AsHOc2VRk1G0T7g3n1Xd+cwVNZFf3ny2vmaFaNKWW57PPFGju6LpvPz6vmxj
+ HvDWLzAstcmWXfrtMe4PDaHwLIliwjLT4uPJuBenbdkbsWU1TPpy1JGpsvov
+ Y+VEDbQ0k5Sxe5JM0n78QllH/c1EGzWOOypTzHyqdWLyNjLsQ6CAfGqvjgUU
+ Wb+dfxjBDQGu0/qMytuPuPTBbgSbLIvsEbzsLcbVHJexhGXWtnNOiU8jRxvY
+ 4fsRczzOvdKFE2A1wOUAV7AW4CquBaih3gVlWMf1LgoZFjLcyHAzw60fGWE6
+ NbwCAAA=
+ """
+ )
+
val TEST_NAV_HOST =
bytecodeStub(
"TestNavHost.kt",
diff --git a/navigation/navigation-runtime-ktx/build.gradle b/navigation/navigation-runtime-ktx/build.gradle
index 1d67a1c6..7ccfe8a 100644
--- a/navigation/navigation-runtime-ktx/build.gradle
+++ b/navigation/navigation-runtime-ktx/build.gradle
@@ -31,9 +31,6 @@
dependencies {
api(project(":navigation:navigation-runtime"))
- api(project(":navigation:navigation-common-ktx")) {
- because 'Mirror navigation-runtime dependency graph for -ktx artifacts'
- }
}
androidx {
diff --git a/navigation/navigation-runtime-lint/build.gradle b/navigation/navigation-runtime-lint/build.gradle
index f7c323c..8baa413 100644
--- a/navigation/navigation-runtime-lint/build.gradle
+++ b/navigation/navigation-runtime-lint/build.gradle
@@ -35,11 +35,16 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
+ compileOnly(libs.androidToolsCommon)
+ compileOnly(libs.intellijCore)
+ compileOnly(libs.uast)
+ bundleInside(project(":navigation:navigation-common-lint"))
bundleInside(project(":navigation:navigation-lint-common"))
testImplementation(libs.kotlinStdlib)
- testImplementation(libs.androidLintPrev)
+ testImplementation(libs.androidLintPrevApi)
testImplementation(libs.androidLintPrevTests)
+ testImplementation(libs.intellijCore)
testImplementation(libs.junit)
testImplementation(libs.truth)
}
diff --git a/navigation/navigation-runtime-truth/build.gradle b/navigation/navigation-runtime-truth/build.gradle
index e250034..581b3f0 100644
--- a/navigation/navigation-runtime-truth/build.gradle
+++ b/navigation/navigation-runtime-truth/build.gradle
@@ -33,12 +33,14 @@
api(project(":navigation:navigation-runtime"))
api(libs.truth)
api(libs.kotlinStdlib)
+
+ implementation(project(":navigation:navigation-common"))
+
androidTestImplementation(project(":internal-testutils-truth"))
androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.junit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 9acb9fd..90bfe60 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -33,32 +33,41 @@
}
dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
api(project(":navigation:navigation-common"))
api("androidx.activity:activity-ktx:1.7.1")
+ api("androidx.core:core-ktx:1.8.0")
+ api("androidx.lifecycle:lifecycle-common:2.6.2")
api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
api("androidx.annotation:annotation-experimental:1.4.1")
- implementation("androidx.collection:collection:1.4.2")
- implementation(libs.kotlinSerializationCore)
- api(libs.kotlinStdlib)
- androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
+ implementation(libs.kotlinSerializationCore)
+ implementation("androidx.collection:collection:1.4.2")
+
+ androidTestImplementation("androidx.annotation:annotation:1.8.0")
+ androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-common:2.6.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.6.2")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.2")
androidTestImplementation(project(":internal-testutils-navigation"))
androidTestImplementation(project(":internal-testutils-runtime"))
+ androidTestImplementation(libs.hamcrestCore)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinTest)
+ androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testExtTruth)
- androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testMonitor)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.espressoIntents)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.kotlinStdlib)
- androidTestImplementation(libs.kotlinTest)
+ androidTestImplementation(libs.mockitoCore)
- lintPublish(project(':navigation:navigation-runtime-lint'))
+ lintPublish(project(":navigation:navigation-runtime-lint"))
}
android {
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index 8b2401e..8d4640b 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -47,8 +47,11 @@
implementation(libs.javapoet)
implementation(libs.kotlinPoet)
- testImplementation(libs.junit)
testImplementation(libs.googleCompileTesting)
+ testImplementation(libs.guava)
+ testImplementation(libs.hamcrestCore)
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
testImplementation(project(":room:room-compiler-processing-testing"), {
exclude group: "androidx.room", module: "room-compiler-processing"
})
diff --git a/navigation/navigation-safe-args-gradle-plugin/build.gradle b/navigation/navigation-safe-args-gradle-plugin/build.gradle
index 87d1ac3..33499f6 100644
--- a/navigation/navigation-safe-args-gradle-plugin/build.gradle
+++ b/navigation/navigation-safe-args-gradle-plugin/build.gradle
@@ -39,13 +39,18 @@
}
dependencies {
- implementation("com.android.tools.build:gradle:7.3.0")
- implementation(libs.kotlinGradlePluginz)
api(project(":navigation:navigation-safe-args-generator"))
api(gradleApi())
+
+ implementation("com.android.tools.build:builder-model:7.3.0")
+ implementation("com.android.tools.build:gradle:7.3.0")
+ implementation("com.android.tools.build:gradle-api:7.3.0")
+ implementation(libs.kotlinGradlePluginz)
implementation(libs.gson)
+
testImplementation(gradleTestKit())
testImplementation(project(":internal-testutils-gradle-plugin"))
+ testImplementation(libs.hamcrestCore)
testImplementation(libs.junit)
testPlugin("com.android.tools.build:gradle:7.3.0")
testPlugin("com.android.tools.build:aapt2:7.3.0-8691043")
diff --git a/navigation/navigation-testing/build.gradle b/navigation/navigation-testing/build.gradle
index 5c80ca7..92925b0 100644
--- a/navigation/navigation-testing/build.gradle
+++ b/navigation/navigation-testing/build.gradle
@@ -32,27 +32,34 @@
dependencies {
api(project(":navigation:navigation-runtime"))
- api("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
- implementation(libs.kotlinSerializationCore)
+ api("androidx.lifecycle:lifecycle-runtime-testing:2.6.2")
+ implementation(libs.kotlinCoroutinesCore)
+ implementation(libs.kotlinSerializationCore)
+ implementation(project(":navigation:navigation-common"))
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.lifecycle:lifecycle-common:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
+
+ androidTestRuntimeOnly(libs.kotlinTestJunit)
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.6.2")
androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
+ androidTestImplementation(libs.junit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testExtTruth)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.kotlinTestJunit)
- testImplementation(libs.testCore)
- testImplementation(libs.testExtJunit)
- testImplementation(libs.testExtTruth)
+ testRuntimeOnly(libs.testCore)
+ testRuntimeOnly(libs.kotlinTestJunit)
+ testImplementation(libs.junit)
testImplementation(libs.testRunner)
- testImplementation(libs.testRules)
testImplementation(libs.truth)
- testImplementation(libs.kotlinTestJunit)
testImplementation(libs.robolectric)
}
diff --git a/navigation/navigation-ui-ktx/build.gradle b/navigation/navigation-ui-ktx/build.gradle
index 77f8cdf..27b2f04 100644
--- a/navigation/navigation-ui-ktx/build.gradle
+++ b/navigation/navigation-ui-ktx/build.gradle
@@ -31,9 +31,6 @@
dependencies {
api(project(":navigation:navigation-ui"))
- api(project(":navigation:navigation-runtime-ktx")) {
- because 'Mirror navigation-ui dependency graph for -ktx artifacts'
- }
}
androidx {
diff --git a/navigation/navigation-ui/build.gradle b/navigation/navigation-ui/build.gradle
index ab3978b..7b15eb9 100644
--- a/navigation/navigation-ui/build.gradle
+++ b/navigation/navigation-ui/build.gradle
@@ -37,19 +37,25 @@
}
dependencies {
+ api(project(":navigation:navigation-common"))
api(project(":navigation:navigation-runtime"))
api("androidx.customview:customview:1.1.0")
api("androidx.drawerlayout:drawerlayout:1.1.1")
api("com.google.android.material:material:1.4.0")
+
+ implementation("androidx.appcompat:appcompat:1.2.0")
+ implementation("androidx.core:core-ktx:1.2.0")
+ implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.transition:transition:1.3.0")
- api("androidx.annotation:annotation-experimental:1.4.1")
androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
+ androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testMonitor)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index c8a852e..7859569 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
-distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index a659c89..c9a1161 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -73,7 +73,7 @@
interfaceDeclaration.declarations
.filterIsInstance<KSClassDeclaration>()
.filter { it.isCompanionObject }
- .forEach { validateCompanion(name, it) }
+ .forEach { validateCompanion(name, it, logger) }
val invalidModifiers =
interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
@@ -114,41 +114,6 @@
)
}
- private fun validateCompanion(name: String, companion: KSClassDeclaration) {
- val nonConstValues =
- companion.declarations
- .filterIsInstance<KSPropertyDeclaration>()
- .filter { !it.modifiers.contains(Modifier.CONST) }
- .toList()
- if (nonConstValues.isNotEmpty()) {
- logger.error(
- "Error in $name: companion object cannot declare non-const values (${
- nonConstValues.joinToString(limit = 3) { it.simpleName.getShortName() }
- })."
- )
- }
- val methods =
- companion.declarations
- .filterIsInstance<KSFunctionDeclaration>()
- .filter { it.simpleName.getFullName() != "<init>" }
- .toList()
- if (methods.isNotEmpty()) {
- logger.error(
- "Error in $name: companion object cannot declare methods (${
- methods.joinToString(limit = 3) { it.simpleName.getShortName() }
- })."
- )
- }
- val classes = companion.declarations.filterIsInstance<KSClassDeclaration>().toList()
- if (classes.isNotEmpty()) {
- logger.error(
- "Error in $name: companion object cannot declare classes (${
- classes.joinToString(limit = 3) { it.simpleName.getShortName() }
- })."
- )
- }
- }
-
private fun parseMethod(method: KSFunctionDeclaration): Method {
val name = method.qualifiedName?.getFullName() ?: method.simpleName.getFullName()
if (!method.isAbstract) {
@@ -201,3 +166,38 @@
)
}
}
+
+internal fun validateCompanion(name: String, companionDecl: KSClassDeclaration, logger: KSPLogger) {
+ val nonConstValues =
+ companionDecl.declarations
+ .filterIsInstance<KSPropertyDeclaration>()
+ .filter { !it.modifiers.contains(Modifier.CONST) }
+ .toList()
+ if (nonConstValues.isNotEmpty()) {
+ logger.error(
+ "Error in $name: companion object cannot declare non-const values (${
+ nonConstValues.joinToString(limit = 3) { it.simpleName.getShortName() }
+ })."
+ )
+ }
+ val methods =
+ companionDecl.declarations
+ .filterIsInstance<KSFunctionDeclaration>()
+ .filter { it.simpleName.getFullName() != "<init>" }
+ .toList()
+ if (methods.isNotEmpty()) {
+ logger.error(
+ "Error in $name: companion object cannot declare methods (${
+ methods.joinToString(limit = 3) { it.simpleName.getShortName() }
+ })."
+ )
+ }
+ val classes = companionDecl.declarations.filterIsInstance<KSClassDeclaration>().toList()
+ if (classes.isNotEmpty()) {
+ logger.error(
+ "Error in $name: companion object cannot declare classes (${
+ classes.joinToString(limit = 3) { it.simpleName.getShortName() }
+ })."
+ )
+ }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 6f548dc..f8ad610 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -50,7 +50,7 @@
if (!value.isPublic()) {
logger.error("Error in $name: annotated values should be public.")
}
- ensureNoCompanion(value, name)
+ validateCompanions(value, name)
ensureNoObject(value, name)
ensureNoTypeParameters(value, name)
ensureNoSuperTypes(value, name)
@@ -77,14 +77,11 @@
)
}
- private fun ensureNoCompanion(classDeclaration: KSClassDeclaration, name: String) {
- if (
- classDeclaration.declarations
- .filterIsInstance<KSClassDeclaration>()
- .any(KSClassDeclaration::isCompanionObject)
- ) {
- logger.error("Error in $name: annotated values cannot declare companion objects.")
- }
+ private fun validateCompanions(classDeclaration: KSClassDeclaration, name: String) {
+ classDeclaration.declarations
+ .filterIsInstance<KSClassDeclaration>()
+ .filter(KSClassDeclaration::isCompanionObject)
+ .forEach { validateCompanion(name, it, logger) }
}
private fun ensureNoObject(classDeclaration: KSClassDeclaration, name: String) {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 88949a2..9b8637b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -138,8 +138,8 @@
.isEqualTo(
setOf(
AnnotatedEnumClass(
- Type(packageName = "com.mysdk", simpleName = "MyEnum"),
- listOf("FOO", "BAR")
+ type = Type(packageName = "com.mysdk", simpleName = "MyEnum"),
+ variants = listOf("FOO", "BAR")
)
)
)
@@ -198,13 +198,15 @@
}
@Test
- fun dataClassWithCompanionObject_fails() {
+ fun dataClassWithCompanionNonConstDeclarations_fails() {
val dataClass =
annotatedValue(
"""
|data class MySdkRequest(val id: Int) {
| companion object {
- | val someConstant = 12
+ | const val OKAY_CONST = true && false
+ | val someVar = 12
+ | fun getNull() { return null }
| }
|}
"""
@@ -212,8 +214,10 @@
)
checkSourceFails(dataClass)
.containsExactlyErrors(
- "Error in com.mysdk.MySdkRequest: annotated values cannot declare companion " +
- "objects."
+ "Error in com.mysdk.MySdkRequest: companion object cannot declare non-const " +
+ "values (someVar).",
+ "Error in com.mysdk.MySdkRequest: companion object cannot declare methods " +
+ "(getNull).",
)
}
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index 22d2387..ad8456b 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -58,11 +58,11 @@
shadowJar {
archiveClassifier = ""
configurations = [project.configurations.shadowed]
- relocate("kotlinx.metadata", "androidx.privacysandbox.tools.kotlinx.metadata")
- mergeServiceFiles() // kotlinx-metadata-jvm has a service descriptor that needs transformation
- // Exclude Kotlin metadata files from kotlinx-metadata-jvm
- exclude 'META-INF/kotlinx-metadata-jvm.kotlin_module'
- exclude 'META-INF/kotlinx-metadata.kotlin_module'
+ relocate("kotlin.metadata", "androidx.privacysandbox.tools.kotlin.metadata")
+ mergeServiceFiles() // kotlin-metadata-jvm has a service descriptor that needs transformation
+ // Exclude Kotlin metadata files from kotlin-metadata-jvm
+ exclude 'META-INF/kotlin-metadata-jvm.kotlin_module'
+ exclude 'META-INF/kotlin-metadata.kotlin_module'
exclude 'META-INF/metadata.jvm.kotlin_module'
exclude 'META-INF/metadata.kotlin_module'
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
index 3379c80..c9a8534 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
@@ -18,17 +18,15 @@
import androidx.privacysandbox.tools.core.generator.addCommonSettings
import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.generateConstant
import androidx.privacysandbox.tools.core.generator.poetClassName
import androidx.privacysandbox.tools.core.generator.poetSpec
import androidx.privacysandbox.tools.core.generator.poetTypeName
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
-import androidx.privacysandbox.tools.core.model.Constant
import androidx.privacysandbox.tools.core.model.Method
-import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
internal class InterfaceFileGenerator {
@@ -52,31 +50,6 @@
.build { addCommonSettings() }
}
- private fun generateConstant(constant: Constant): PropertySpec {
- var value = constant.value
- // JVM bytecode stores boolean values as 0 or 1, so we convert this back to Boolean.
- if (constant.type.simpleName == "Boolean") {
- value = value != 0
- }
- if (constant.type.simpleName == "String") {
- // Escape strings using KotlinPoet
- value = CodeBlock.of("%S", value)
- }
- // JVM bytecode stores char values as a u16, so we convert this back to Char.
- if (constant.type.simpleName == "Char") {
- val char = (value as Int).toChar()
- // Use KotlinPoet to handle most escape sequences, but we need to handle single-quote
- // ourselves since Poet thinks this is a String.
- val escapedAsString = CodeBlock.of("%S", char).toString().replace("'", "\\'")
- val escapedChar = escapedAsString.substring(1, escapedAsString.length - 1)
- value = "'$escapedChar'"
- }
-
- return PropertySpec.builder(constant.name, constant.type.poetTypeName(), KModifier.CONST)
- .initializer("%L", value)
- .build()
- }
-
private fun generateInterfaceMethod(method: Method) =
FunSpec.builder(method.name).build {
addModifiers(KModifier.ABSTRACT)
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
index c971df0..ff90e75 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
@@ -24,9 +24,9 @@
import androidx.privacysandbox.tools.core.model.Constant
import androidx.privacysandbox.tools.core.model.Types
import java.nio.file.Path
-import kotlinx.metadata.KmClass
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import kotlinx.metadata.jvm.Metadata
+import kotlin.metadata.KmClass
+import kotlin.metadata.jvm.KotlinClassMetadata
+import kotlin.metadata.jvm.Metadata
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
@@ -42,62 +42,51 @@
data class ClassAndConstants(
val kClass: KmClass,
- val constants: List<Constant>?,
+ val constants: List<Constant>,
)
internal object AnnotatedClassReader {
val annotations = listOf(PrivacySandboxService::class)
fun readAnnotatedClasses(stubClassPath: Path): AnnotatedClasses {
- val companionPaths = mutableMapOf<String, String>()
- val classNodeByPath = mutableMapOf<String, ClassNode>()
- for (classFile in
- stubClassPath.toFile().walk().filter { it.isFile && it.extension == "class" }) {
- val classNode = toClassNode(classFile.readBytes())
- classNodeByPath[classNode.name] = classNode
- if (
- !(classNode.isAnnotatedWith<PrivacySandboxService>() ||
- classNode.isAnnotatedWith<PrivacySandboxValue>() ||
- classNode.isAnnotatedWith<PrivacySandboxInterface>() ||
- classNode.isAnnotatedWith<PrivacySandboxCallback>())
- ) {
- continue
- }
- val kotlinMetadata = parseKotlinMetadata(classNode)
- if (kotlinMetadata.companionObject != null) {
- val companionName = kotlinMetadata.companionObject!!
- companionPaths[kotlinMetadata.name] = "${kotlinMetadata.name}$${companionName}"
- }
- }
-
val services = mutableSetOf<ClassAndConstants>()
val values = mutableSetOf<ClassAndConstants>()
val callbacks = mutableSetOf<ClassAndConstants>()
val interfaces = mutableSetOf<ClassAndConstants>()
- classNodeByPath.values.forEach { classNode ->
- val companionNode = companionPaths[classNode.name]?.let { classNodeByPath[it] }
- val constants =
- companionNode
- ?.let { companion ->
- companion.fields
- .filter { it.name != "\$\$INSTANCE" }
- .map { Constant(it.name, getConstType(it.desc), it.value) }
- }
- ?.toList()
- if (classNode.isAnnotatedWith<PrivacySandboxService>()) {
- services.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
+
+ stubClassPath
+ .toFile()
+ .walk()
+ .filter { it.isFile && it.extension == "class" }
+ .map { toClassNode(it.readBytes()) }
+ .forEach { classNode ->
+ // Data classes and enum classes store their constants on the object itself, rather
+ // than in the companion's class file, so we extract the constants from amongst the
+ // other fields on the annotated value/interface.
+ // Thankfully, data class fields are always non-static, and enum variants are always
+ // of the enum's type (hence not primitive or string, which consts must be).
+ // The const-allowed-types check also filters out the Companion and the VALUES
+ // array.
+ val constants =
+ classNode.fields
+ .filter { it.access.hasFlag(PUBLIC_STATIC_FINAL_ACCESS) }
+ .filter { it.desc in constAllowedTypes.keys }
+ .map { Constant(it.name, getConstType(it.desc), it.value) }
+ .toList()
+ if (classNode.isAnnotatedWith<PrivacySandboxService>()) {
+ services.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
+ }
+ // TODO(b/323369085): Validate that enum variants don't have methods
+ if (classNode.isAnnotatedWith<PrivacySandboxValue>()) {
+ values.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
+ }
+ if (classNode.isAnnotatedWith<PrivacySandboxCallback>()) {
+ callbacks.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
+ }
+ if (classNode.isAnnotatedWith<PrivacySandboxInterface>()) {
+ interfaces.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
+ }
}
- // TODO(b/323369085): Validate that enum variants don't have methods
- if (classNode.isAnnotatedWith<PrivacySandboxValue>()) {
- values.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
- }
- if (classNode.isAnnotatedWith<PrivacySandboxCallback>()) {
- callbacks.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
- }
- if (classNode.isAnnotatedWith<PrivacySandboxInterface>()) {
- interfaces.add(ClassAndConstants(parseKotlinMetadata(classNode), constants))
- }
- }
return AnnotatedClasses(
services = services.toSet(),
values = values.toSet(),
@@ -171,26 +160,31 @@
return attributes
}
+ private val constAllowedTypes =
+ mapOf(
+ "Ljava/lang/String;" to Types.string,
+ "I" to Types.int,
+ "Z" to Types.boolean,
+ "B" to Types.byte,
+ "C" to Types.char,
+ "D" to Types.double,
+ "F" to Types.float,
+ "J" to Types.long,
+ "S" to Types.short
+ )
+
private fun getConstType(desc: String): androidx.privacysandbox.tools.core.model.Type {
- if (desc == "Ljava/lang/String;") {
- return Types.string
- } else if (desc == "I") {
- return Types.int
- } else if (desc == "Z") {
- return Types.boolean
- } else if (desc == "B") {
- return Types.byte
- } else if (desc == "C") {
- return Types.char
- } else if (desc == "D") {
- return Types.double
- } else if (desc == "F") {
- return Types.float
- } else if (desc == "J") {
- return Types.long
- } else if (desc == "S") {
- return Types.short
- }
- throw PrivacySandboxParsingException("Unrecognised constant type: '$desc'")
+ return constAllowedTypes[desc]
+ ?: throw PrivacySandboxParsingException("Unrecognised constant type: '$desc'")
}
}
+
+// See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
+// TODO: Once we upgrade to Java 22 we can import these constants from
+// java.lang.classfile
+private const val PUBLIC_STATIC_FINAL_ACCESS =
+ 0x0001 or // public
+ 0x0008 or // static
+ 0x0010 // final
+
+private fun Int.hasFlag(flag: Int) = flag and this == flag
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index d1e1c05..1235106 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -29,18 +29,18 @@
import androidx.privacysandbox.tools.core.model.ValueProperty
import androidx.privacysandbox.tools.core.validator.ModelValidator
import java.nio.file.Path
-import kotlinx.metadata.ClassKind
-import kotlinx.metadata.ClassName
-import kotlinx.metadata.KmClass
-import kotlinx.metadata.KmClassifier
-import kotlinx.metadata.KmFunction
-import kotlinx.metadata.KmProperty
-import kotlinx.metadata.KmType
-import kotlinx.metadata.isData
-import kotlinx.metadata.isNullable
-import kotlinx.metadata.isSuspend
-import kotlinx.metadata.isVar
-import kotlinx.metadata.kind
+import kotlin.metadata.ClassKind
+import kotlin.metadata.ClassName
+import kotlin.metadata.KmClass
+import kotlin.metadata.KmClassifier
+import kotlin.metadata.KmFunction
+import kotlin.metadata.KmProperty
+import kotlin.metadata.KmType
+import kotlin.metadata.isData
+import kotlin.metadata.isNullable
+import kotlin.metadata.isSuspend
+import kotlin.metadata.isVar
+import kotlin.metadata.kind
internal object ApiStubParser {
/**
@@ -83,7 +83,7 @@
type = type,
superTypes = superTypes,
methods = service.functions.map(this::parseMethod),
- constants = interfaceAndConstants.constants?.toList() ?: listOf(),
+ constants = interfaceAndConstants.constants,
)
}
@@ -117,9 +117,17 @@
}
return if (value.isData) {
- AnnotatedDataClass(type, parseProperties(type, value))
+ AnnotatedDataClass(
+ type,
+ constants = classAndConstants.constants,
+ properties = parseProperties(type, value)
+ )
} else {
- AnnotatedEnumClass(type, value.enumEntries.toList())
+ AnnotatedEnumClass(
+ type,
+ constants = classAndConstants.constants,
+ variants = value.enumEntries.toList()
+ )
}
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
index 6359994..1a42b78 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
@@ -34,12 +34,21 @@
val maybeNumber: Int?,
val maybeInterface: MyInterface?,
val maybeBundle: Bundle?,
-)
+) {
+ companion object {
+ const val DEFAULT_USER_ID = 42
+ const val DEFAULT_SEPARATOR = '"'
+ }
+}
@PrivacySandboxValue
enum class RequestFlag {
UP,
- DOWN,
+ DOWN;
+
+ companion object {
+ const val STEP_SIZE = 5
+ }
}
@PrivacySandboxValue
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
index b88cc6a..1be6291 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
@@ -17,4 +17,10 @@
public val maybeNumber: Int?,
public val maybeInterface: MyInterface?,
public val maybeBundle: Bundle?,
-)
+) {
+ public companion object {
+ public const val DEFAULT_USER_ID: Int = 42
+
+ public const val DEFAULT_SEPARATOR: Char = '\"'
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlag.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlag.kt
index 9a969f5..4f90ade 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlag.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlag.kt
@@ -3,4 +3,9 @@
public enum class RequestFlag {
UP,
DOWN,
+ ;
+
+ public companion object {
+ public const val STEP_SIZE: Int = 5
+ }
}
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/ClassReader.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/ClassReader.kt
index f83ecd0..aaef95c 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/ClassReader.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/ClassReader.kt
@@ -17,9 +17,9 @@
package androidx.privacysandbox.tools.apipackager
import androidx.privacysandbox.tools.core.PrivacySandboxParsingException
-import kotlinx.metadata.KmClass
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import kotlinx.metadata.jvm.Metadata
+import kotlin.metadata.KmClass
+import kotlin.metadata.jvm.KotlinClassMetadata
+import kotlin.metadata.jvm.Metadata
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
index 523b668b..ccb01d9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
@@ -19,6 +19,8 @@
import androidx.privacysandbox.tools.core.model.AnnotatedDataClass
import androidx.privacysandbox.tools.core.model.AnnotatedEnumClass
import androidx.privacysandbox.tools.core.model.AnnotatedValue
+import androidx.privacysandbox.tools.core.model.Constant
+import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
@@ -44,10 +46,49 @@
.build()
}
)
+ if (value.constants.isNotEmpty()) {
+ addType(
+ TypeSpec.companionObjectBuilder().build {
+ addProperties(value.constants.map(::generateConstant))
+ }
+ )
+ }
}
is AnnotatedEnumClass ->
TypeSpec.enumBuilder(value.type.poetClassName()).build {
value.variants.forEach(::addEnumConstant)
+ if (value.constants.isNotEmpty()) {
+ addType(
+ TypeSpec.companionObjectBuilder().build {
+ addProperties(value.constants.map(::generateConstant))
+ }
+ )
+ }
}
}
}
+
+fun generateConstant(constant: Constant): PropertySpec {
+ var value = constant.value
+ // JVM bytecode stores boolean values as 0 or 1, so we convert this back to Boolean.
+ if (constant.type.simpleName == "Boolean") {
+ value = value != 0
+ }
+ if (constant.type.simpleName == "String") {
+ // Escape strings using KotlinPoet
+ value = CodeBlock.of("%S", value)
+ }
+ // JVM bytecode stores char values as a u16, so we convert this back to Char.
+ if (constant.type.simpleName == "Char") {
+ val char = (value as Int).toChar()
+ // Use KotlinPoet to handle most escape sequences, but we need to handle single-quote
+ // ourselves since Poet thinks this is a String.
+ val escapedAsString = CodeBlock.of("%S", char).toString().replace("'", "\\'")
+ val escapedChar = escapedAsString.substring(1, escapedAsString.length - 1)
+ value = "'$escapedChar'"
+ }
+
+ return PropertySpec.builder(constant.name, constant.type.poetTypeName(), KModifier.CONST)
+ .initializer("%L", value)
+ .build()
+}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedValue.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedValue.kt
index 86a7f18..91d49d6 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedValue.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/AnnotatedValue.kt
@@ -16,14 +16,19 @@
package androidx.privacysandbox.tools.core.model
-sealed class AnnotatedValue(open val type: Type)
+sealed class AnnotatedValue(
+ open val type: Type,
+ open val constants: List<Constant> = emptyList(),
+)
data class AnnotatedDataClass(
override val type: Type,
+ override val constants: List<Constant> = emptyList(),
val properties: List<ValueProperty>,
) : AnnotatedValue(type)
data class AnnotatedEnumClass(
override val type: Type,
+ override val constants: List<Constant> = emptyList(),
val variants: List<String>,
) : AnnotatedValue(type)
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
index 6aeff74..d7db134 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
@@ -39,31 +39,33 @@
fun generate() {
val innerEnum =
AnnotatedEnumClass(
- Type(packageName = "com.mysdk", simpleName = "InnerEnum"),
- listOf("ONE, TWO, THREE")
+ type = Type(packageName = "com.mysdk", simpleName = "InnerEnum"),
+ variants = listOf("ONE, TWO, THREE")
)
val innerValue =
AnnotatedDataClass(
- Type(packageName = "com.mysdk", simpleName = "InnerValue"),
- listOf(
- ValueProperty("intProperty", Types.int),
- ValueProperty("booleanProperty", Types.boolean),
- ValueProperty("longProperty", Types.long),
- ValueProperty("maybeFloatProperty", Types.float.asNullable()),
- ValueProperty("maybeByteProperty", Types.byte.asNullable()),
- ValueProperty("enumProperty", innerEnum.type),
- ValueProperty("bundleProperty", Types.bundle),
- ValueProperty("maybeBundleProperty", Types.bundle.asNullable())
- )
+ type = Type(packageName = "com.mysdk", simpleName = "InnerValue"),
+ properties =
+ listOf(
+ ValueProperty("intProperty", Types.int),
+ ValueProperty("booleanProperty", Types.boolean),
+ ValueProperty("longProperty", Types.long),
+ ValueProperty("maybeFloatProperty", Types.float.asNullable()),
+ ValueProperty("maybeByteProperty", Types.byte.asNullable()),
+ ValueProperty("enumProperty", innerEnum.type),
+ ValueProperty("bundleProperty", Types.bundle),
+ ValueProperty("maybeBundleProperty", Types.bundle.asNullable())
+ )
)
val outerValue =
AnnotatedDataClass(
- Type(packageName = "com.mysdk", simpleName = "OuterValue"),
- listOf(
- ValueProperty("innerValue", innerValue.type),
- ValueProperty("innerValueList", Types.list(innerValue.type)),
- ValueProperty("maybeInnerValue", innerValue.type.asNullable()),
- )
+ type = Type(packageName = "com.mysdk", simpleName = "OuterValue"),
+ properties =
+ listOf(
+ ValueProperty("innerValue", innerValue.type),
+ ValueProperty("innerValueList", Types.list(innerValue.type)),
+ ValueProperty("maybeInnerValue", innerValue.type.asNullable()),
+ )
)
val api =
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
index 1d05403..0ede714 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
@@ -256,14 +256,6 @@
canvas.drawColor(viewColor)
canvas.drawText(text, 75F, 75F, paint)
pointerIdToPathMap.forEach { (_, path) -> canvas.drawPath(path, paint) }
-
- setOnClickListener {
- Log.i(TAG, "Click on ad detected")
- val visitUrl = Intent(Intent.ACTION_VIEW)
- visitUrl.data = Uri.parse(GOOGLE_URL)
- visitUrl.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(visitUrl)
- }
}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_resize.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_resize.xml
index d4acda9..04451fa 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_resize.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_resize.xml
@@ -30,7 +30,7 @@
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
- android:background="#FF0000" />
+ android:background="#FFFFFF" />
<LinearLayout
android:layout_width="wrap_content"
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_scroll.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_scroll.xml
index e391ec4..8bdb2e8 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_scroll.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_scroll.xml
@@ -39,7 +39,7 @@
android:layout_width="wrap_content"
android:layout_weight="2"
android:layout_height="0dp"
- android:background="#FF0000" />
+ android:background="#FFFFFF" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index 1621feb..d9dafed 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -101,7 +101,7 @@
clientExecutor: Executor,
client: SandboxedUiAdapter.SessionClient
) {
- client.onSessionError(Exception("Error in openSession()"))
+ clientExecutor.execute { client.onSessionError(Exception("Error in openSession()")) }
}
}
@@ -133,13 +133,15 @@
) {
internalClient = client
testSession = TestSession(context, initialWidth, initialHeight, signalOptions)
- if (!delayOpenSessionCallback) {
- client.onSessionOpened(testSession!!)
+ clientExecutor.execute {
+ if (!delayOpenSessionCallback) {
+ client.onSessionOpened(testSession!!)
+ }
+ isSessionOpened = true
+ this.isZOrderOnTop = isZOrderOnTop
+ this.inputToken = windowInputToken
+ openSessionLatch.countDown()
}
- isSessionOpened = true
- this.isZOrderOnTop = isZOrderOnTop
- this.inputToken = windowInputToken
- openSessionLatch.countDown()
}
internal fun sendOnSessionOpened() {
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/PoolingContainerTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/PoolingContainerTests.kt
index ffa54e7..3d6f679 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/PoolingContainerTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/PoolingContainerTests.kt
@@ -110,9 +110,6 @@
@Test
fun testPoolingContainerListener_AllViewsRemovedFromContainer() {
- // TODO(b/309848703): Stop skipping this for backwards compat flow
- assumeTrue(!invokeBackwardsCompatFlow)
-
val adapter =
createRecyclerViewTestAdapterAndWaitForChildrenToBeActive(isNestedView = false)
@@ -124,9 +121,6 @@
@Test
fun testPoolingContainerListener_ContainerRemovedFromLayout() {
- // TODO(b/309848703): Stop skipping this for backwards compat flow
- assumeTrue(!invokeBackwardsCompatFlow)
-
val adapter = createRecyclerViewTestAdapterAndWaitForChildrenToBeActive(isNestedView = true)
activityScenarioRule.withActivity { linearLayout.removeView(recyclerView) }
@@ -136,9 +130,6 @@
@Test
fun testPoolingContainerListener_ViewWithinAnotherView_AllViewsRemovedFromContainer() {
- // TODO(b/309848703): Stop skipping this for backwards compat flow
- assumeTrue(!invokeBackwardsCompatFlow)
-
val adapter =
createRecyclerViewTestAdapterAndWaitForChildrenToBeActive(isNestedView = false)
@@ -150,9 +141,6 @@
@Test
fun testPoolingContainerListener_ViewWithinAnotherView_ContainerRemovedFromLayout() {
- // TODO(b/309848703): Stop skipping this for backwards compat flow
- assumeTrue(!invokeBackwardsCompatFlow)
-
val adapter = createRecyclerViewTestAdapterAndWaitForChildrenToBeActive(isNestedView = true)
activityScenarioRule.withActivity { linearLayout.removeView(recyclerView) }
@@ -165,8 +153,6 @@
fun testPoolingContainerListener_NotifyFetchUiForSession() {
// verifyColorOfScreenshot is only available for U+ devices.
assumeTrue(SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- // TODO(b/309848703): Stop skipping this for backwards compat flow
- assumeTrue(!invokeBackwardsCompatFlow)
val recyclerViewAdapter = RecyclerViewTestAdapterForFetchingUi()
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
index 85868f9..6b52951 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
@@ -160,12 +160,14 @@
this.initialWidth = initialWidth
session =
if (hasFailingTestSession) {
- FailingTestSession(context, client)
+ FailingTestSession(context, client, clientExecutor)
} else {
TestSession(context, client, placeViewInsideFrameLayout)
}
- client.onSessionOpened(session)
- openSessionLatch.countDown()
+ clientExecutor.execute {
+ client.onSessionOpened(session)
+ openSessionLatch.countDown()
+ }
}
/**
@@ -173,11 +175,14 @@
*/
inner class FailingTestSession(
private val context: Context,
- sessionClient: SandboxedUiAdapter.SessionClient
+ sessionClient: SandboxedUiAdapter.SessionClient,
+ private val clientExecutor: Executor,
) : TestSession(context, sessionClient) {
override val view: View
get() {
- sessionClient.onSessionError(Throwable("Test Session Exception"))
+ clientExecutor.execute {
+ sessionClient.onSessionError(Throwable("Test Session Exception"))
+ }
return View(context)
}
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
new file mode 100644
index 0000000..fb45166
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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.integration.multiplatformtestapp.test
+
+import androidx.kruth.assertThat
+import androidx.room.Room
+import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.driver.AndroidSQLiteDriver
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlinx.coroutines.Dispatchers
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+@RunWith(Parameterized::class)
+class PagingTest(private val driver: SQLiteDriver) : BasePagingTest() {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val file = instrumentation.targetContext.getDatabasePath("test.db")
+
+ override fun getRoomDatabase(): SampleDatabase {
+ return Room.databaseBuilder<SampleDatabase>(
+ context = instrumentation.targetContext,
+ name = file.path
+ )
+ .setDriver(driver)
+ .setQueryCoroutineContext(Dispatchers.IO)
+ .build()
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "driver={0}")
+ fun drivers() = arrayOf(BundledSQLiteDriver(), AndroidSQLiteDriver())
+ }
+
+ @BeforeTest
+ fun before() {
+ assertThat(file).isNotNull()
+ file.parentFile?.mkdirs()
+ deleteDatabaseFile()
+ }
+
+ @AfterTest
+ fun after() {
+ deleteDatabaseFile()
+ }
+
+ private fun deleteDatabaseFile() {
+ instrumentation.targetContext.deleteDatabase(file.name)
+ }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BasePagingTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BasePagingTest.kt
new file mode 100644
index 0000000..0141bb2
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BasePagingTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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.integration.multiplatformtestapp.test
+
+import androidx.kruth.assertThat
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+
+abstract class BasePagingTest {
+
+ abstract fun getRoomDatabase(): SampleDatabase
+
+ @Test
+ fun pagingQuery() = runTest {
+ val db = getRoomDatabase()
+ val entity1 = SampleEntity(1, 1)
+ val entity2 = SampleEntity(2, 2)
+ val sampleEntities = listOf(entity1, entity2)
+ val dao = db.dao()
+
+ dao.insertSampleEntityList(sampleEntities)
+ val pagingSource = dao.getAllIds()
+
+ val onlyLoadFirst =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 1,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(onlyLoadFirst.data).containsExactly(entity1)
+
+ val loadAll =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 2,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(loadAll.data).containsExactlyElementsIn(sampleEntities)
+ }
+
+ @Test
+ fun pagingQueryWithParams() = runTest {
+ val db = getRoomDatabase()
+ val entity1 = SampleEntity(1, 1)
+ val entity2 = SampleEntity(2, 2)
+ val entity3 = SampleEntity(3, 3)
+ val sampleEntities = listOf(entity1, entity2, entity3)
+ val dao = db.dao()
+
+ dao.insertSampleEntityList(sampleEntities)
+ val pagingSource = dao.getAllIdsWithArgs(1)
+
+ val onlyLoadFirst =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 1,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(onlyLoadFirst.data).containsExactly(entity2)
+
+ val loadAll =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 2,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(loadAll.data).containsExactlyElementsIn(listOf(entity2, entity3))
+ }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
index 760c416..78e59dc 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
@@ -18,8 +18,6 @@
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
-import androidx.paging.PagingSource
-import androidx.paging.PagingSource.LoadResult
import androidx.room.RoomRawQuery
import androidx.room.execSQL
import androidx.room.immediateTransaction
@@ -505,67 +503,4 @@
.hasMessageThat()
.contains("Only bind*() calls are allowed")
}
-
- @Test
- fun simplePagingQuery() = runTest {
- val entity1 = SampleEntity(1, 1)
- val entity2 = SampleEntity(2, 2)
- val sampleEntities = listOf(entity1, entity2)
- val dao = db.dao()
-
- dao.insertSampleEntityList(sampleEntities)
- val pagingSource = dao.getAllIds()
-
- val onlyLoadFirst =
- pagingSource.load(
- PagingSource.LoadParams.Refresh(
- key = null,
- loadSize = 1,
- placeholdersEnabled = true
- )
- ) as LoadResult.Page
- assertThat(onlyLoadFirst.data).containsExactly(entity1)
-
- val loadAll =
- pagingSource.load(
- PagingSource.LoadParams.Refresh(
- key = null,
- loadSize = 2,
- placeholdersEnabled = true
- )
- ) as LoadResult.Page
- assertThat(loadAll.data).containsExactlyElementsIn(sampleEntities)
- }
-
- @Test
- fun pagingQueryWithParams() = runTest {
- val entity1 = SampleEntity(1, 1)
- val entity2 = SampleEntity(2, 2)
- val entity3 = SampleEntity(3, 3)
- val sampleEntities = listOf(entity1, entity2, entity3)
- val dao = db.dao()
-
- dao.insertSampleEntityList(sampleEntities)
- val pagingSource = dao.getAllIdsWithArgs(1)
-
- val onlyLoadFirst =
- pagingSource.load(
- PagingSource.LoadParams.Refresh(
- key = null,
- loadSize = 1,
- placeholdersEnabled = true
- )
- ) as LoadResult.Page
- assertThat(onlyLoadFirst.data).containsExactly(entity2)
-
- val loadAll =
- pagingSource.load(
- PagingSource.LoadParams.Refresh(
- key = null,
- loadSize = 2,
- placeholdersEnabled = true
- )
- ) as LoadResult.Page
- assertThat(loadAll.data).containsExactlyElementsIn(listOf(entity2, entity3))
- }
}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
new file mode 100644
index 0000000..7c9901c
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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.integration.multiplatformtestapp.test
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlin.io.path.createTempFile
+import kotlinx.coroutines.Dispatchers
+
+class PagingTest : BasePagingTest() {
+
+ override fun getRoomDatabase(): SampleDatabase {
+ val tempFile = createTempFile("test.db").also { it.toFile().deleteOnExit() }
+ return Room.databaseBuilder<SampleDatabase>(name = tempFile.toString())
+ .setDriver(BundledSQLiteDriver())
+ .setQueryCoroutineContext(Dispatchers.IO)
+ .build()
+ }
+}
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
new file mode 100644
index 0000000..67c3738
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/PagingTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.integration.multiplatformtestapp.test
+
+import androidx.room.Room
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlin.random.Random
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import platform.posix.remove
+
+class PagingTest : BasePagingTest() {
+
+ private val filename = "/tmp/test-${Random.nextInt()}.db"
+
+ override fun getRoomDatabase(): SampleDatabase {
+ return Room.databaseBuilder<SampleDatabase>(name = filename)
+ .setDriver(BundledSQLiteDriver())
+ .setQueryCoroutineContext(Dispatchers.IO)
+ .build()
+ }
+
+ @BeforeTest
+ fun before() {
+ deleteDatabaseFile()
+ }
+
+ @AfterTest
+ fun after() {
+ deleteDatabaseFile()
+ }
+
+ private fun deleteDatabaseFile() {
+ remove(filename)
+ remove("$filename-wal")
+ remove("$filename-shm")
+ }
+}
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 8c6ea4f..6e083ee 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -45,11 +45,11 @@
shadowJar {
archiveClassifier = ""
configurations = [project.configurations.shadowed]
- relocate("kotlinx.metadata", "androidx.room.jarjarred.kotlinx.metadata")
- mergeServiceFiles() // kotlinx-metadata-jvm has a service descriptor that needs transformation
- // Exclude Kotlin metadata files from kotlinx-metadata-jvm
- exclude 'META-INF/kotlinx-metadata-jvm.kotlin_module'
- exclude 'META-INF/kotlinx-metadata.kotlin_module'
+ relocate("kotlin.metadata", "androidx.room.jarjarred.kotlin.metadata")
+ mergeServiceFiles() // kotlin-metadata-jvm has a service descriptor that needs transformation
+ // Exclude Kotlin metadata files from kotlin-metadata-jvm
+ exclude 'META-INF/kotlin-metadata-jvm.kotlin_module'
+ exclude 'META-INF/kotlin-metadata.kotlin_module'
exclude 'META-INF/metadata.jvm.kotlin_module'
exclude 'META-INF/metadata.kotlin_module'
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
index 73f99d5..2f96281 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -29,36 +29,36 @@
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic
-import kotlinx.metadata.ClassKind
-import kotlinx.metadata.KmAnnotation
-import kotlinx.metadata.KmAnnotationArgument
-import kotlinx.metadata.KmClass
-import kotlinx.metadata.KmClassifier
-import kotlinx.metadata.KmConstructor
-import kotlinx.metadata.KmFunction
-import kotlinx.metadata.KmProperty
-import kotlinx.metadata.KmType
-import kotlinx.metadata.KmTypeParameter
-import kotlinx.metadata.KmValueParameter
-import kotlinx.metadata.Visibility
-import kotlinx.metadata.declaresDefaultValue
-import kotlinx.metadata.isData
-import kotlinx.metadata.isDelegated
-import kotlinx.metadata.isExpect
-import kotlinx.metadata.isFunInterface
-import kotlinx.metadata.isNullable
-import kotlinx.metadata.isSecondary
-import kotlinx.metadata.isSuspend
-import kotlinx.metadata.isValue
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import kotlinx.metadata.jvm.annotations
-import kotlinx.metadata.jvm.fieldSignature
-import kotlinx.metadata.jvm.getterSignature
-import kotlinx.metadata.jvm.setterSignature
-import kotlinx.metadata.jvm.signature
-import kotlinx.metadata.jvm.syntheticMethodForAnnotations
-import kotlinx.metadata.kind
-import kotlinx.metadata.visibility
+import kotlin.metadata.ClassKind
+import kotlin.metadata.KmAnnotation
+import kotlin.metadata.KmAnnotationArgument
+import kotlin.metadata.KmClass
+import kotlin.metadata.KmClassifier
+import kotlin.metadata.KmConstructor
+import kotlin.metadata.KmFunction
+import kotlin.metadata.KmProperty
+import kotlin.metadata.KmType
+import kotlin.metadata.KmTypeParameter
+import kotlin.metadata.KmValueParameter
+import kotlin.metadata.Visibility
+import kotlin.metadata.declaresDefaultValue
+import kotlin.metadata.isData
+import kotlin.metadata.isDelegated
+import kotlin.metadata.isExpect
+import kotlin.metadata.isFunInterface
+import kotlin.metadata.isNullable
+import kotlin.metadata.isSecondary
+import kotlin.metadata.isSuspend
+import kotlin.metadata.isValue
+import kotlin.metadata.jvm.KotlinClassMetadata
+import kotlin.metadata.jvm.annotations
+import kotlin.metadata.jvm.fieldSignature
+import kotlin.metadata.jvm.getterSignature
+import kotlin.metadata.jvm.setterSignature
+import kotlin.metadata.jvm.signature
+import kotlin.metadata.jvm.syntheticMethodForAnnotations
+import kotlin.metadata.kind
+import kotlin.metadata.visibility
internal interface KmData
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index 0ea2bc0..7a043aa 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -22,7 +22,7 @@
import androidx.paging.PagingSource.LoadResult
import androidx.room.RoomDatabase
import androidx.room.RoomRawQuery
-import androidx.room.immediateTransaction
+import androidx.room.Transactor.SQLiteTransactionType
import androidx.room.paging.util.INITIAL_ITEM_COUNT
import androidx.room.paging.util.queryDatabase
import androidx.room.paging.util.queryItemCount
@@ -110,7 +110,7 @@
*/
private suspend fun initialLoad(params: LoadParams<Int>): LoadResult<Int, Value> {
return db.useReaderConnection { connection ->
- connection.immediateTransaction {
+ connection.withTransaction(SQLiteTransactionType.DEFERRED) {
val tempCount = queryItemCount(sourceQuery, db)
itemCount.value = tempCount
queryDatabase(
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 260e53c..c8434e1 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -293,11 +293,19 @@
public static final class RevealValue.Companion {
method public int getCovered();
- method public int getRevealed();
- method public int getRevealing();
+ method public int getLeftRevealed();
+ method public int getLeftRevealing();
+ method @Deprecated public int getRevealed();
+ method @Deprecated public int getRevealing();
+ method public int getRightRevealed();
+ method public int getRightRevealing();
property public final int Covered;
- property public final int Revealed;
- property public final int Revealing;
+ property public final int LeftRevealed;
+ property public final int LeftRevealing;
+ property @Deprecated public final int Revealed;
+ property @Deprecated public final int Revealing;
+ property public final int RightRevealed;
+ property public final int RightRevealing;
}
public interface ScrollInfoProvider {
@@ -320,6 +328,19 @@
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.ScalingLazyListState state);
}
+ @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi @kotlin.jvm.JvmInline public final value class SwipeDirection {
+ method public int getValue();
+ property public final int value;
+ field public static final androidx.wear.compose.foundation.SwipeDirection.Companion Companion;
+ }
+
+ public static final class SwipeDirection.Companion {
+ method public int getBoth();
+ method public int getRightToLeft();
+ property public final int Both;
+ property public final int RightToLeft;
+ }
+
public final class SwipeToDismissBoxDefaults {
method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
method public float getEdgeWidth();
@@ -352,7 +373,7 @@
public final class SwipeToRevealKt {
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit> primaryAction, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onFullSwipe, optional androidx.wear.compose.foundation.RevealState state, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? secondaryAction, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? undoAction, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor);
+ method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor, optional int swipeDirection);
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold, optional java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> anchors);
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 260e53c..c8434e1 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -293,11 +293,19 @@
public static final class RevealValue.Companion {
method public int getCovered();
- method public int getRevealed();
- method public int getRevealing();
+ method public int getLeftRevealed();
+ method public int getLeftRevealing();
+ method @Deprecated public int getRevealed();
+ method @Deprecated public int getRevealing();
+ method public int getRightRevealed();
+ method public int getRightRevealing();
property public final int Covered;
- property public final int Revealed;
- property public final int Revealing;
+ property public final int LeftRevealed;
+ property public final int LeftRevealing;
+ property @Deprecated public final int Revealed;
+ property @Deprecated public final int Revealing;
+ property public final int RightRevealed;
+ property public final int RightRevealing;
}
public interface ScrollInfoProvider {
@@ -320,6 +328,19 @@
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.ScalingLazyListState state);
}
+ @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi @kotlin.jvm.JvmInline public final value class SwipeDirection {
+ method public int getValue();
+ property public final int value;
+ field public static final androidx.wear.compose.foundation.SwipeDirection.Companion Companion;
+ }
+
+ public static final class SwipeDirection.Companion {
+ method public int getBoth();
+ method public int getRightToLeft();
+ property public final int Both;
+ property public final int RightToLeft;
+ }
+
public final class SwipeToDismissBoxDefaults {
method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
method public float getEdgeWidth();
@@ -352,7 +373,7 @@
public final class SwipeToRevealKt {
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit> primaryAction, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onFullSwipe, optional androidx.wear.compose.foundation.RevealState state, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? secondaryAction, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? undoAction, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor);
+ method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor, optional int swipeDirection);
method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold, optional java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> anchors);
}
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToRevealSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToRevealSample.kt
index 008a0b0..4ac4377 100644
--- a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToRevealSample.kt
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToRevealSample.kt
@@ -185,7 +185,7 @@
listOf(
CustomAccessibilityAction("Delete") {
coroutineScope.launch {
- revealState.animateTo(RevealValue.Revealed)
+ revealState.animateTo(RevealValue.RightRevealed)
}
true
}
@@ -199,7 +199,7 @@
.background(Color.Red, actionShape)
.clickable {
coroutineScope.launch {
- revealState.animateTo(RevealValue.Revealed)
+ revealState.animateTo(RevealValue.RightRevealed)
}
},
contentAlignment = Alignment.Center
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
index 340ddc8..98a92bd 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
@@ -80,7 +80,7 @@
fun onRevealing_drawsAction() {
rule.setContent {
swipeToRevealWithDefaults(
- state = rememberRevealState(initialValue = RevealValue.Revealing),
+ state = rememberRevealState(initialValue = RevealValue.RightRevealing),
primaryAction = { actionContent(modifier = Modifier.testTag(TEST_TAG)) }
)
}
@@ -115,13 +115,13 @@
@Test
fun stateToSwiped_onFullSwipeLeft() {
- verifyGesture(revealValue = RevealValue.Revealed, gesture = { swipeLeft() })
+ verifyGesture(revealValue = RevealValue.RightRevealed, gesture = { swipeLeft() })
}
@Test
fun stateToIconsVisible_onPartialSwipeLeft() {
verifyGesture(
- revealValue = RevealValue.Revealing,
+ revealValue = RevealValue.RightRevealing,
gesture = { swipeLeft(startX = width / 2f, endX = 0f) }
)
}
@@ -132,7 +132,9 @@
rule.setContent {
revealState =
rememberRevealState(
- confirmValueChange = { revealValue -> revealValue != RevealValue.Revealing }
+ confirmValueChange = { revealValue ->
+ revealValue != RevealValue.RightRevealing
+ }
)
swipeToRevealWithDefaults(state = revealState, modifier = Modifier.testTag(TEST_TAG))
}
@@ -152,7 +154,9 @@
revealStateOne = rememberRevealState()
revealStateTwo =
rememberRevealState(
- confirmValueChange = { revealValue -> revealValue != RevealValue.Revealing }
+ confirmValueChange = { revealValue ->
+ revealValue != RevealValue.RightRevealing
+ }
)
Column {
swipeToRevealWithDefaults(
@@ -177,7 +181,7 @@
}
rule.runOnIdle {
- assertEquals(RevealValue.Revealing, revealStateOne.currentValue)
+ assertEquals(RevealValue.RightRevealing, revealStateOne.currentValue)
assertEquals(RevealValue.Covered, revealStateTwo.currentValue)
}
}
@@ -215,7 +219,7 @@
rule.runOnIdle {
assertEquals(RevealValue.Covered, revealStateOne.currentValue)
- assertEquals(RevealValue.Revealing, revealStateTwo.currentValue)
+ assertEquals(RevealValue.RightRevealing, revealStateTwo.currentValue)
}
}
@@ -252,8 +256,8 @@
rule.runOnIdle {
// assert that state does not reset
- assertEquals(RevealValue.Revealed, revealStateOne.currentValue)
- assertEquals(RevealValue.Revealing, revealStateTwo.currentValue)
+ assertEquals(RevealValue.RightRevealed, revealStateOne.currentValue)
+ assertEquals(RevealValue.RightRevealing, revealStateTwo.currentValue)
}
}
@@ -270,9 +274,9 @@
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
// First change
- revealStateOne.snapTo(RevealValue.Revealing)
+ revealStateOne.snapTo(RevealValue.RightRevealing)
// Second change, in a different state
- revealStateTwo.snapTo(RevealValue.Revealing)
+ revealStateTwo.snapTo(RevealValue.RightRevealing)
}
}
@@ -283,7 +287,7 @@
fun onMultiSnapOnSameState_doesNotReset() {
lateinit var revealStateOne: RevealState
lateinit var revealStateTwo: RevealState
- val lastValue = RevealValue.Revealed
+ val lastValue = RevealValue.RightRevealed
rule.setContent {
revealStateOne = rememberRevealState()
revealStateTwo = rememberRevealState()
@@ -292,7 +296,7 @@
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
- revealStateOne.snapTo(RevealValue.Revealing) // First change
+ revealStateOne.snapTo(RevealValue.RightRevealing) // First change
revealStateOne.snapTo(lastValue) // Second change, same state
}
}
@@ -304,7 +308,7 @@
fun onSecondaryActionClick_setsLastClickAction() =
verifyLastClickAction(
expectedClickType = RevealActionType.SecondaryAction,
- initialRevealValue = RevealValue.Revealing,
+ initialRevealValue = RevealValue.RightRevealing,
secondaryActionModifier = Modifier.testTag(TEST_TAG)
)
@@ -312,7 +316,7 @@
fun onPrimaryActionClick_setsLastClickAction() =
verifyLastClickAction(
expectedClickType = RevealActionType.PrimaryAction,
- initialRevealValue = RevealValue.Revealing,
+ initialRevealValue = RevealValue.RightRevealing,
primaryActionModifier = Modifier.testTag(TEST_TAG)
)
@@ -320,7 +324,7 @@
fun onUndoActionClick_setsLastClickAction() =
verifyLastClickAction(
expectedClickType = RevealActionType.UndoAction,
- initialRevealValue = RevealValue.Revealed,
+ initialRevealValue = RevealValue.RightRevealed,
undoActionModifier = Modifier.testTag(TEST_TAG)
)
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index 26080e1..1c4a997 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -85,12 +85,44 @@
/** Standard easing for Swipe To Reveal. */
internal val STANDARD_IN_OUT = CubicBezierEasing(0.20f, 0.0f, 0.0f, 1.00f)
-/** Different values which the swipeable modifier can be configured to. */
+/**
+ * Different values that determine the state of the [SwipeToReveal] composable, reflected in
+ * [RevealState.currentValue]. [RevealValue.Covered] is considered the default state where none of
+ * the actions are revealed yet.
+ *
+ * [SwipeToReveal] direction is not localised, with the default being [SwipeDirection.RightToLeft],
+ * and [RevealValue.RightRevealing] and [RevealValue.RightRevealed] correspond to the actions
+ * getting revealed from the right side of the screen. In case swipe direction is set to
+ * [SwipeDirection.Both], actions can also get revealed from the left side of the screen, and in
+ * that case [RevealValue.LeftRevealing] and [RevealValue.LeftRevealed] are used.
+ *
+ * @see [SwipeDirection]
+ */
@ExperimentalWearFoundationApi
@JvmInline
-public value class RevealValue private constructor(val value: Int) {
+value class RevealValue private constructor(val value: Int) {
companion object {
/**
+ * The value which represents the state in which the whole revealable content is fully
+ * revealed, and they are displayed on the left side of the screen. This also represents the
+ * state in which one of the actions has been triggered/performed.
+ *
+ * This is only used when the swipe direction is set to [SwipeDirection.Both], and the user
+ * swipes from the left side of the screen.
+ */
+ val LeftRevealed = RevealValue(-2)
+
+ /**
+ * The value which represents the state in which all the actions are revealed and the top
+ * content is not being swiped. In this state, none of the actions have been triggered or
+ * performed yet, and they are displayed on the left side of the screen.
+ *
+ * This is only used when the swipe direction is set to [SwipeDirection.Both], and the user
+ * swipes from the left side of the screen.
+ */
+ val LeftRevealing = RevealValue(-1)
+
+ /**
* The default first value which generally represents the state where the revealable actions
* have not been revealed yet. In this state, none of the actions have been triggered or
* performed yet.
@@ -101,15 +133,64 @@
* The value which represents the state in which all the actions are revealed and the top
* content is not being swiped. In this state, none of the actions have been triggered or
* performed yet.
+ *
+ * @deprecated Use [RightRevealing] instead.
*/
+ @Deprecated("Use RightRevealing instead.", ReplaceWith("RightRevealing"))
val Revealing = RevealValue(1)
/**
* The value which represents the state in which the whole revealable content is fully
* revealed. This also represents the state in which one of the actions has been
* triggered/performed.
+ *
+ * @deprecated Use [RightRevealed] instead.
*/
+ @Deprecated("Use RightRevealed instead.", ReplaceWith("RightRevealed"))
val Revealed = RevealValue(2)
+
+ /**
+ * The value which represents the state in which all the actions are revealed and the top
+ * content is not being swiped. In this state, none of the actions have been triggered or
+ * performed yet, and they are displayed on the right side of the screen.
+ */
+ val RightRevealing = RevealValue(1)
+
+ /**
+ * The value which represents the state in which the whole revealable content is fully
+ * revealed, and the actions are revealed on the right side of the screen. This also
+ * represents the state in which one of the actions has been triggered/performed.
+ */
+ val RightRevealed = RevealValue(2)
+ }
+}
+
+/**
+ * Different values [SwipeToReveal] composable can reveal the actions from.
+ *
+ * [SwipeDirection] is not localised, with the default being [SwipeDirection.RightToLeft] to prevent
+ * conflict with the system-wide swipe to dismiss gesture in an activity, so it's strongly advised
+ * to respect the default value to avoid conflicting gestures.
+ */
+@ExperimentalWearFoundationApi
+@JvmInline
+value class SwipeDirection private constructor(val value: Int) {
+ companion object {
+ /**
+ * The default value which allows the user to swipe right to left to reveal or execute the
+ * actions. It's strongly advised to respect the default behavior to avoid conflict with the
+ * swipe-to-dismiss gesture.
+ */
+ val RightToLeft = SwipeDirection(0)
+
+ /**
+ * The value which allows the user to swipe in either direction to reveal or execute the
+ * actions. This should not be used if the component is used in an activity as the gesture
+ * might conflict with the swipe-to-dismiss gesture and could be confusing for the users.
+ * This is only supported for rare cases where the current screen does not support swipe to
+ * dismiss.
+ */
+ val Both = SwipeDirection(1)
}
}
@@ -120,7 +201,7 @@
*/
@ExperimentalWearFoundationApi
@JvmInline
-public value class RevealActionType private constructor(val value: Int) {
+value class RevealActionType private constructor(val value: Int) {
companion object {
/**
* Represents the primary action composable of [SwipeToReveal]. This corresponds to the
@@ -151,30 +232,45 @@
* width of the top content starting from right and ending on left.
*
* @param coveredAnchor Anchor for the [RevealValue.Covered] value
- * @param revealingAnchor Anchor for the [RevealValue.Revealing] value
- * @param revealedAnchor Anchor for the [RevealValue.Revealed] value
+ * @param revealingAnchor Anchor for the [RevealValue.LeftRevealing] or [RevealValue.RightRevealing]
+ * value
+ * @param revealedAnchor Anchor for the [RevealValue.LeftRevealed] or [RevealValue.RightRevealed]
+ * value
+ * @param swipeDirection The direction in which the content can be swiped. It's strongly advised to
+ * keep the default [SwipeDirection.RightToLeft] in order to preserve compatibility with the
+ * system wide swipe to dismiss gesture.
*/
@ExperimentalWearFoundationApi
-public fun createAnchors(
+fun createAnchors(
coveredAnchor: Float = 0f,
revealingAnchor: Float = SwipeToRevealDefaults.revealingRatio,
- revealedAnchor: Float = 1f
+ revealedAnchor: Float = 1f,
+ swipeDirection: SwipeDirection = SwipeDirection.RightToLeft
): Map<RevealValue, Float> {
+ if (swipeDirection == SwipeDirection.Both) {
+ return mapOf(
+ RevealValue.LeftRevealed to -revealedAnchor,
+ RevealValue.LeftRevealing to -revealingAnchor,
+ RevealValue.Covered to coveredAnchor,
+ RevealValue.RightRevealing to revealingAnchor,
+ RevealValue.RightRevealed to revealedAnchor
+ )
+ }
return mapOf(
RevealValue.Covered to coveredAnchor,
- RevealValue.Revealing to revealingAnchor,
- RevealValue.Revealed to revealedAnchor
+ RevealValue.RightRevealing to revealingAnchor,
+ RevealValue.RightRevealed to revealedAnchor
)
}
/**
- * A class to keep track of the state of the composable. It can be used to customise the behaviour
+ * A class to keep track of the state of the composable. It can be used to customise the behavior
* and state of the composable.
*
* @constructor Create a [RevealState].
*/
@ExperimentalWearFoundationApi
-public class RevealState
+class RevealState
internal constructor(
initialValue: RevealValue,
animationSpec: AnimationSpec<Float>,
@@ -182,7 +278,7 @@
positionalThreshold: Density.(totalDistance: Float) -> Float,
internal val anchors: Map<RevealValue, Float>,
internal val coroutineScope: CoroutineScope,
- internal val nestedScrollDispatcher: NestedScrollDispatcher
+ internal val nestedScrollDispatcher: NestedScrollDispatcher,
) {
/** [SwipeableV2State] internal instance for the state. */
internal val swipeableState =
@@ -193,17 +289,17 @@
confirmValueChangeAndReset(confirmValueChange, revealValue)
},
positionalThreshold = positionalThreshold,
- nestedScrollDispatcher = nestedScrollDispatcher
+ nestedScrollDispatcher = nestedScrollDispatcher,
)
- public var lastActionType by mutableStateOf(RevealActionType.None)
+ var lastActionType by mutableStateOf(RevealActionType.None)
/**
* The current [RevealValue] based on the status of the component.
*
* @see Modifier.swipeableV2
*/
- public val currentValue: RevealValue
+ val currentValue: RevealValue
get() = swipeableState.currentValue
/**
@@ -213,7 +309,7 @@
*
* @see Modifier.swipeableV2
*/
- public val targetValue: RevealValue
+ val targetValue: RevealValue
get() = swipeableState.targetValue
/**
@@ -221,7 +317,7 @@
*
* @see Modifier.swipeableV2
*/
- public val isAnimationRunning: Boolean
+ val isAnimationRunning: Boolean
get() = swipeableState.isAnimationRunning
/**
@@ -229,7 +325,7 @@
*
* @see Modifier.swipeableV2
*/
- public val offset: Float
+ val offset: Float
get() = swipeableState.offset ?: 0f
/**
@@ -239,7 +335,7 @@
*
* @see Modifier.swipeableV2
*/
- public val swipeAnchors: Map<RevealValue, Float>
+ val swipeAnchors: Map<RevealValue, Float>
get() = anchors
/**
@@ -248,7 +344,7 @@
* @param targetValue The target [RevealValue] where the [currentValue] will be changed to.
* @see Modifier.swipeableV2
*/
- public suspend fun snapTo(targetValue: RevealValue) {
+ suspend fun snapTo(targetValue: RevealValue) {
// Cover the previously open component if revealing a different one
if (targetValue != RevealValue.Covered) {
resetLastState(this)
@@ -261,7 +357,7 @@
*
* @param targetValue The target [RevealValue] where the [currentValue] will animate to.
*/
- public suspend fun animateTo(targetValue: RevealValue) {
+ suspend fun animateTo(targetValue: RevealValue) {
// Cover the previously open component if revealing a different one
if (targetValue != RevealValue.Covered) {
resetLastState(this)
@@ -297,12 +393,12 @@
/**
* Resets last state if a different SwipeToReveal is being moved to new anchor and the last
- * state is in [RevealValue.Revealing] mode which represents no action has been performed yet.
- * In [RevealValue.Revealed], the action has been performed and it will not be reset.
+ * state is in [RevealValue.RightRevealing] mode which represents no action has been performed
+ * yet. In [RevealValue.RightRevealed], the action has been performed and it will not be reset.
*/
private suspend fun resetLastState(currentState: RevealState) {
val oldState = SingleSwipeCoordinator.lastUpdatedState.getAndSet(currentState)
- if (currentState != oldState && oldState?.currentValue == RevealValue.Revealing) {
+ if (currentState != oldState && oldState?.currentValue == RevealValue.RightRevealing) {
oldState.animateTo(RevealValue.Covered)
}
}
@@ -329,7 +425,7 @@
*/
@ExperimentalWearFoundationApi
@Composable
-public fun rememberRevealState(
+fun rememberRevealState(
initialValue: RevealValue = RevealValue.Covered,
animationSpec: AnimationSpec<Float> = SwipeToRevealDefaults.animationSpec,
confirmValueChange: (RevealValue) -> Boolean = { true },
@@ -347,7 +443,7 @@
positionalThreshold = positionalThreshold,
anchors = anchors,
coroutineScope = coroutineScope,
- nestedScrollDispatcher = nestedScrollDispatcher
+ nestedScrollDispatcher = nestedScrollDispatcher,
)
}
}
@@ -362,7 +458,7 @@
* triggered.
*
* An optional undo action can also be added. This undo action will be visible to users once the
- * [RevealValue] becomes [RevealValue.Revealed].
+ * [RevealValue] becomes [RevealValue.RightRevealed].
*
* It is strongly recommended to have icons represent the actions and maybe a text and icon for the
* undo action.
@@ -388,12 +484,12 @@
* @param secondaryAction An optional action that can be added to the component. We strongly
* recommend triggering the action when it is clicked.
* @param undoAction The optional undo action that will be applied to the component once the the
- * [RevealState.currentValue] becomes [RevealValue.Revealed].
+ * [RevealState.currentValue] becomes [RevealValue.RightRevealed].
* @param content The content that will be initially displayed over the other actions provided.
*/
@ExperimentalWearFoundationApi
@Composable
-public fun SwipeToReveal(
+fun SwipeToReveal(
primaryAction: @Composable RevealScope.() -> Unit,
modifier: Modifier = Modifier,
onFullSwipe: () -> Unit = {},
@@ -415,7 +511,7 @@
.swipeableV2(
state = state.swipeableState,
orientation = Orientation.Horizontal,
- enabled = state.currentValue != RevealValue.Revealed,
+ enabled = state.currentValue != RevealValue.RightRevealed,
)
.swipeAnchors(
state = state.swipeableState,
@@ -433,11 +529,14 @@
// connection applied before this modifier consume the scroll/fling events.
.nestedScroll(noOpNestedScrollConnection, state.nestedScrollDispatcher)
) {
- val swipeCompleted = state.currentValue == RevealValue.Revealed
+ val swipeCompleted =
+ state.currentValue == RevealValue.RightRevealed ||
+ state.currentValue == RevealValue.LeftRevealed
val lastActionIsSecondary = state.lastActionType == RevealActionType.SecondaryAction
val isWithinRevealOffset by remember {
derivedStateOf { abs(state.offset) <= revealScope.revealOffset }
}
+ val canSwipeRight = (state.swipeAnchors.minOfOrNull { (_, offset) -> offset } ?: 0f) < 0f
// Determines whether the secondary action will be visible based on the current
// reveal offset
@@ -447,13 +546,21 @@
// when secondary action is clicked
val hideActions = !isWithinRevealOffset && lastActionIsSecondary
- val shouldDrawActions by remember { derivedStateOf { abs(state.offset) > 0 } }
+ val swipingRight by remember { derivedStateOf { state.offset > 0 } }
+
+ // Don't draw actions on the left side if the user cannot swipe right, and they are
+ // currently swiping right
+ val shouldDrawActions by remember {
+ derivedStateOf { abs(state.offset) > 0 && (canSwipeRight || !swipingRight) }
+ }
// Draw the buttons only when offset is greater than zero.
if (shouldDrawActions) {
Box(
modifier = Modifier.matchParentSize(),
- contentAlignment = AbsoluteAlignment.CenterRight
+ contentAlignment =
+ if (swipingRight) AbsoluteAlignment.CenterLeft
+ else AbsoluteAlignment.CenterRight
) {
AnimatedContent(
targetState = swipeCompleted && undoAction != null,
@@ -544,22 +651,41 @@
},
horizontalArrangement = Arrangement.Absolute.Right
) {
- // weight cannot be 0 so remove the composable when weight becomes 0
- if (secondaryAction != null && secondaryActionWeight.value > 0) {
+ if (!swipingRight) {
+ // weight cannot be 0 so remove the composable when weight becomes 0
+ if (secondaryAction != null && secondaryActionWeight.value > 0) {
+ Spacer(Modifier.size(SwipeToRevealDefaults.padding))
+ ActionSlot(
+ revealScope,
+ weight = secondaryActionWeight.value,
+ opacity = secondaryActionAlpha,
+ content = secondaryAction,
+ )
+ }
Spacer(Modifier.size(SwipeToRevealDefaults.padding))
ActionSlot(
revealScope,
- weight = secondaryActionWeight.value,
- opacity = secondaryActionAlpha,
- content = secondaryAction,
+ content = primaryAction,
+ opacity = primaryActionAlpha
)
+ } else {
+ ActionSlot(
+ revealScope,
+ content = primaryAction,
+ opacity = primaryActionAlpha
+ )
+ Spacer(Modifier.size(SwipeToRevealDefaults.padding))
+ // weight cannot be 0 so remove the composable when weight becomes 0
+ if (secondaryAction != null && secondaryActionWeight.value > 0) {
+ ActionSlot(
+ revealScope,
+ weight = secondaryActionWeight.value,
+ opacity = secondaryActionAlpha,
+ content = secondaryAction,
+ )
+ Spacer(Modifier.size(SwipeToRevealDefaults.padding))
+ }
}
- Spacer(Modifier.size(SwipeToRevealDefaults.padding))
- ActionSlot(
- revealScope,
- content = primaryAction,
- opacity = primaryActionAlpha
- )
}
}
}
@@ -568,14 +694,15 @@
Row(
modifier =
Modifier.absoluteOffset {
- IntOffset(x = state.requireOffset().roundToInt().coerceAtMost(0), y = 0)
+ val xOffset = state.requireOffset().roundToInt()
+ IntOffset(x = if (canSwipeRight) xOffset else xOffset.coerceAtMost(0), y = 0)
}
) {
content()
}
LaunchedEffect(state.currentValue) {
if (
- state.currentValue == RevealValue.Revealed &&
+ state.currentValue == RevealValue.RightRevealed &&
state.lastActionType == RevealActionType.None
) {
onFullSwipe()
@@ -585,22 +712,22 @@
}
@ExperimentalWearFoundationApi
-public interface RevealScope {
+interface RevealScope {
/**
* The offset, in pixels, where the revealed actions are fully visible but the existing content
* would be left in place if the reveal action was stopped. This offset is used to create the
- * anchor for [RevealValue.Revealing]. If there is no such anchor defined for
- * [RevealValue.Revealing], it returns 0.0f.
+ * anchor for [RevealValue.RightRevealing]. If there is no such anchor defined for
+ * [RevealValue.RightRevealing], it returns 0.0f.
*/
/* @FloatRange(from = 0.0) */
- public val revealOffset: Float
+ val revealOffset: Float
/**
* The last [RevealActionType] that was set in [RevealState]. This may not be set if the state
* changed via interaction and not through API call.
*/
- public val lastActionType: RevealActionType
+ val lastActionType: RevealActionType
}
@OptIn(ExperimentalWearFoundationApi::class)
@@ -616,7 +743,7 @@
val width = mutableFloatStateOf(0.0f)
override val revealOffset: Float
- get() = width.floatValue * (revealState.swipeAnchors[RevealValue.Revealing] ?: 0.0f)
+ get() = width.floatValue * (revealState.swipeAnchors[RevealValue.RightRevealing] ?: 0.0f)
override val lastActionType: RevealActionType
get() = revealState.lastActionType
@@ -632,8 +759,8 @@
internal val padding = 2.dp
/**
- * Default ratio of the content displayed when in [RevealValue.Revealing] state, i.e. all the
- * actions are revealed and the top content is not being swiped. For example, a value of 0.7
+ * Default ratio of the content displayed when in [RevealValue.RightRevealing] state, i.e. all
+ * the actions are revealed and the top content is not being swiped. For example, a value of 0.7
* means that 70% of the width is used to place the actions.
*/
internal const val revealingRatio = 0.7f
@@ -642,9 +769,9 @@
* Default position threshold that needs to be swiped in order to transition to the next state.
* Used in conjunction with [revealingRatio]; for example, a threshold of 0.5 with a revealing
* ratio of 0.7 means that the user needs to swipe at least 35% (0.5 * 0.7) of the component
- * width to go from [RevealValue.Covered] to [RevealValue.Revealing] and at least 85% (0.7 +
- * 0.5 * (1 - 0.7)) of the component width to go from [RevealValue.Revealing] to
- * [RevealValue.Revealed].
+ * width to go from [RevealValue.Covered] to [RevealValue.RightRevealing] and at least 85%
+ * (0.7 + 0.5 * (1 - 0.7)) of the component width to go from [RevealValue.RightRevealing] to
+ * [RevealValue.RightRevealed].
*/
internal val positionalThreshold = fractionalPositionalThreshold(0.5f)
}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt
index 6dd1581a..0b23556 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealScreenshotTest.kt
@@ -56,7 +56,7 @@
fun swipeToRevealCard_singleAction() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealCard(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing),
secondaryAction = null
)
}
@@ -66,7 +66,7 @@
fun swipeToRevealChip_singleAction() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealChip(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing),
secondaryAction = null
)
}
@@ -76,7 +76,7 @@
fun swipeToRevealCard_twoActions() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealCard(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
)
}
}
@@ -85,7 +85,7 @@
fun swipeToRevealChip_twoActions() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealChip(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
)
}
}
@@ -94,7 +94,7 @@
fun swipeToRevealChip_undoPrimaryAction() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealChip(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
}
@@ -103,7 +103,7 @@
fun swipeToRevealCard_undoPrimaryAction() {
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
swipeToRevealCard(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
}
@@ -113,7 +113,7 @@
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
val revealState = rememberRevealState()
val coroutineScope = rememberCoroutineScope()
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch { revealState.animateTo(RevealValue.RightRevealed) }
revealState.lastActionType = RevealActionType.SecondaryAction
swipeToRevealChip(revealState = revealState)
}
@@ -124,7 +124,7 @@
rule.verifyScreenshot(screenshotRule = screenshotRule, methodName = testName.methodName) {
val revealState = rememberRevealState()
val coroutineScope = rememberCoroutineScope()
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch { revealState.animateTo(RevealValue.RightRevealed) }
revealState.lastActionType = RevealActionType.SecondaryAction
swipeToRevealCard(revealState = revealState)
}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealTest.kt
index 80ce95b..686e659 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SwipeToRevealTest.kt
@@ -97,7 +97,7 @@
fun whenRevealing_actionsExist_inChip() {
rule.setContentWithTheme {
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
)
}
rule.onNodeWithTag(PRIMARY_ACTION_TAG).assertExists()
@@ -108,7 +108,7 @@
fun whenRevealing_actionsExist_inCard() {
rule.setContentWithTheme {
swipeToRevealCardDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
)
}
rule.onNodeWithTag(PRIMARY_ACTION_TAG).assertExists()
@@ -119,7 +119,7 @@
fun whenRevealed_undoActionExists_inChip() {
rule.setContentWithTheme {
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
@@ -130,7 +130,7 @@
fun whenRevealed_undoActionExists_inCard() {
rule.setContentWithTheme {
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
@@ -141,7 +141,7 @@
fun whenRevealed_actionsDoNotExist_inChip() {
rule.setContentWithTheme {
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
@@ -153,7 +153,7 @@
fun whenRevealed_actionsDoNotExist_inCard() {
rule.setContentWithTheme {
swipeToRevealCardDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealed)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed)
)
}
@@ -165,7 +165,7 @@
fun onPrimaryActionClick_triggersOnClick_forChip() {
var clicked = false
rule.setContentWithTheme {
- val revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ val revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
swipeToRevealChipDefault(
revealState = revealState,
primaryAction = {
@@ -182,7 +182,7 @@
fun onSecondaryActionClick_triggersOnClick_forChip() {
var clicked = false
rule.setContentWithTheme {
- val revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ val revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
swipeToRevealChipDefault(
revealState = revealState,
secondaryAction = {
@@ -198,7 +198,7 @@
@Test
fun onPrimaryActionClickWithStateToRevealed_undoPrimaryActionCanBeClicked() {
rule.setContentWithTheme {
- val revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ val revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
val coroutineScope = rememberCoroutineScope()
swipeToRevealCardDefault(
revealState = revealState,
@@ -206,7 +206,9 @@
createPrimaryAction(
revealState = revealState,
onClick = {
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
}
)
},
@@ -225,7 +227,7 @@
fun onPrimaryActionClickAndPrimaryUndoClicked_stateChangesToCovered() {
lateinit var revealState: RevealState
rule.setContentWithTheme {
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
val coroutineScope = rememberCoroutineScope()
swipeToRevealCardDefault(
revealState = revealState,
@@ -233,7 +235,9 @@
createPrimaryAction(
revealState = revealState,
onClick = {
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
}
)
},
@@ -266,7 +270,7 @@
@Test
fun onSecondaryActionClickWithStateToRevealed_undoSecondaryActionCanBeClicked() {
rule.setContentWithTheme {
- val revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ val revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
val coroutineScope = rememberCoroutineScope()
swipeToRevealCardDefault(
revealState = revealState,
@@ -274,7 +278,9 @@
createSecondaryAction(
revealState = revealState,
onClick = {
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
}
)
},
@@ -293,7 +299,7 @@
fun onSecondaryActionClickAndUndoSecondaryClicked_stateChangesToCovered() {
lateinit var revealState: RevealState
rule.setContentWithTheme {
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
val coroutineScope = rememberCoroutineScope()
swipeToRevealCardDefault(
revealState = revealState,
@@ -301,7 +307,9 @@
createSecondaryAction(
revealState = revealState,
onClick = {
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
}
)
},
@@ -341,7 +349,7 @@
primaryActionColor = MaterialTheme.colors.error
secondaryActionColor = MaterialTheme.colors.surface
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing)
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing)
)
}
@@ -362,7 +370,7 @@
val overrideSecondaryActionColor = Color.Green
rule.setContentWithTheme {
swipeToRevealChipDefault(
- revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing),
colors =
SwipeToRevealDefaults.actionColors(
primaryActionBackgroundColor = overridePrimaryActionColor,
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 308b0e6..7dd366a 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -6,20 +6,19 @@
method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void GroupSeparator();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues confirmDismissContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
- method public float getEdgeButtonExtraTopPadding();
method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
- property public final float edgeButtonExtraTopPadding;
field public static final androidx.wear.compose.material3.AlertDialogDefaults INSTANCE;
}
public final class AlertDialogKt {
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
}
@@ -453,16 +452,12 @@
}
public final class EdgeButtonKt {
- method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float buttonHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float preferredHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
@SuppressCompatibility @kotlin.RequiresOptIn(message="This Wear Material3 API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterial3Api {
}
- public final class HorizontalPageIndicatorKt {
- method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(int pageCount, int currentPage, kotlin.jvm.functions.Function0<java.lang.Float> currentPageOffsetFraction, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional float indicatorSize, optional float spacing);
- }
-
@androidx.compose.runtime.Immutable public final class IconButtonColors {
ctor public IconButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
method public long getContainerColor();
@@ -704,6 +699,21 @@
method @androidx.compose.runtime.Composable public static void OpenOnPhoneDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.OpenOnPhoneDialogColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class PageIndicatorDefaults {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getBackgroundColor();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getSelectedColor();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getUnselectedColor();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long backgroundColor;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long selectedColor;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long unselectedColor;
+ field public static final androidx.wear.compose.material3.PageIndicatorDefaults INSTANCE;
+ }
+
+ public final class PageIndicatorKt {
+ method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional long backgroundColor);
+ method @androidx.compose.runtime.Composable public static void VerticalPageIndicator(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional long backgroundColor);
+ }
+
public final class PickerDefaults {
method public float getGradientRatio();
method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior rotarySnapBehavior(androidx.wear.compose.material3.PickerState state);
@@ -1185,7 +1195,7 @@
public final class SwipeToRevealKt {
method @androidx.compose.runtime.Composable public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.SwipeToRevealScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.RevealState revealState, optional float actionButtonHeight, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions);
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions, optional int swipeDirection);
}
public final class SwipeToRevealScope {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 308b0e6..7dd366a 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -6,20 +6,19 @@
method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void GroupSeparator();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues confirmDismissContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
- method public float getEdgeButtonExtraTopPadding();
method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
- property public final float edgeButtonExtraTopPadding;
field public static final androidx.wear.compose.material3.AlertDialogDefaults INSTANCE;
}
public final class AlertDialogKt {
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
}
@@ -453,16 +452,12 @@
}
public final class EdgeButtonKt {
- method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float buttonHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float preferredHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
@SuppressCompatibility @kotlin.RequiresOptIn(message="This Wear Material3 API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterial3Api {
}
- public final class HorizontalPageIndicatorKt {
- method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(int pageCount, int currentPage, kotlin.jvm.functions.Function0<java.lang.Float> currentPageOffsetFraction, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional float indicatorSize, optional float spacing);
- }
-
@androidx.compose.runtime.Immutable public final class IconButtonColors {
ctor public IconButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
method public long getContainerColor();
@@ -704,6 +699,21 @@
method @androidx.compose.runtime.Composable public static void OpenOnPhoneDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.OpenOnPhoneDialogColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class PageIndicatorDefaults {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getBackgroundColor();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getSelectedColor();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getUnselectedColor();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long backgroundColor;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long selectedColor;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long unselectedColor;
+ field public static final androidx.wear.compose.material3.PageIndicatorDefaults INSTANCE;
+ }
+
+ public final class PageIndicatorKt {
+ method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional long backgroundColor);
+ method @androidx.compose.runtime.Composable public static void VerticalPageIndicator(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long selectedColor, optional long unselectedColor, optional long backgroundColor);
+ }
+
public final class PickerDefaults {
method public float getGradientRatio();
method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior rotarySnapBehavior(androidx.wear.compose.material3.PickerState state);
@@ -1185,7 +1195,7 @@
public final class SwipeToRevealKt {
method @androidx.compose.runtime.Composable public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.material3.SwipeToRevealScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.RevealState revealState, optional float actionButtonHeight, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions);
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.RevealState rememberRevealState(optional int initialValue, optional float anchorWidth, optional boolean useAnchoredActions, optional int swipeDirection);
}
public final class SwipeToRevealScope {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
index 4440425..c8618c6 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
@@ -23,6 +23,9 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -41,6 +44,8 @@
import androidx.wear.compose.material3.AlertDialog
import androidx.wear.compose.material3.AlertDialogDefaults
import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.RadioButton
@@ -56,6 +61,7 @@
ComposableDemo("Bottom button") { AlertDialogWithBottomButtonSample() },
ComposableDemo("Confirm and Dismiss") { AlertDialogWithConfirmAndDismissSample() },
ComposableDemo("Content groups") { AlertDialogWithContentGroupsSample() },
+ ComposableDemo("Button stack") { AlertDialogWithButtonStack() },
ComposableDemo("AlertDialog builder") { AlertDialogBuilder() },
)
@@ -67,7 +73,7 @@
var showMessage by remember { mutableStateOf(false) }
var showSecondaryButton by remember { mutableStateOf(false) }
var showCaption by remember { mutableStateOf(false) }
- var showTwoButtons by remember { mutableStateOf(false) }
+ var buttonsType by remember { mutableStateOf(AlertButtonsType.BOTTOM_BUTTON) }
var showDialog by remember { mutableStateOf(false) }
@@ -121,19 +127,27 @@
item {
RadioButton(
modifier = Modifier.fillMaxWidth(),
- selected = !showTwoButtons,
- onSelect = { showTwoButtons = false },
+ selected = buttonsType == AlertButtonsType.BOTTOM_BUTTON,
+ onSelect = { buttonsType = AlertButtonsType.BOTTOM_BUTTON },
label = { Text("Single Bottom button") },
)
}
item {
RadioButton(
modifier = Modifier.fillMaxWidth(),
- selected = showTwoButtons,
- onSelect = { showTwoButtons = true },
+ selected = buttonsType == AlertButtonsType.CONFIRM_DISMISS,
+ onSelect = { buttonsType = AlertButtonsType.CONFIRM_DISMISS },
label = { Text("Ok/Cancel buttons") },
)
}
+ item {
+ RadioButton(
+ modifier = Modifier.fillMaxWidth(),
+ selected = buttonsType == AlertButtonsType.NO_BUTTONS,
+ onSelect = { buttonsType = AlertButtonsType.NO_BUTTONS },
+ label = { Text("No bottom button") },
+ )
+ }
item { Button(onClick = { showDialog = true }, label = { Text("Show dialog") }) }
}
}
@@ -145,7 +159,7 @@
showCaption = showCaption,
showSecondaryButton = showSecondaryButton,
showMessage = showMessage,
- showTwoButtons = showTwoButtons,
+ buttonsType = buttonsType,
onConfirmButton = { showDialog = false },
onDismissRequest = { showDialog = false }
)
@@ -153,6 +167,60 @@
}
@Composable
+fun AlertDialogWithButtonStack() {
+ var showDialog by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showDialog = true },
+ label = { Text("Show Dialog") }
+ )
+ }
+
+ AlertDialog(
+ show = showDialog,
+ onDismissRequest = { showDialog = false },
+ icon = {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ modifier = Modifier.size(32.dp),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ title = { Text("Allow access to your photos?") },
+ text = { Text("Lerp ipsum dolor sit amet.") },
+ bottomButton = null,
+ ) {
+ item {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {},
+ label = { Text("While using app") },
+ icon = { Icon(Icons.Filled.Check, "Check") }
+ )
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {},
+ label = { Text("Ask every time") },
+ icon = { Icon(Icons.Filled.Check, "Check") }
+ )
+ }
+ item {
+ FilledTonalButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {},
+ label = { Text("Don't allow") },
+ icon = { Icon(Icons.Filled.Check, "Check") }
+ )
+ }
+ }
+}
+
+@Composable
private fun CustomAlertDialog(
show: Boolean,
onDismissRequest: () -> Unit,
@@ -160,7 +228,7 @@
properties: DialogProperties = DialogProperties(),
showIcon: Boolean,
onConfirmButton: () -> Unit,
- showTwoButtons: Boolean,
+ buttonsType: AlertButtonsType,
showMessage: Boolean,
showSecondaryButton: Boolean,
showCaption: Boolean,
@@ -183,15 +251,15 @@
{ Message() }
} else null,
onConfirmButton =
- if (showTwoButtons) {
+ if (buttonsType == AlertButtonsType.CONFIRM_DISMISS) {
onConfirmButton
} else null,
onDismissButton =
- if (showTwoButtons) {
+ if (buttonsType == AlertButtonsType.CONFIRM_DISMISS) {
{ /* dismiss action */ }
} else null,
onBottomButton =
- if (!showTwoButtons) {
+ if (buttonsType == AlertButtonsType.BOTTOM_BUTTON) {
onConfirmButton
} else null,
content =
@@ -202,7 +270,7 @@
}
if (showCaption) {
item { Caption(captionHorizontalPadding) }
- if (!showTwoButtons) {
+ if (buttonsType == AlertButtonsType.BOTTOM_BUTTON) {
item { AlertDialogDefaults.GroupSeparator() }
}
}
@@ -279,7 +347,7 @@
dismissButton = { AlertDialogDefaults.DismissButton(onDismissButton) },
content = content
)
- } else if (onBottomButton != null) {
+ } else
AlertDialog(
show = show,
onDismissRequest = onDismissRequest,
@@ -288,8 +356,16 @@
title = title,
icon = icon,
text = message,
- bottomButton = { AlertDialogDefaults.BottomButton(onBottomButton) },
+ bottomButton =
+ if (onBottomButton != null) {
+ { AlertDialogDefaults.BottomButton(onBottomButton) }
+ } else null,
content = content
)
- }
+}
+
+private enum class AlertButtonsType {
+ NO_BUTTONS,
+ BOTTOM_BUTTON,
+ CONFIRM_DISMISS
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index d93f73d..ba4a754 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -72,7 +72,7 @@
bottomButton = {
EdgeButton(
onClick = {},
- buttonHeight = ButtonDefaults.EdgeButtonHeightLarge,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
) {
Text(labels[selectedLabel.intValue], color = Color.White)
@@ -120,7 +120,7 @@
bottomButton = {
EdgeButton(
onClick = {},
- buttonHeight = ButtonDefaults.EdgeButtonHeightLarge,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
) {
Text(labels[selectedLabel.intValue], color = Color.White)
@@ -215,7 +215,7 @@
EdgeButton(
onClick = {},
enabled = colorNames[color] != "D",
- buttonHeight = sizes[size],
+ preferredHeight = sizes[size],
colors = colors[color],
border =
if (colorNames[color] == "O")
@@ -257,7 +257,7 @@
bottomButton = {
EdgeButton(
onClick = {},
- buttonHeight = sizes[selectedSize].second,
+ preferredHeight = sizes[selectedSize].second,
colors = colors[selectedColor].second,
border =
if (colors[selectedColor].first == "Outlined")
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/HapticsDemo.kt
similarity index 65%
rename from wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt
rename to wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/HapticsDemo.kt
index 8a9bbea..4ecf02f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/HapticsDemo.kt
@@ -16,14 +16,29 @@
package androidx.wear.compose.material3.demos
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
import android.view.HapticFeedbackConstants
import android.view.View
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.Text
@@ -63,7 +78,26 @@
Pair(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE, "Virtual Key Release"),
)
+ val premiumVibratorEnabled = isPremiumVibratorEnabled()
+
ScalingLazyDemo {
+ item { ListHeader { Text("Premium Haptics") } }
+ item {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector =
+ if (premiumVibratorEnabled) Icons.Filled.Check else Icons.Filled.Close,
+ contentDescription = "Premium Haptics Status",
+ tint = if (premiumVibratorEnabled) Color.Green else Color.Red
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(if (premiumVibratorEnabled) "Enabled" else "Disabled")
+ }
+ }
item { ListHeader { Text("Haptic Constants") } }
items(hapticConstants.size) { index ->
val (constant, name) = hapticConstants[index]
@@ -92,3 +126,23 @@
view.performHapticFeedback(feedbackConstant)
}
}
+
+@Composable
+fun isPremiumVibratorEnabled(): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // NB whilst the 'areAllPrimitivesSupported' API needs R (API 30), we need S (API
+ // 31) so that PRIMITIVE_THUD is available.
+ val vibrator = LocalContext.current.getSystemService(Vibrator::class.java)
+ if (
+ vibrator.areAllPrimitivesSupported(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ VibrationEffect.Composition.PRIMITIVE_THUD
+ )
+ ) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt
index 0c777e61..85b99df 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwipeToRevealDemo.kt
@@ -28,6 +28,7 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -40,6 +41,7 @@
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.RevealActionType
import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeDirection
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Card
@@ -54,6 +56,71 @@
@OptIn(ExperimentalWearFoundationApi::class)
@Composable
+fun SwipeToRevealBothDirectionsNonAnchoring() {
+ SwipeToReveal(
+ revealState =
+ rememberRevealState(
+ swipeDirection = SwipeDirection.Both,
+ useAnchoredActions = false,
+ ),
+ actions = {
+ primaryAction(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ undoPrimaryAction(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ label = "Undo Delete"
+ )
+ }
+ ) {
+ Button(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text("This Button has only one action", modifier = Modifier.fillMaxSize())
+ }
+ }
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealBothDirections() {
+ SwipeToReveal(
+ revealState =
+ rememberRevealState(
+ // Use the double action anchor width when revealing two actions
+ anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth,
+ swipeDirection = SwipeDirection.Both
+ ),
+ actions = {
+ primaryAction(
+ onClick = { /* This block is called when the primary action is executed. */ },
+ icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
+ label = "Delete"
+ )
+ secondaryAction(
+ onClick = { /* This block is called when the secondary action is executed. */ },
+ icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "More") },
+ label = "More"
+ )
+ undoPrimaryAction(
+ onClick = { /* This block is called when the undo primary action is executed. */ },
+ label = "Undo Delete"
+ )
+ undoSecondaryAction(
+ onClick = { /* This block is called when the undo secondary action is executed. */
+ },
+ label = "Undo Secondary"
+ )
+ }
+ ) {
+ Button(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Text("This Button has two actions", modifier = Modifier.fillMaxSize())
+ }
+ }
+}
+
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
fun SwipeToRevealTwoActionsWithUndo() {
val context = LocalContext.current
val showToasts = remember { mutableStateOf(true) }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt
index 92d8d87..20d4ca7 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt
@@ -18,11 +18,8 @@
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
@@ -32,7 +29,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Icon
@@ -71,16 +67,14 @@
initialTime = timePickerTime
)
} else {
- Column(
+ Box(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
+ contentAlignment = Alignment.Center,
) {
- Text("Selected Time")
- Spacer(Modifier.height(12.dp))
Button(
onClick = { showTimePicker = true },
- label = { Text(timePickerTime.format(formatter)) },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
)
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
index 37bfa5b..8c634101c 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.material3.demos
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@@ -52,7 +53,8 @@
ComposableDemo("Clock with Icon") { TimeTextWithIcon() },
ComposableDemo("Clock with custom colors") { TimeTextWithCustomColors() },
ComposableDemo("Clock with custom font size") { TimeTextCustomSize() },
- ComposableDemo("Clock on list") { TimeTextOnScreen() }
+ ComposableDemo("Clock on list") { TimeTextOnScreen() },
+ ComposableDemo("Clock on white background") { TimeTextOnScreenWhiteBackground() }
)
@Composable
@@ -150,3 +152,8 @@
TimeText { time() }
}
}
+
+@Composable
+fun TimeTextOnScreenWhiteBackground() {
+ Box(Modifier.fillMaxSize().background(Color.White)) { TimeText { time() } }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
index 3be3531..d2db0be 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
@@ -71,7 +71,7 @@
ComposableDemo("Display Small") {
Centralize {
Text(
- "Display Small",
+ "Display\nSmall",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall
)
@@ -80,7 +80,7 @@
ComposableDemo("Display Medium") {
Centralize {
Text(
- "Display Medium",
+ "Display\nMedium",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displayMedium
)
@@ -89,7 +89,7 @@
ComposableDemo("Display Large") {
Centralize {
Text(
- "Display Large",
+ "Display\nLarge",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displayLarge
)
@@ -100,28 +100,68 @@
ComposableDemo("Title") {
Centralize {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text("Title Small", style = MaterialTheme.typography.titleSmall)
- Text("Title Medium", style = MaterialTheme.typography.titleMedium)
- Text("Title Large", style = MaterialTheme.typography.titleLarge)
+ Text(
+ "Title\nSmall",
+ style = MaterialTheme.typography.titleSmall,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Title\nMedium",
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Title\nLarge",
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center
+ )
}
}
},
ComposableDemo("Label") {
Centralize {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text("Label Small", style = MaterialTheme.typography.labelSmall)
- Text("Label Medium", style = MaterialTheme.typography.labelMedium)
- Text("Label Large", style = MaterialTheme.typography.labelLarge)
+ Text(
+ "Label\nSmall",
+ style = MaterialTheme.typography.labelSmall,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Label\nMedium",
+ style = MaterialTheme.typography.labelMedium,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Label\nLarge",
+ style = MaterialTheme.typography.labelLarge,
+ textAlign = TextAlign.Center
+ )
}
}
},
ComposableDemo("Body") {
Centralize {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text("Body Extra Small", style = MaterialTheme.typography.bodyExtraSmall)
- Text("Body Small", style = MaterialTheme.typography.bodySmall)
- Text("Body Medium", style = MaterialTheme.typography.bodyMedium)
- Text("Body Large", style = MaterialTheme.typography.bodyLarge)
+ Text(
+ "Body\nExtra\nSmall",
+ style = MaterialTheme.typography.bodyExtraSmall,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Body\nSmall",
+ style = MaterialTheme.typography.bodySmall,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Body\nMedium",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ "Body\nLarge",
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center
+ )
}
}
},
@@ -129,19 +169,25 @@
"Numeral",
listOf(
ComposableDemo("Extra Small") {
- Centralize { Text("0123", style = MaterialTheme.typography.numeralExtraSmall) }
+ Centralize {
+ Text("0123\n6789", style = MaterialTheme.typography.numeralExtraSmall)
+ }
},
ComposableDemo("Small") {
- Centralize { Text("0123", style = MaterialTheme.typography.numeralSmall) }
+ Centralize { Text("0123\n6789", style = MaterialTheme.typography.numeralSmall) }
},
ComposableDemo("Medium") {
- Centralize { Text("0123", style = MaterialTheme.typography.numeralMedium) }
+ Centralize {
+ Text("0123\n6789", style = MaterialTheme.typography.numeralMedium)
+ }
},
ComposableDemo("Large") {
- Centralize { Text("0123", style = MaterialTheme.typography.numeralLarge) }
+ Centralize { Text("0123\n6789", style = MaterialTheme.typography.numeralLarge) }
},
ComposableDemo("Extra Large") {
- Centralize { Text("0123", style = MaterialTheme.typography.numeralExtraLarge) }
+ Centralize {
+ Text("0123\n6789", style = MaterialTheme.typography.numeralExtraLarge)
+ }
}
)
),
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index fb753ac..079c6ce 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -30,8 +30,9 @@
import androidx.wear.compose.material3.samples.EdgeButtonSample
import androidx.wear.compose.material3.samples.EdgeSwipeForSwipeToDismiss
import androidx.wear.compose.material3.samples.FixedFontSize
-import androidx.wear.compose.material3.samples.HorizontalPageIndicatorSample
import androidx.wear.compose.material3.samples.HorizontalPageIndicatorWithPagerSample
+import androidx.wear.compose.material3.samples.LazyColumnScalingMorphingEffectSample
+import androidx.wear.compose.material3.samples.LazyColumnTargetMorphingHeightSample
import androidx.wear.compose.material3.samples.ScaffoldSample
import androidx.wear.compose.material3.samples.SimpleSwipeToDismissBox
import androidx.wear.compose.material3.samples.StatefulSwipeToDismissBox
@@ -41,11 +42,13 @@
import androidx.wear.compose.material3.samples.SwipeToRevealNonAnchoredSample
import androidx.wear.compose.material3.samples.SwipeToRevealSample
import androidx.wear.compose.material3.samples.SwipeToRevealSingleActionCardSample
+import androidx.wear.compose.material3.samples.VerticalPageIndicatorWithPagerSample
val WearMaterial3Demos =
Material3DemoCategory(
"Material 3",
listOf(
+ ComposableDemo("Haptics") { Centralize { HapticsDemos() } },
Material3DemoCategory(title = "Typography", TypographyDemos),
Material3DemoCategory(
"Button",
@@ -69,7 +72,6 @@
Material3DemoCategory("Open on phone Dialog", OpenOnPhoneDialogDemos),
ComposableDemo("Scaffold") { ScaffoldSample() },
Material3DemoCategory("ScrollAway", ScrollAwayDemos),
- ComposableDemo("Haptics") { Centralize { HapticsDemos() } },
ComposableDemo("Compact Button") { CompactButtonDemo() },
ComposableDemo("Icon Button") { IconButtonDemo() },
ComposableDemo("Image Button") { ImageButtonDemo() },
@@ -146,19 +148,25 @@
)
),
Material3DemoCategory(
- title = "Horizontal Page Indicator",
+ title = "Page Indicator",
listOf(
- ComposableDemo("Simple HorizontalPageIndicator") {
- HorizontalPageIndicatorSample()
- },
- ComposableDemo("HorizontalPageIndicator with Pager") {
+ ComposableDemo("HorizontalPageIndicator") {
HorizontalPageIndicatorWithPagerSample(it.swipeToDismissBoxState)
},
+ ComposableDemo("VerticalPageIndicator") {
+ VerticalPageIndicatorWithPagerSample()
+ },
)
),
Material3DemoCategory(
title = "Swipe to Reveal",
listOf(
+ ComposableDemo("Bi-directional / Non-anchoring") {
+ Centralize { SwipeToRevealBothDirectionsNonAnchoring() }
+ },
+ ComposableDemo("Bi-directional Two Actions") {
+ Centralize { SwipeToRevealBothDirections() }
+ },
ComposableDemo("Two Actions") { Centralize { SwipeToRevealSample() } },
ComposableDemo("Two Undo Actions") {
Centralize { SwipeToRevealTwoActionsWithUndo() }
@@ -192,7 +200,15 @@
ComposableDemo("Settings Demo") { SettingsDemo() },
Material3DemoCategory(
title = "LazyColumn",
- listOf(ComposableDemo("Notifications") { LazyColumnNotificationsDemo() })
+ listOf(
+ ComposableDemo("Notifications") { LazyColumnNotificationsDemo() },
+ ComposableDemo("Scaling Morphing Effect Sample") {
+ LazyColumnScalingMorphingEffectSample()
+ },
+ ComposableDemo("Target Morphing Height Sample") {
+ LazyColumnTargetMorphingHeightSample()
+ }
+ )
)
)
)
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
index 122d612..2aa006b 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
@@ -47,7 +47,7 @@
Text("Confirm", Modifier.align(Alignment.Center))
EdgeButton(
onClick = { /* Do something */ },
- buttonHeight = ButtonDefaults.EdgeButtonHeightMedium,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightMedium,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(
@@ -68,7 +68,7 @@
bottomButton = {
EdgeButton(
onClick = {},
- buttonHeight = ButtonDefaults.EdgeButtonHeightLarge,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
colors = buttonColors(containerColor = Color.DarkGray)
) {
Text("Ok", textAlign = TextAlign.Center)
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/HorizontalPageIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/HorizontalPageIndicatorSample.kt
deleted file mode 100644
index c1752a1..0000000
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/HorizontalPageIndicatorSample.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3.samples
-
-import androidx.annotation.Sampled
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.wear.compose.foundation.SwipeToDismissBoxState
-import androidx.wear.compose.foundation.edgeSwipeToDismiss
-import androidx.wear.compose.material3.HorizontalPageIndicator
-import androidx.wear.compose.material3.Slider
-import androidx.wear.compose.material3.Text
-
-@Sampled
-@Composable
-fun HorizontalPageIndicatorSample() {
- val pageCount = 9
- var selectedPage by remember { mutableStateOf(0) }
-
- val animatedSelectedPage by
- animateFloatAsState(
- targetValue = selectedPage.toFloat(),
- label = "animateSelectedPage",
- )
-
- Box(modifier = Modifier.fillMaxSize()) {
- Slider(
- modifier = Modifier.align(Alignment.Center),
- value = selectedPage,
- valueProgression = 0 until pageCount,
- onValueChange = { selectedPage = it }
- )
- HorizontalPageIndicator(
- pageCount = pageCount,
- currentPage = selectedPage,
- currentPageOffsetFraction = { animatedSelectedPage - selectedPage },
- )
- }
-}
-
-@Sampled
-@Composable
-fun HorizontalPageIndicatorWithPagerSample(swipeState: SwipeToDismissBoxState) {
- val pageCount = 9
- val pagerState = rememberPagerState { pageCount }
-
- Box {
- HorizontalPager(
- modifier = Modifier.fillMaxSize().edgeSwipeToDismiss(swipeState),
- state = pagerState,
- ) { page ->
- Box(modifier = Modifier.fillMaxSize()) {
- Text(modifier = Modifier.align(Alignment.Center), text = "Page #$page")
- }
- }
- HorizontalPageIndicator(
- pageCount = pageCount,
- currentPage = pagerState.currentPage,
- currentPageOffsetFraction = { pagerState.currentPageOffsetFraction },
- )
- }
-}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PageIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PageIndicatorSample.kt
new file mode 100644
index 0000000..018b762
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/PageIndicatorSample.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.VerticalPager
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.HorizontalPageIndicator
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.VerticalPageIndicator
+
+@Sampled
+@Composable
+fun HorizontalPageIndicatorWithPagerSample(
+ swipeState: SwipeToDismissBoxState,
+) {
+ val pageCount = 9
+ val pagerState = rememberPagerState { pageCount }
+
+ Box {
+ HorizontalPager(
+ modifier = Modifier.fillMaxSize().edgeSwipeToDismiss(swipeState),
+ state = pagerState,
+ ) { page ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(modifier = Modifier.align(Alignment.Center), text = "Page #$page")
+ }
+ }
+ HorizontalPageIndicator(pagerState = pagerState)
+ }
+}
+
+@Sampled
+@Composable
+fun VerticalPageIndicatorWithPagerSample() {
+ val pageCount = 9
+ val pagerState = rememberPagerState { pageCount }
+
+ Box {
+ VerticalPager(
+ modifier = Modifier.fillMaxSize(),
+ state = pagerState,
+ ) { page ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(modifier = Modifier.align(Alignment.Center), text = "Page #$page")
+ }
+ }
+ VerticalPageIndicator(pagerState = pagerState)
+ }
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt
index 7e5ccbb..0bed18c 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/SwipeToRevealSample.kt
@@ -41,7 +41,9 @@
SwipeToReveal(
// Use the double action anchor width when revealing two actions
revealState =
- rememberRevealState(anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth),
+ rememberRevealState(
+ anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth,
+ ),
actions = {
primaryAction(
onClick = { /* This block is called when the primary action is executed. */ },
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt
index 78cc200..d136db7 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimePickerSample.kt
@@ -17,11 +17,8 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
@@ -32,7 +29,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.Text
@@ -59,16 +55,14 @@
initialTime = timePickerTime // Initialize with last picked time on reopen
)
} else {
- Column(
+ Box(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
+ contentAlignment = Alignment.Center,
) {
- Text("Selected Time")
- Spacer(Modifier.height(12.dp))
Button(
onClick = { showTimePicker = true },
- label = { Text(timePickerTime.format(formatter)) },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
)
}
@@ -91,16 +85,14 @@
initialTime = timePickerTime // Initialize with last picked time on reopen
)
} else {
- Column(
+ Box(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
+ contentAlignment = Alignment.Center,
) {
- Text("Selected Time")
- Spacer(Modifier.height(12.dp))
Button(
onClick = { showTimePicker = true },
- label = { Text(timePickerTime.format(formatter)) },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
)
}
@@ -123,16 +115,14 @@
initialTime = timePickerTime // Initialize with last picked time on reopen
)
} else {
- Column(
+ Box(
modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
+ contentAlignment = Alignment.Center,
) {
- Text("Selected Time")
- Spacer(Modifier.height(12.dp))
Button(
onClick = { showTimePicker = true },
- label = { Text(timePickerTime.format(formatter)) },
+ label = { Text("Selected Time") },
+ secondaryLabel = { Text(timePickerTime.format(formatter)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 076610a..a987c30 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -19,6 +19,8 @@
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -37,6 +39,7 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -325,7 +328,7 @@
expectedContentColor = MaterialTheme.colorScheme.onBackground
expectedTextStyle = MaterialTheme.typography.titleMedium
expectedTextAlign = TextAlign.Center
- expectedTextMaxLines = AlertDialogDefaults.titleMaxLines
+ expectedTextMaxLines = AlertTitleMaxLines
AlertDialog(
modifier = Modifier.testTag(TEST_TAG),
title = {
@@ -439,46 +442,87 @@
@Test
fun with_title_confirmDismissButtons_positioning() {
rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
- AlertDialog(
- show = true,
- title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
- onDismissRequest = {},
- confirmButton = {
- Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
- },
- dismissButton = {
- Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
- },
- verticalArrangement =
- Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
- modifier = Modifier.testTag(TEST_TAG),
- )
+ ScreenConfiguration(AlertScreenSize) {
+ AlertDialog(
+ show = true,
+ title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
+ },
+ dismissButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
+ },
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(AlertScreenSize.dp).testTag(TEST_TAG),
+ )
+ }
}
val titleBottom = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot().bottom
val confirmButtonTop =
rule.onNodeWithTag(ConfirmButtonTestTag).getUnclippedBoundsInRoot().top
- confirmButtonTop.assertIsEqualTo(titleBottom + AlertDialogDefaults.bottomSpacing)
+ confirmButtonTop.assertIsEqualTo(titleBottom + AlertBottomSpacing)
+ }
+
+ @Test
+ fun with_title_noBottomButton_positioning() {
+ rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+ ScreenConfiguration(SmallScreenSize) {
+ AlertDialog(
+ show = true,
+ title = { Text("Title") },
+ onDismissRequest = {},
+ bottomButton = null,
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(SmallScreenSize.dp).testTag(TEST_TAG),
+ ) {
+ item {
+ Text(
+ "ContentText",
+ // We set height larger than the screen size to be sure that the list
+ // will be scrollable
+ modifier =
+ Modifier.size(width = 100.dp, height = (SmallScreenSize + 50).dp)
+ .testTag(ContentTestTag)
+ )
+ }
+ }
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeUp() }
+
+ val contentBottom = rule.onNodeWithTag(ContentTestTag).getUnclippedBoundsInRoot().bottom
+ val alertDialogBottom = rule.onNodeWithTag(TEST_TAG).getUnclippedBoundsInRoot().bottom
+ // Assert that there is a proper padding between the bottom of the content and the bottom of
+ // the dialog.
+ contentBottom.assertIsEqualTo(
+ alertDialogBottom * (1 - AlertDialogDefaults.noEdgeButtonBottomPaddingFraction)
+ )
}
@Test
fun with_icon_title_confirmDismissButtons_positioning() {
rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
- AlertDialog(
- show = true,
- icon = { TestImage(IconTestTag) },
- title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
- onDismissRequest = {},
- confirmButton = {
- Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
- },
- dismissButton = {
- Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
- },
- verticalArrangement =
- Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
- modifier = Modifier.testTag(TEST_TAG),
- )
+ ScreenConfiguration(AlertScreenSize) {
+ AlertDialog(
+ show = true,
+ icon = { TestImage(IconTestTag) },
+ title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
+ },
+ dismissButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
+ },
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(AlertScreenSize.dp).testTag(TEST_TAG),
+ )
+ }
}
val iconBottom = rule.onNodeWithTag(IconTestTag).getUnclippedBoundsInRoot().bottom
@@ -487,29 +531,31 @@
val confirmButtonTop =
rule.onNodeWithTag(ConfirmButtonTestTag).getUnclippedBoundsInRoot().top
- titleTop.assertIsEqualTo(iconBottom + AlertDialogDefaults.iconBottomSpacing)
- confirmButtonTop.assertIsEqualTo(titleBottom + AlertDialogDefaults.bottomSpacing)
+ titleTop.assertIsEqualTo(iconBottom + AlertIconBottomSpacing)
+ confirmButtonTop.assertIsEqualTo(titleBottom + AlertBottomSpacing)
}
@Test
fun with_icon_title_textMessage_confirmDismissButtons_positioning() {
rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
- AlertDialog(
- show = true,
- icon = { TestImage(IconTestTag) },
- title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
- text = { Text("Text", modifier = Modifier.testTag(TextTestTag)) },
- onDismissRequest = {},
- confirmButton = {
- Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
- },
- dismissButton = {
- Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
- },
- verticalArrangement =
- Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
- modifier = Modifier.testTag(TEST_TAG),
- )
+ ScreenConfiguration(AlertScreenSize) {
+ AlertDialog(
+ show = true,
+ icon = { TestImage(IconTestTag) },
+ title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
+ text = { Text("Text", modifier = Modifier.testTag(TextTestTag)) },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
+ },
+ dismissButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
+ },
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(AlertScreenSize.dp).testTag(TEST_TAG),
+ )
+ }
}
val iconBottom = rule.onNodeWithTag(IconTestTag).getUnclippedBoundsInRoot().bottom
@@ -520,31 +566,33 @@
val confirmButtonTop =
rule.onNodeWithTag(ConfirmButtonTestTag).getUnclippedBoundsInRoot().top
- titleTop.assertIsEqualTo(iconBottom + AlertDialogDefaults.iconBottomSpacing)
- textTop.assertIsEqualTo(titleBottom + AlertDialogDefaults.textMessageTopSpacing)
- confirmButtonTop.assertIsEqualTo(textBottom + AlertDialogDefaults.bottomSpacing)
+ titleTop.assertIsEqualTo(iconBottom + AlertIconBottomSpacing)
+ textTop.assertIsEqualTo(titleBottom + AlertTextMessageTopSpacing)
+ confirmButtonTop.assertIsEqualTo(textBottom + AlertBottomSpacing)
}
@Test
fun with_icon_title_textMessage_content_confirmDismissButtons_positioning() {
rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
- AlertDialog(
- show = true,
- icon = { TestImage(IconTestTag) },
- title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
- text = { Text("Text", modifier = Modifier.testTag(TextTestTag)) },
- onDismissRequest = {},
- confirmButton = {
- Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
- },
- dismissButton = {
- Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
- },
- verticalArrangement =
- Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
- modifier = Modifier.testTag(TEST_TAG),
- ) {
- item { Text("ContentText", modifier = Modifier.testTag(ContentTestTag)) }
+ ScreenConfiguration(AlertScreenSize) {
+ AlertDialog(
+ show = true,
+ icon = { TestImage(IconTestTag) },
+ title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
+ text = { Text("Text", modifier = Modifier.testTag(TextTestTag)) },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
+ },
+ dismissButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
+ },
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(AlertScreenSize.dp).testTag(TEST_TAG),
+ ) {
+ item { Text("ContentText", modifier = Modifier.testTag(ContentTestTag)) }
+ }
}
}
@@ -558,31 +606,33 @@
val confirmButtonTop =
rule.onNodeWithTag(ConfirmButtonTestTag).getUnclippedBoundsInRoot().top
- titleTop.assertIsEqualTo(iconBottom + AlertDialogDefaults.iconBottomSpacing)
- textTop.assertIsEqualTo(titleBottom + AlertDialogDefaults.textMessageTopSpacing)
- contentTop.assertIsEqualTo(textBottom + AlertDialogDefaults.textMessageTopSpacing)
- confirmButtonTop.assertIsEqualTo(contentBottom + AlertDialogDefaults.bottomSpacing)
+ titleTop.assertIsEqualTo(iconBottom + AlertIconBottomSpacing)
+ textTop.assertIsEqualTo(titleBottom + AlertTextMessageTopSpacing)
+ contentTop.assertIsEqualTo(textBottom + AlertTextMessageTopSpacing)
+ confirmButtonTop.assertIsEqualTo(contentBottom + AlertBottomSpacing)
}
@Test
fun with_icon_title_content_confirmDismissButtons_positioning() {
rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
- AlertDialog(
- show = true,
- icon = { TestImage(IconTestTag) },
- title = { Text("Title", modifier = Modifier.testTag(TitleTestTag)) },
- onDismissRequest = {},
- confirmButton = {
- Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
- },
- dismissButton = {
- Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
- },
- verticalArrangement =
- Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
- modifier = Modifier.testTag(TEST_TAG),
- ) {
- item { Text("ContentText", modifier = Modifier.testTag(ContentTestTag)) }
+ ScreenConfiguration(AlertScreenSize) {
+ AlertDialog(
+ show = true,
+ icon = { TestImage(IconTestTag) },
+ title = { Box(modifier = Modifier.size(3.dp).testTag(TitleTestTag)) },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(ConfirmButtonTestTag)) {}
+ },
+ dismissButton = {
+ Button(onClick = {}, modifier = Modifier.testTag(DismissButtonTestTag)) {}
+ },
+ verticalArrangement =
+ Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
+ modifier = Modifier.size(AlertScreenSize.dp).testTag(TEST_TAG),
+ ) {
+ item { Text("ContentText", modifier = Modifier.testTag(ContentTestTag)) }
+ }
}
}
@@ -594,9 +644,9 @@
val confirmButtonTop =
rule.onNodeWithTag(ConfirmButtonTestTag).getUnclippedBoundsInRoot().top
- titleTop.assertIsEqualTo(iconBottom + AlertDialogDefaults.iconBottomSpacing)
- contentTop.assertIsEqualTo(titleBottom + AlertDialogDefaults.textMessageTopSpacing)
- confirmButtonTop.assertIsEqualTo(contentBottom + AlertDialogDefaults.bottomSpacing)
+ titleTop.assertIsEqualTo(iconBottom + AlertIconBottomSpacing)
+ contentTop.assertIsEqualTo(titleBottom + AlertTextMessageTopSpacing)
+ confirmButtonTop.assertIsEqualTo(contentBottom + AlertBottomSpacing)
}
// TODO: add more positioning tests for EdgeButton.
@@ -608,3 +658,5 @@
private const val ContentTestTag = "content"
private const val ConfirmButtonTestTag = "confirmButton"
private const val DismissButtonTestTag = "dismissButton"
+private const val AlertScreenSize = 400
+private const val SmallScreenSize = 100
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
index 7d9b092..7eada56 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
@@ -135,7 +135,7 @@
EdgeButton(
onClick = { /* Do something */ },
enabled = enabled,
- buttonHeight = buttonHeight,
+ preferredHeight = buttonHeight,
modifier =
Modifier.align(Alignment.BottomEnd)
.testTag(TEST_TAG)
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorScreenshotTest.kt
deleted file mode 100644
index 92820fd..0000000
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorScreenshotTest.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3
-
-import android.os.Build
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.DeviceConfigurationOverride
-import androidx.compose.ui.test.LayoutDirection
-import androidx.compose.ui.test.RoundScreen
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import androidx.wear.compose.material3.HorizontalPageIndicatorTest.Companion.PAGE_COUNT
-import androidx.wear.compose.material3.HorizontalPageIndicatorTest.Companion.SELECTED_PAGE_INDEX
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TestName
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-class HorizontalPageIndicatorScreenshotTest {
-
- @get:Rule val rule = createComposeRule()
-
- @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
-
- @get:Rule val testName = TestName()
-
- @Test
- fun horizontalPageIndicator_circular_selected_page() {
- selected_page(true, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontalPageIndicator_linear_selected_page() {
- selected_page(false, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontalPageIndicator_circular_selected_page_rtl() {
- selected_page(true, LayoutDirection.Rtl)
- }
-
- @Test
- fun horizontalPageIndicator_linear_selected_page_rtl() {
- selected_page(false, LayoutDirection.Rtl)
- }
-
- @Test
- fun horizontalPageIndicator_circular_between_pages() {
- between_pages(true)
- }
-
- @Test
- fun horizontalPageIndicator_linear_between_pages() {
- between_pages(false)
- }
-
- private fun selected_page(isRound: Boolean, layoutDirection: LayoutDirection) {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(
- DeviceConfigurationOverride.LayoutDirection(layoutDirection)
- ) {
- defaultHorizontalPageIndicator(isRound)
- }
- }
- rule.waitForIdle()
-
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertAgainstGolden(screenshotRule, testName.methodName)
- }
-
- private fun between_pages(isRound: Boolean) {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(isRound)) {
- Box(modifier = Modifier.testTag(TEST_TAG).size(200.dp)) {
- HorizontalPageIndicator(
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.5f },
- selectedColor = Color.Yellow,
- unselectedColor = Color.Red,
- indicatorSize = 15.dp
- )
- }
- }
- }
- rule.waitForIdle()
-
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertAgainstGolden(screenshotRule, testName.methodName)
- }
-
- @Composable
- private fun defaultHorizontalPageIndicator(isRound: Boolean) {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(isRound)) {
- Box(modifier = Modifier.testTag(TEST_TAG).size(200.dp)) {
- HorizontalPageIndicator(
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.0f },
- selectedColor = Color.Yellow,
- unselectedColor = Color.Red,
- indicatorSize = 15.dp
- )
- }
- }
- }
-}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorTest.kt
deleted file mode 100644
index 92182e4..0000000
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/HorizontalPageIndicatorTest.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.DeviceConfigurationOverride
-import androidx.compose.ui.test.RoundScreen
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import org.junit.Rule
-import org.junit.Test
-
-@RequiresApi(Build.VERSION_CODES.O)
-class HorizontalPageIndicatorTest {
- @get:Rule val rule = createComposeRule()
-
- @Test
- public fun supports_testtag_circular() {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(
- DeviceConfigurationOverride.RoundScreen(isScreenRound = true)
- ) {
- HorizontalPageIndicator(
- modifier = Modifier.testTag(TEST_TAG),
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.0f },
- )
- }
- }
- rule.onNodeWithTag(TEST_TAG).assertExists()
- }
-
- @Test
- public fun supports_testtag_linear() {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(
- DeviceConfigurationOverride.RoundScreen(isScreenRound = false)
- ) {
- HorizontalPageIndicator(
- modifier = Modifier.testTag(TEST_TAG),
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.0f },
- )
- }
- }
- rule.onNodeWithTag(TEST_TAG).assertExists()
- }
-
- @Test
- public fun position_is_selected_circular() {
- position_is_selected(isRound = true)
- }
-
- @Test
- public fun position_is_selected_linear() {
- position_is_selected(isRound = false)
- }
-
- @Test
- public fun in_between_positions_circular() {
- in_between_positions(isRound = true)
- }
-
- @Test
- public fun in_between_positions_linear() {
- in_between_positions(isRound = false)
- }
-
- @Test
- fun horizontal_page_indicator_circular_9_pages_sized_appropriately() {
- val indicatorSize = 6.dp
- val spacing = 2.dp
-
- rule.setContent {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- Box(modifier = Modifier.size(200.dp)) {
- HorizontalPageIndicator(
- modifier = Modifier.testTag(TEST_TAG),
- pageCount = 9,
- currentPage = 1,
- currentPageOffsetFraction = { 0f },
- indicatorSize = indicatorSize,
- spacing = spacing
- )
- }
- }
- }
-
- rule
- .onNodeWithTag(TEST_TAG)
- .assertWidthIsEqualTo(
- (indicatorSize + spacing) * 6 + PageIndicatorDefaults.edgePadding * 2
- )
- rule
- .onNodeWithTag(TEST_TAG)
- .assertHeightIsEqualTo(indicatorSize * 2 + PageIndicatorDefaults.edgePadding * 2)
- }
-
- @Test
- fun horizontal_page_indicator_circular_3_pages_sized_appropriately() {
- val indicatorSize = 6.dp
- val spacing = 2.dp
- val pagesCount = 3
-
- rule.setContent {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
- Box(modifier = Modifier.size(200.dp)) {
- HorizontalPageIndicator(
- modifier = Modifier.testTag(TEST_TAG),
- pageCount = pagesCount,
- currentPage = 1,
- currentPageOffsetFraction = { 0f },
- indicatorSize = indicatorSize,
- spacing = spacing
- )
- }
- }
- }
-
- rule
- .onNodeWithTag(TEST_TAG)
- .assertWidthIsEqualTo(
- (indicatorSize + spacing) * pagesCount + PageIndicatorDefaults.edgePadding * 2
- )
- rule
- .onNodeWithTag(TEST_TAG)
- .assertHeightIsEqualTo(indicatorSize * 2 + PageIndicatorDefaults.edgePadding * 2)
- }
-
- private fun position_is_selected(isRound: Boolean) {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(isRound)) {
- Box(modifier = Modifier.testTag(TEST_TAG).size(150.dp)) {
- HorizontalPageIndicator(
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.0f },
- selectedColor = selectedColor,
- unselectedColor = unselectedColor,
- indicatorSize = 20.dp
- )
- }
- }
- }
- rule.waitForIdle()
-
- // A selected dot with specified color should be visible on the screen, which is apprx 1.3%
- // (1.3% per dot, 1 dot in total)
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertColorInPercentageRange(selectedColor, 1.2f..1.6f)
- // Unselected dots should also be visible on the screen, and should take around 4%
- // (1.3% per dot, 3 dots total)
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertColorInPercentageRange(unselectedColor, 3.8f..4.5f)
- }
-
- private fun in_between_positions(isRound: Boolean) {
- rule.setContentWithTheme {
- DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(isRound)) {
- Box(modifier = Modifier.testTag(TEST_TAG).size(150.dp)) {
- HorizontalPageIndicator(
- pageCount = PAGE_COUNT,
- currentPage = SELECTED_PAGE_INDEX,
- currentPageOffsetFraction = { 0.5f },
- selectedColor = selectedColor,
- unselectedColor = unselectedColor,
- indicatorSize = 20.dp
- )
- }
- }
- }
- rule.waitForIdle()
-
- // Selected color should occupy 2 dots with space in between, which
- // approximately equals to 3.5%
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertColorInPercentageRange(selectedColor, 3f..4f)
- // Unselected dots ( which doesn't participate in color merge)
- // should also be visible on the screen, and should take around 2.7%
- // (1.3% per dot, 2 dots in total)
- rule
- .onNodeWithTag(TEST_TAG)
- .captureToImage()
- .assertColorInPercentageRange(unselectedColor, 2.5f..3f)
- }
-
- companion object {
- val selectedColor = Color.Yellow
- val unselectedColor = Color.Red
-
- const val PAGE_COUNT = 4
- const val SELECTED_PAGE_INDEX = 1
- }
-}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index e3af5a4..79a7733 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -123,9 +123,10 @@
/**
* Valid characters for golden identifiers are [A-Za-z0-9_-] TestParameterInjector adds '[' +
- * parameter_values + ']' to the test name.
+ * parameter_values + ']' + ',' to the test name.
*/
-fun TestName.goldenIdentifier(): String = methodName.replace("[", "_").replace("]", "")
+fun TestName.goldenIdentifier(): String =
+ methodName.replace("[", "_").replace("]", "").replace(",", "_")
internal const val TEST_TAG = "test-item"
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorScreenshotTest.kt
new file mode 100644
index 0000000..c00c2e5
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorScreenshotTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+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.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.DeviceConfigurationOverride
+import androidx.compose.ui.test.LayoutDirection
+import androidx.compose.ui.test.RoundScreen
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.foundation.pager.PagerState
+import androidx.wear.compose.material3.PageIndicatorTest.Companion.PAGE_COUNT
+import androidx.wear.compose.material3.PageIndicatorTest.Companion.SELECTED_PAGE_INDEX
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(TestParameterInjector::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class PageIndicatorScreenshotTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun horizontalPageIndicator_selected_page(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(shape.isRound, true, LayoutDirection.Ltr, screenSize)
+ }
+
+ @Test
+ fun verticalPageIndicator_selected_page(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(shape.isRound, false, LayoutDirection.Ltr, screenSize)
+ }
+
+ @Test
+ fun horizontalPageIndicator_selected_page_rtl(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(shape.isRound, true, LayoutDirection.Rtl, screenSize)
+ }
+
+ @Test
+ fun verticalPageIndicator_selected_page_rtl(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(shape.isRound, false, LayoutDirection.Rtl, screenSize)
+ }
+
+ @Test
+ fun horizontalPageIndicator_between_pages(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(
+ shape.isRound,
+ true,
+ LayoutDirection.Ltr,
+ screenSize,
+ offsetFraction = 0.5f,
+ )
+ }
+
+ @Test
+ fun verticalPageIndicator_between_pages(
+ @TestParameter screenSize: ScreenSize,
+ @TestParameter shape: ScreenShape
+ ) {
+ verifyPageIndicator(
+ shape.isRound,
+ false,
+ LayoutDirection.Ltr,
+ screenSize,
+ offsetFraction = 0.5f
+ )
+ }
+
+ @Test
+ fun horizontalPageIndicator_circular_9_pages(@TestParameter screenSize: ScreenSize) {
+ verifyPageIndicator(
+ true,
+ true,
+ LayoutDirection.Ltr,
+ screenSize,
+ pageCount = 9,
+ selectedPageIndex = 6
+ )
+ }
+
+ @Test
+ fun verticalPageIndicator_circular_9_pages(@TestParameter screenSize: ScreenSize) {
+ verifyPageIndicator(
+ true,
+ false,
+ LayoutDirection.Ltr,
+ screenSize,
+ pageCount = 9,
+ selectedPageIndex = 6
+ )
+ }
+
+ // TODO(b/369535289) Add tests for linear page indicator with 9 pages
+
+ private fun verifyPageIndicator(
+ isRound: Boolean,
+ isHorizontal: Boolean,
+ layoutDirection: LayoutDirection,
+ screenSize: ScreenSize = ScreenSize.SMALL,
+ offsetFraction: Float = 0.0f,
+ pageCount: Int = PAGE_COUNT,
+ selectedPageIndex: Int = SELECTED_PAGE_INDEX
+ ) {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.LayoutDirection(layoutDirection)
+ ) {
+ PageIndicator(
+ isRound,
+ isHorizontal,
+ offsetFraction,
+ screenSize,
+ pageCount,
+ selectedPageIndex
+ )
+ }
+ }
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
+ }
+
+ @Composable
+ private fun PageIndicator(
+ isRound: Boolean,
+ isHorizontal: Boolean,
+ offsetFraction: Float,
+ screenSize: ScreenSize,
+ pageCount: Int,
+ selectedPageIndex: Int
+ ) {
+ DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(isRound)) {
+ ScreenConfiguration(screenSize.size) {
+ Box(modifier = Modifier.testTag(TEST_TAG).fillMaxSize().background(Color.White)) {
+ val pagerState =
+ PagerState(
+ currentPage = selectedPageIndex,
+ currentPageOffsetFraction = offsetFraction,
+ pageCount = { pageCount }
+ )
+ if (isHorizontal) {
+ HorizontalPageIndicator(pagerState = pagerState)
+ } else {
+ VerticalPageIndicator(pagerState = pagerState)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorTest.kt
new file mode 100644
index 0000000..34be1f5
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/PageIndicatorTest.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.DeviceConfigurationOverride
+import androidx.compose.ui.test.LayoutDirection
+import androidx.compose.ui.test.RoundScreen
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.then
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.pager.PagerState
+import org.junit.Rule
+import org.junit.Test
+
+@RequiresApi(Build.VERSION_CODES.O)
+class PageIndicatorTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ public fun horizontalPageIndicator_supports_testtag_circular() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(isScreenRound = true)
+ ) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start,
+ )
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ public fun horizontalPageIndicator_supports_testtag_linear() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(isScreenRound = false)
+ ) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start
+ )
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ public fun verticalPageIndicator_supports_testtag_circular() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(isScreenRound = true)
+ ) {
+ VerticalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start,
+ )
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ public fun verticalPageIndicator_supports_testtag_linear() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(isScreenRound = false)
+ ) {
+ VerticalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start
+ )
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ public fun horizontalPageIndicator_position_is_selected_circular() {
+ horizontalPageIndicator_position_is_selected_circular(LayoutDirection.Ltr)
+ }
+
+ @Test
+ public fun horizontalPageIndicator_in_between_positions_circular() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
+ Box(modifier = Modifier.size(150.dp)) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_middle,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // Selected color should occupy 2 dots with space in between, which
+ // approximately equals to 12%
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(selectedColor, 11f..14f)
+ // Unselected dots should also be visible on the screen, and should take around 9%
+ // (4.4% per dot, 2 dots total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(unselectedColor, 7f..10f)
+
+ // Check that background color exists
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(backgroundColor)
+ }
+
+ @Test
+ public fun verticalPageIndicator_position_is_selected_circular() {
+ verticalPageIndicator_position_is_selected_circular(LayoutDirection.Ltr)
+ }
+
+ @Test
+ public fun verticalPageIndicator_in_between_positions_circular() {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
+ Box(modifier = Modifier.size(150.dp)) {
+ VerticalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_middle,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // Selected color should occupy 2 dots with space in between, which
+ // approximately equals to 12%
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(selectedColor, 10f..14f)
+ // Unselected dots ( which doesn't participate in color merge)
+ // should also be visible on the screen, and should take around 8.8%
+ // (4.4% per dot, 2 dots in total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(unselectedColor, 7.5f..9f)
+
+ // Check that background color exists
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(backgroundColor)
+ }
+
+ @Test
+ fun horizontalPageIndicator_9_pages_sized_appropriately_circular() {
+ val indicatorSize = PageIndicatorItemSize
+ val spacing = PageIndicatorSpacing
+ val padding = PaddingDefaults.edgePadding
+ rule.setContent {
+ DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
+ Box(modifier = Modifier.size(150.dp)) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState =
+ PagerState(
+ currentPage = 1,
+ currentPageOffsetFraction = 0.0f,
+ pageCount = { 9 }
+ ),
+ )
+ }
+ }
+ }
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .assertWidthIsEqualTo((indicatorSize + spacing) * 6 + padding * 2)
+ rule.onNodeWithTag(TEST_TAG).assertHeightIsEqualTo(indicatorSize * 2 + padding * 2)
+ }
+
+ @Test
+ fun horizontalPageIndicator_3_pages_sized_appropriately_circular() {
+ val indicatorSize = PageIndicatorItemSize
+ val spacing = PageIndicatorSpacing
+ val pagesCount = 3
+ val padding = PaddingDefaults.edgePadding
+
+ rule.setContent {
+ DeviceConfigurationOverride(DeviceConfigurationOverride.RoundScreen(true)) {
+ Box(modifier = Modifier.size(150.dp)) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState =
+ PagerState(
+ currentPage = 1,
+ currentPageOffsetFraction = 0.0f,
+ pageCount = { pagesCount }
+ ),
+ )
+ }
+ }
+ }
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .assertWidthIsEqualTo((indicatorSize + spacing) * pagesCount + padding * 2)
+ rule.onNodeWithTag(TEST_TAG).assertHeightIsEqualTo(indicatorSize * 2 + padding * 2)
+ }
+
+ @Test
+ fun horizontalPageIndicator_position_is_selected_circular_rtl() {
+ horizontalPageIndicator_position_is_selected_circular(LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun verticalPageIndicator_position_is_selected_circular_rtl() {
+ verticalPageIndicator_position_is_selected_circular(LayoutDirection.Rtl)
+ }
+
+ private fun horizontalPageIndicator_position_is_selected_circular(
+ layoutDirection: LayoutDirection
+ ) {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(true)
+ .then(DeviceConfigurationOverride.LayoutDirection(layoutDirection))
+ ) {
+ Box(modifier = Modifier.size(150.dp)) {
+ HorizontalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // A selected dot with specified color should be visible on the screen, which is apprx 4.4%
+ // (4.4% per dot, 1 dot in total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(selectedColor, 4f..5f)
+ // Unselected dots should also be visible on the screen, and should take around 13.2%
+ // (4.4% per dot, 3 dots total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(unselectedColor, 11f..16f)
+
+ // Check that background color exists
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(backgroundColor)
+ }
+
+ private fun verticalPageIndicator_position_is_selected_circular(
+ layoutDirection: LayoutDirection
+ ) {
+ rule.setContentWithTheme {
+ DeviceConfigurationOverride(
+ DeviceConfigurationOverride.RoundScreen(true)
+ .then(DeviceConfigurationOverride.LayoutDirection(layoutDirection))
+ ) {
+ Box(modifier = Modifier.size(150.dp)) {
+ VerticalPageIndicator(
+ modifier = Modifier.testTag(TEST_TAG),
+ pagerState = pagerState_start,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // A selected dot with specified color should be visible on the screen, which is apprx 4.4%
+ // (4.% per dot, 1 dot in total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(selectedColor, 3.5f..5.5f)
+ // Unselected dots should also be visible on the screen, and should take around 13%
+ // (4.4% per dot, 3 dots total)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertColorInPercentageRange(unselectedColor, 11f..14f)
+
+ // Check that background color exists
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(backgroundColor)
+ }
+
+ private val pagerState_start =
+ PagerState(
+ currentPage = SELECTED_PAGE_INDEX,
+ currentPageOffsetFraction = 0.0f,
+ pageCount = { PAGE_COUNT }
+ )
+
+ private val pagerState_middle =
+ PagerState(
+ currentPage = SELECTED_PAGE_INDEX,
+ currentPageOffsetFraction = 0.5f,
+ pageCount = { PAGE_COUNT }
+ )
+
+ companion object {
+ val selectedColor = Color.Yellow
+ val unselectedColor = Color.Red
+ val backgroundColor = Color.Green
+
+ const val PAGE_COUNT = 4
+ const val SELECTED_PAGE_INDEX = 1
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
index 2031ed4..ca94d41 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
@@ -77,7 +77,7 @@
fun contains_progress_color() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 1f },
colors =
ProgressIndicatorDefaults.colors(
@@ -100,7 +100,7 @@
fun contains_progress_incomplete_color() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0f },
colors =
ProgressIndicatorDefaults.colors(
@@ -123,7 +123,7 @@
fun change_start_end_angle() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
startAngle = 0f,
endAngle = 180f,
@@ -152,7 +152,7 @@
fun set_small_progress_value() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.02f },
colors =
ProgressIndicatorDefaults.colors(
@@ -178,7 +178,7 @@
fun set_small_stroke_width() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.smallStrokeWidth,
colors =
@@ -204,7 +204,7 @@
fun set_large_stroke_width() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.largeStrokeWidth,
colors =
@@ -231,7 +231,7 @@
fun progress_disabled_contains_disabled_colors() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
enabled = false,
colors =
@@ -253,6 +253,10 @@
private fun setContentWithTheme(composable: @Composable BoxScope.() -> Unit) {
// Use constant size modifier to limit relative color percentage ranges.
- rule.setContentWithTheme(modifier = Modifier.size(204.dp), composable = composable)
+ rule.setContentWithTheme(modifier = Modifier.size(COMPONENT_SIZE)) {
+ ScreenConfiguration(SCREEN_SIZE_LARGE) { composable() }
+ }
}
}
+
+private val COMPONENT_SIZE = 204.dp
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt
index 5c2711f..d0455e0 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwipeToRevealScreenshotTest.kt
@@ -33,6 +33,7 @@
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.RevealActionType
import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeDirection
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Rule
@@ -57,7 +58,7 @@
Box(modifier = Modifier.fillMaxSize()) {
SwipeToReveal(
modifier = Modifier.testTag(TEST_TAG),
- revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing),
actions = {
primaryAction(
{},
@@ -83,7 +84,7 @@
modifier = Modifier.testTag(TEST_TAG),
revealState =
rememberRevealState(
- initialValue = RevealValue.Revealing,
+ initialValue = RevealValue.RightRevealing,
anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth
),
actions = {
@@ -114,7 +115,7 @@
Box(modifier = Modifier.fillMaxSize()) {
SwipeToReveal(
modifier = Modifier.testTag(TEST_TAG),
- revealState = rememberRevealState(initialValue = RevealValue.Revealed),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealed),
actions = {
primaryAction({}, /* Empty for testing */ {}, /* Empty for testing */ "")
undoPrimaryAction({}, "Undo Primary")
@@ -134,7 +135,7 @@
SwipeToReveal(
modifier = Modifier.testTag(TEST_TAG),
revealState =
- rememberRevealState(initialValue = RevealValue.Revealed).apply {
+ rememberRevealState(initialValue = RevealValue.RightRevealed).apply {
lastActionType = RevealActionType.SecondaryAction
},
actions = {
@@ -176,7 +177,7 @@
Box(modifier = Modifier.fillMaxSize()) {
SwipeToReveal(
modifier = Modifier.testTag(TEST_TAG),
- revealState = rememberRevealState(initialValue = RevealValue.Revealing),
+ revealState = rememberRevealState(initialValue = RevealValue.RightRevealing),
actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
actions = {
primaryAction(
@@ -205,7 +206,7 @@
modifier = Modifier.testTag(TEST_TAG),
revealState =
rememberRevealState(
- initialValue = RevealValue.Revealing,
+ initialValue = RevealValue.RightRevealing,
anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth
),
actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
@@ -230,6 +231,40 @@
}
}
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun swipeToReveal_showsPrimaryAndSecondaryActionsLeft(@TestParameter screenSize: ScreenSize) {
+ verifyScreenshotForSize(screenSize) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SwipeToReveal(
+ modifier = Modifier.testTag(TEST_TAG),
+ revealState =
+ rememberRevealState(
+ initialValue = RevealValue.LeftRevealing,
+ anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth,
+ swipeDirection = SwipeDirection.Both
+ ),
+ actions = {
+ primaryAction(
+ {},
+ { Icon(Icons.Outlined.Close, contentDescription = "Clear") },
+ "Clear"
+ )
+ secondaryAction(
+ {},
+ { Icon(Icons.Outlined.MoreVert, contentDescription = "More") },
+ "More"
+ )
+ }
+ ) {
+ Button({}, Modifier.fillMaxWidth()) {
+ Text("This text should be partially visible.")
+ }
+ }
+ }
+ }
+ }
+
private fun verifyScreenshotForSize(screenSize: ScreenSize, content: @Composable () -> Unit) {
rule.verifyScreenshot(
screenshotRule = screenshotRule,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index 4f7238f..5fd2645 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -39,7 +39,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -47,13 +46,10 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.material3.AlertDialogDefaults.bottomSpacing
-import androidx.wear.compose.material3.AlertDialogDefaults.contentTopSpacing
-import androidx.wear.compose.material3.AlertDialogDefaults.iconBottomSpacing
-import androidx.wear.compose.material3.AlertDialogDefaults.textMessageTopSpacing
-import androidx.wear.compose.material3.AlertDialogDefaults.textPaddingFraction
-import androidx.wear.compose.material3.AlertDialogDefaults.titlePaddingFraction
+import androidx.wear.compose.material3.PaddingDefaults.horizontalContentPadding
+import androidx.wear.compose.material3.PaddingDefaults.verticalContentPadding
import androidx.wear.compose.materialcore.isSmallScreen
+import androidx.wear.compose.materialcore.screenHeightDp
import androidx.wear.compose.materialcore.screenWidthDp
/**
@@ -72,14 +68,14 @@
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping
* right (typically also called by the [dismissButton]).
* @param confirmButton A slot for a [Button] indicating positive sentiment. Clicking the button
- * must remove the dialog from the composition hierarchy. It's recommended to use
- * [AlertDialogDefaults.ConfirmButton] in this slot with onClick callback.
+ * must remove the dialog from the composition hierarchy e.g. by setting [show] to false. It's
+ * recommended to use [AlertDialogDefaults.ConfirmButton] in this slot with onClick callback.
* @param title A slot for displaying the title of the dialog. Title should contain a summary of the
* dialog's purpose or content and should not exceed 3 lines of text.
* @param modifier Modifier to be applied to the dialog content.
* @param dismissButton A slot for a [Button] indicating negative sentiment. Clicking the button
- * must remove the dialog from the composition hierarchy. It's recommended to use
- * [AlertDialogDefaults.DismissButton] in this slot with onClick callback.
+ * must remove the dialog from the composition hierarchy e.g. by setting [show] to false. It's
+ * recommended to use [AlertDialogDefaults.DismissButton] in this slot with onClick callback.
* @param icon Optional slot for an icon to be shown at the top of the dialog.
* @param text Optional slot for displaying the message of the dialog below the title. Should
* contain additional text that presents further details about the dialog's purpose if the title
@@ -104,7 +100,7 @@
icon: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
verticalArrangement: Arrangement.Vertical = AlertDialogDefaults.VerticalArrangement,
- contentPadding: PaddingValues = AlertDialogDefaults.contentPadding(hasBottomButton = false),
+ contentPadding: PaddingValues = AlertDialogDefaults.confirmDismissContentPadding(),
properties: DialogProperties = DialogProperties(),
content: (ScalingLazyListScope.() -> Unit)? = null
) {
@@ -142,9 +138,9 @@
* @param show A boolean indicating whether the dialog should be displayed.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
* the right or by other dismiss action.
- * @param bottomButton A slot for a [EdgeButton] indicating positive sentiment. Clicking the button
- * must remove the dialog from the composition hierarchy. It's recommended to use
- * [AlertDialogDefaults.BottomButton] in this slot with onClick callback.
+ * @param bottomButton Optional slot for a [EdgeButton] indicating positive sentiment. Clicking the
+ * button must remove the dialog from the composition hierarchy e.g. by setting [show] to false.
+ * It's recommended to use [AlertDialogDefaults.BottomButton] in this slot with onClick callback.
* @param title A slot for displaying the title of the dialog. Title should contain a summary of the
* dialog's purpose or content and should not exceed 3 lines of text.
* @param modifier Modifier to be applied to the dialog content.
@@ -163,13 +159,13 @@
fun AlertDialog(
show: Boolean,
onDismissRequest: () -> Unit,
- bottomButton: @Composable BoxScope.() -> Unit,
+ bottomButton: (@Composable BoxScope.() -> Unit)?,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
verticalArrangement: Arrangement.Vertical = AlertDialogDefaults.VerticalArrangement,
- contentPadding: PaddingValues = AlertDialogDefaults.contentPadding(hasBottomButton = true),
+ contentPadding: PaddingValues = AlertDialogDefaults.contentPadding(bottomButton != null),
properties: DialogProperties = DialogProperties(),
content: (ScalingLazyListScope.() -> Unit)? = null
) {
@@ -183,7 +179,9 @@
title = title,
icon = icon,
text = text,
- alertButtonsParams = AlertButtonsParams.BottomButton(bottomButton),
+ alertButtonsParams =
+ if (bottomButton != null) AlertButtonsParams.BottomButton(bottomButton)
+ else AlertButtonsParams.NoButtons,
content = content
)
}
@@ -203,7 +201,7 @@
EdgeButton(
modifier = Modifier.padding(top = edgeButtonExtraTopPadding),
onClick = onClick,
- buttonHeight = ButtonDefaults.EdgeButtonHeightMedium,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightMedium,
content = content
)
}
@@ -258,23 +256,32 @@
}
/**
- * The padding to apply around the content. Changes based on whether the dialog has a bottom
- * button or not.
- *
- * @param hasBottomButton A boolean indicating whether the dialog has a bottom button.
+ * The padding to apply around the content for the [AlertDialog] variation with confirm dismiss
+ * buttons.
+ */
+ @Composable
+ fun confirmDismissContentPadding(): PaddingValues {
+ val verticalPadding = verticalContentPadding()
+ val horizontalPadding = horizontalContentPadding()
+ return PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding)
+ }
+
+ /**
+ * The padding to apply around the content for the [AlertDialog] variation with a stack of
+ * options and optional bottom button.
*/
@Composable
fun contentPadding(hasBottomButton: Boolean): PaddingValues {
- val screenWidth = LocalConfiguration.current.screenWidthDp
- val verticalContentPadding =
- screenWidth.dp * PaddingDefaults.verticalContentPaddingPercentage / 100
- val horizontalContentPadding =
- screenWidth.dp * PaddingDefaults.horizontalContentPaddingPercentage / 100
+ val topPadding = verticalContentPadding()
+ val horizontalPadding = horizontalContentPadding()
+ val bottomPadding =
+ if (hasBottomButton) edgeButtonHeightWithPadding
+ else screenHeightDp().dp * noEdgeButtonBottomPaddingFraction
return PaddingValues(
- top = verticalContentPadding,
- bottom = if (hasBottomButton) edgeButtonHeightWithPadding else verticalContentPadding,
- start = horizontalContentPadding,
- end = horizontalContentPadding,
+ top = topPadding,
+ bottom = bottomPadding,
+ start = horizontalPadding,
+ end = horizontalPadding,
)
}
@@ -310,19 +317,10 @@
}
/** The extra top padding to apply to the edge button. */
- val edgeButtonExtraTopPadding = 1.dp
-
- internal val edgeButtonHeightWithPadding = ButtonDefaults.EdgeButtonHeightMedium + 7.dp
-
- internal val titlePaddingFraction = 0.12f
- internal val textPaddingFraction = 0.0416f
-
+ private val edgeButtonExtraTopPadding = 1.dp
+ private val edgeButtonHeightWithPadding = ButtonDefaults.EdgeButtonHeightMedium + 7.dp
+ internal val noEdgeButtonBottomPaddingFraction = 0.3646f
internal val cancelButtonPadding = 1.dp
- internal val iconBottomSpacing = 4.dp
- internal val textMessageTopSpacing = 8.dp
- internal val contentTopSpacing = 8.dp
- internal val bottomSpacing = 8.dp
- internal val titleMaxLines = 3
}
@Composable
@@ -370,7 +368,7 @@
item { TextMessage(text) }
}
if (content != null) {
- item { Spacer(Modifier.height(contentTopSpacing)) }
+ item { Spacer(Modifier.height(ContentTopSpacing)) }
content()
}
@@ -379,8 +377,9 @@
item { ConfirmDismissButtons(alertButtonsParams) }
is AlertButtonsParams.BottomButton ->
if (content == null) {
- item { Spacer(Modifier.height(bottomSpacing)) }
+ item { Spacer(Modifier.height(AlertBottomSpacing)) }
}
+ is AlertButtonsParams.NoButtons -> Unit
}
}
}
@@ -391,13 +390,13 @@
private fun IconAlert(content: @Composable () -> Unit) {
Column {
content()
- Spacer(Modifier.height(iconBottomSpacing))
+ Spacer(Modifier.height(AlertIconBottomSpacing))
}
}
@Composable
private fun Title(content: @Composable () -> Unit) {
- val horizontalPadding = screenWidthDp().dp * titlePaddingFraction
+ val horizontalPadding = screenWidthDp().dp * TitlePaddingFraction
Column(modifier = Modifier.padding(horizontal = horizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
@@ -405,7 +404,7 @@
LocalTextConfiguration provides
TextConfiguration(
textAlign = TextAlign.Center,
- maxLines = AlertDialogDefaults.titleMaxLines,
+ maxLines = AlertTitleMaxLines,
overflow = TextOverflow.Ellipsis
),
content = content
@@ -416,7 +415,7 @@
@Composable
private fun ConfirmDismissButtons(alertButtonsParams: AlertButtonsParams.ConfirmDismissButtons) {
Column {
- Spacer(modifier = Modifier.height(bottomSpacing))
+ Spacer(modifier = Modifier.height(AlertBottomSpacing))
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
@@ -432,9 +431,9 @@
@Composable
private fun TextMessage(content: @Composable () -> Unit) {
- val horizontalPadding = screenWidthDp().dp * textPaddingFraction
+ val horizontalPadding = screenWidthDp().dp * TextPaddingFraction
Column(modifier = Modifier.padding(horizontal = horizontalPadding)) {
- Spacer(Modifier.height(textMessageTopSpacing))
+ Spacer(Modifier.height(AlertTextMessageTopSpacing))
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
LocalTextStyle provides MaterialTheme.typography.bodyMedium,
@@ -450,12 +449,23 @@
}
private sealed interface AlertButtonsParams {
- data class BottomButton(
+ object NoButtons : AlertButtonsParams
+
+ class BottomButton(
val bottomButton: @Composable BoxScope.() -> Unit,
) : AlertButtonsParams
- data class ConfirmDismissButtons(
+ class ConfirmDismissButtons(
val confirmButton: @Composable RowScope.() -> Unit,
val dismissButton: @Composable RowScope.() -> Unit
) : AlertButtonsParams
}
+
+internal val AlertIconBottomSpacing = 4.dp
+internal val AlertTextMessageTopSpacing = 8.dp
+internal val AlertBottomSpacing = 8.dp
+internal const val AlertTitleMaxLines = 3
+
+private val ContentTopSpacing = 8.dp
+private const val TextPaddingFraction = 0.0416f
+private const val TitlePaddingFraction = 0.12f
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
index 7764d08..b5e9a6c 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
@@ -34,9 +34,6 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
-import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@@ -66,6 +63,7 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.ButtonDefaults.buttonColors
import androidx.wear.compose.material3.ButtonDefaults.filledTonalButtonColors
+import androidx.wear.compose.material3.internal.Icons
import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerDay
import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerMonth
import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerYear
@@ -433,9 +431,9 @@
Icon(
imageVector =
if (showConfirm) {
- Icons.Filled.Check
+ Icons.Check
} else {
- Icons.AutoMirrored.Filled.KeyboardArrowRight
+ Icons.AutoMirrored.KeyboardArrowRight
},
contentDescription =
if (showConfirm) {
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
index 469df92..c25b822 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
@@ -107,7 +107,7 @@
* @param modifier Modifier to be applied to the button. When animating the button to appear/
* disappear from the screen, a Modifier.height can be used to change the height of the component,
* but that won't change the space available for the content (though it may be scaled)
- * @param buttonHeight Defines the base size of the button, see the 4 standard sizes specified in
+ * @param preferredHeight Defines the base size of the button, see the 4 standard sizes specified in
* [ButtonDefaults]. This is used to determine the size constraints passed on to the content, and
* the size of the edge button if no height Modifier is specified. Note that if a height Modifier
* is specified for the EdgeButton, that will determine the size of the container, and the content
@@ -130,7 +130,7 @@
fun EdgeButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
- buttonHeight: Dp = ButtonDefaults.EdgeButtonHeightSmall,
+ preferredHeight: Dp = ButtonDefaults.EdgeButtonHeightSmall,
enabled: Boolean = true,
colors: ButtonColors = ButtonDefaults.buttonColors(),
border: BorderStroke? = null,
@@ -143,10 +143,10 @@
val screenWidthDp = screenWidthDp().dp
val contentShapeHelper =
- remember(buttonHeight) {
+ remember(preferredHeight) {
ShapeHelper(density).apply {
// Compute the inner size using only the screen size and the buttonSize parameter
- val size = with(density) { DpSize(screenWidthDp, buttonHeight).toSize() }
+ val size = with(density) { DpSize(screenWidthDp, preferredHeight).toSize() }
update(size)
}
}
@@ -178,7 +178,7 @@
} else {
screenWidthDp.roundToPx()
}
- val buttonHeightPx = with(density) { buttonHeight.roundToPx() }
+ val buttonHeightPx = with(density) { preferredHeight.roundToPx() }
val size =
IntSize(
buttonWidthPx,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/HorizontalPageIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/HorizontalPageIndicator.kt
deleted file mode 100644
index ff584f5..0000000
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/HorizontalPageIndicator.kt
+++ /dev/null
@@ -1,424 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.pager.HorizontalPager
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.CurvedAlignment
-import androidx.wear.compose.foundation.CurvedDirection
-import androidx.wear.compose.foundation.CurvedLayout
-import androidx.wear.compose.foundation.CurvedModifier
-import androidx.wear.compose.foundation.CurvedScope
-import androidx.wear.compose.foundation.angularSizeDp
-import androidx.wear.compose.foundation.background
-import androidx.wear.compose.foundation.curvedBox
-import androidx.wear.compose.foundation.curvedRow
-import androidx.wear.compose.foundation.radialSize
-import androidx.wear.compose.foundation.size
-import androidx.wear.compose.foundation.weight
-import androidx.wear.compose.material3.PageIndicatorDefaults.MaxNumberOfIndicators
-import androidx.wear.compose.materialcore.BoundsLimiter
-import androidx.wear.compose.materialcore.PagesState
-import androidx.wear.compose.materialcore.isLayoutDirectionRtl
-import androidx.wear.compose.materialcore.isRoundDevice
-import kotlin.math.roundToInt
-
-/**
- * Horizontal page indicator for use with [HorizontalPager], representing the currently active page
- * and the total number of pages. Pages are indicated as a Circle shape. The indicator shows up to
- * six pages individually - if there are more than six pages, [HorizontalPageIndicator] shows a
- * half-size indicator to the left or right to indicate that more are available.
- *
- * Here's how different positions 0..10 might be visually represented: "X" is selected item, "O" and
- * "o" full and half size items respectively.
- *
- * O X O O O o - 2nd position out of 10. There are no more items on the left but more on the right o
- * O O O X o - current page could be 6, 7 or 8 out of 10, as there are more possible items on the
- * left and on the right o O O O X O - current page is 9 out of 10, as there're no more items on the
- * right
- *
- * [HorizontalPageIndicator] can be linear or curved, depending on the screen shape of the device -
- * for circular screens it will be curved, whilst for square screens it will be linear.
- *
- * @sample androidx.wear.compose.material3.samples.HorizontalPageIndicatorSample
- *
- * Example usage with HorizontalPager:
- *
- * @sample androidx.wear.compose.material3.samples.HorizontalPageIndicatorWithPagerSample
- * @param pageCount Total number of pages
- * @param currentPage The currently selected page index
- * @param currentPageOffsetFraction The offset fraction of the currently selected page. Represents
- * the offset as a fraction of the transition from the selected page to the next or previous page.
- * Can be positive or negative.
- * @param modifier Modifier to be applied to the [HorizontalPageIndicator]
- * @param selectedColor The color of the selected [HorizontalPageIndicator] item
- * @param unselectedColor The color of unselected [HorizontalPageIndicator] items. Defaults to
- * [selectedColor] with 30% alpha
- * @param indicatorSize The size of each [HorizontalPageIndicator] item in [Dp]
- * @param spacing The spacing between indicator items in [Dp]
- */
-@Composable
-public fun HorizontalPageIndicator(
- pageCount: Int,
- currentPage: Int,
- currentPageOffsetFraction: () -> Float,
- modifier: Modifier = Modifier,
- selectedColor: Color = MaterialTheme.colorScheme.onBackground,
- unselectedColor: Color = selectedColor.copy(alpha = 0.3f),
- indicatorSize: Dp = 6.dp,
- spacing: Dp = 4.dp
-) {
- val isScreenRound = isRoundDevice()
- val padding = PageIndicatorDefaults.edgePadding
-
- // Converting offsetFraction into range 0..1f
- val currentPageOffsetWithFraction = currentPage + currentPageOffsetFraction()
- val selectedPage: Int = currentPageOffsetWithFraction.toInt()
- val offset = currentPageOffsetWithFraction - selectedPage
-
- val pagesOnScreen = Integer.min(MaxNumberOfIndicators, pageCount)
- val pagesState =
- remember(pageCount) { PagesState(totalPages = pageCount, pagesOnScreen = pagesOnScreen) }
- pagesState.recalculateState(selectedPage, offset)
-
- val leftSpacerSize = (indicatorSize + spacing) * pagesState.leftSpacerSizeRatio
- val rightSpacerSize = (indicatorSize + spacing) * pagesState.rightSpacerSizeRatio
-
- if (isScreenRound) {
- var containerSize by remember { mutableStateOf(IntSize.Zero) }
-
- val boundsSize: Density.() -> IntSize = {
- val size =
- IntSize(
- width = ((indicatorSize + spacing).toPx() * pagesOnScreen).roundToInt(),
- height = (indicatorSize * 2).toPx().roundToInt().coerceAtLeast(0)
- )
- size
- }
-
- val boundsOffset: Density.() -> IntOffset = {
- val measuredSize = boundsSize()
- // Offset here is the distance between top left corner of the outer container to
- // the top left corner of the indicator. Its placement should look similar to
- // Alignment.BottomCenter.
- IntOffset(
- x = (containerSize.width - measuredSize.width) / 2 - padding.toPx().toInt(),
- y = containerSize.height - measuredSize.height - padding.toPx().toInt() * 2,
- )
- }
-
- BoundsLimiter(
- offset = boundsOffset,
- size = boundsSize,
- modifier = modifier.padding(padding),
- onSizeChanged = { containerSize = it }
- ) {
- CurvedPageIndicator(
- visibleDotIndex = pagesState.visibleDotIndex,
- pagesOnScreen = pagesOnScreen,
- indicator = { page ->
- curvedIndicator(
- page = page,
- size = indicatorSize,
- unselectedColor = unselectedColor,
- pagesState = pagesState
- )
- },
- itemsSpacer = { curvedSpacer(indicatorSize + spacing) },
- selectedIndicator = {
- curvedSelectedIndicator(
- indicatorSize = indicatorSize,
- spacing = spacing,
- selectedColor = selectedColor,
- progress = offset
- )
- },
- spacerLeft = { curvedSpacer(leftSpacerSize) },
- spacerRight = { curvedSpacer(rightSpacerSize) }
- )
- }
- } else {
- LinearPageIndicator(
- modifier = modifier.padding(padding),
- visibleDotIndex = pagesState.visibleDotIndex,
- pagesOnScreen = pagesOnScreen,
- indicator = { page ->
- LinearIndicator(
- page = page,
- pagesState = pagesState,
- unselectedColor = unselectedColor,
- indicatorSize = indicatorSize,
- spacing = spacing,
- )
- },
- selectedIndicator = {
- LinearSelectedIndicator(
- indicatorSize = indicatorSize,
- spacing = spacing,
- selectedColor = selectedColor,
- progress = offset
- )
- },
- spacerLeft = { LinearSpacer(leftSpacerSize) },
- spacerRight = { LinearSpacer(rightSpacerSize) }
- )
- }
-}
-
-/** Contains the default values used by [HorizontalPageIndicator] */
-internal object PageIndicatorDefaults {
-
- val MaxNumberOfIndicators = 6
- internal val edgePadding = PaddingDefaults.edgePadding
-}
-
-@Composable
-private fun LinearPageIndicator(
- modifier: Modifier,
- visibleDotIndex: Int,
- pagesOnScreen: Int,
- indicator: @Composable (Int) -> Unit,
- selectedIndicator: @Composable () -> Unit,
- spacerLeft: @Composable () -> Unit,
- spacerRight: @Composable () -> Unit
-) {
- Box(
- modifier = Modifier.fillMaxSize(),
- ) {
- Row(
- modifier = modifier.align(Alignment.BottomCenter),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.Bottom
- ) {
- // drawing 1 extra spacer for transition
- spacerLeft()
- for (page in 0 until visibleDotIndex) {
- indicator(page)
- }
- Box(contentAlignment = Alignment.Center) {
- Row(verticalAlignment = Alignment.Bottom) {
- indicator(visibleDotIndex)
- indicator(visibleDotIndex + 1)
- }
- Box { selectedIndicator() }
- }
- for (page in visibleDotIndex + 2..pagesOnScreen) {
- indicator(page)
- }
- spacerRight()
- }
- }
-}
-
-@Composable
-private fun LinearSelectedIndicator(
- indicatorSize: Dp,
- spacing: Dp,
- selectedColor: Color,
- progress: Float
-) {
- val horizontalPadding = spacing / 2
- val isRtl = isLayoutDirectionRtl()
- Spacer(
- modifier =
- Modifier.drawWithCache {
- // Adding 2px to fully cover edges of non-selected indicators
- val strokeWidth = indicatorSize.toPx() + 2
- val startX = horizontalPadding.toPx() + strokeWidth / 2
- val endX = this.size.width - horizontalPadding.toPx() - strokeWidth / 2
- val drawWidth = endX - startX
-
- val startSpacerWeight = (progress * 2 - 1).coerceAtLeast(0f)
- val endSpacerWeight = (1 - progress * 2).coerceAtLeast(0f)
-
- // Adding +1 or -1 for cases when start and end have the same coordinates -
- // otherwise on APIs <= 26 line will not be drawn
- val additionalPixel = if (isRtl) -1 else 1
-
- val start =
- Offset(
- startX +
- drawWidth * (if (isRtl) startSpacerWeight else endSpacerWeight) +
- additionalPixel,
- this.size.height / 2
- )
- val end =
- Offset(
- endX - drawWidth * (if (isRtl) endSpacerWeight else startSpacerWeight),
- this.size.height / 2
- )
- onDrawBehind {
- drawLine(
- color = selectedColor,
- start = start,
- end = end,
- cap = StrokeCap.Round,
- strokeWidth = strokeWidth
- )
- }
- }
- )
-}
-
-@Composable
-private fun LinearIndicator(
- page: Int,
- pagesState: PagesState,
- unselectedColor: Color,
- indicatorSize: Dp,
- spacing: Dp,
-) {
- Spacer(
- modifier =
- Modifier.padding(horizontal = spacing / 2).size(indicatorSize).drawWithCache {
- val strokeWidth = indicatorSize.toPx() * pagesState.sizeRatio(page)
- val start = Offset(strokeWidth / 2 + 1, this.size.height / 2)
- val end = Offset(strokeWidth / 2, this.size.height / 2)
- onDrawBehind {
- drawLine(
- color = unselectedColor,
- start = start,
- end = end,
- cap = StrokeCap.Round,
- alpha = pagesState.alpha(page),
- strokeWidth = strokeWidth
- )
- }
- }
- )
-}
-
-@Composable
-private fun LinearSpacer(leftSpacerSize: Dp) {
- Spacer(Modifier.size(leftSpacerSize, 0.dp))
-}
-
-@Composable
-private fun CurvedPageIndicator(
- visibleDotIndex: Int,
- pagesOnScreen: Int,
- indicator: CurvedScope.(Int) -> Unit,
- itemsSpacer: CurvedScope.() -> Unit,
- selectedIndicator: CurvedScope.() -> Unit,
- spacerLeft: CurvedScope.() -> Unit,
- spacerRight: CurvedScope.() -> Unit
-) {
- CurvedLayout(
- modifier = Modifier,
- // 90 degrees equals to 6 o'clock position, at the bottom of the screen
- anchor = 90f,
- angularDirection = CurvedDirection.Angular.Reversed
- ) {
- // drawing 1 extra spacer for transition
- spacerLeft()
-
- curvedRow(radialAlignment = CurvedAlignment.Radial.Center) {
- for (page in 0 until visibleDotIndex) {
- indicator(page)
- itemsSpacer()
- }
- curvedBox(radialAlignment = CurvedAlignment.Radial.Center) {
- curvedRow(radialAlignment = CurvedAlignment.Radial.Center) {
- indicator(visibleDotIndex)
- itemsSpacer()
- indicator(visibleDotIndex + 1)
- }
- selectedIndicator()
- }
- for (page in visibleDotIndex + 2..pagesOnScreen) {
- itemsSpacer()
- indicator(page)
- }
- }
- spacerRight()
- }
-}
-
-private fun CurvedScope.curvedSelectedIndicator(
- indicatorSize: Dp,
- spacing: Dp,
- selectedColor: Color,
- progress: Float
-) {
-
- val startSpacerWeight = (1 - progress * 2).coerceAtLeast(0f)
- val endSpacerWeight = (progress * 2 - 1).coerceAtLeast(0f)
- val blurbWeight = (1 - startSpacerWeight - endSpacerWeight).coerceAtLeast(0.01f)
-
- // Add 0.5dp to cover the sweepDegrees of unselected indicators
- curvedRow(CurvedModifier.angularSizeDp(spacing + indicatorSize + 0.5.dp)) {
- if (endSpacerWeight > 0f) {
- curvedRow(CurvedModifier.weight(endSpacerWeight)) {}
- }
- curvedRow(
- CurvedModifier.background(selectedColor, cap = StrokeCap.Round)
- .weight(blurbWeight)
- // Adding 0.3dp to fully cover edges of non-selected indicators
- .radialSize(indicatorSize + 0.3.dp)
- ) {}
- if (startSpacerWeight > 0f) {
- curvedRow(CurvedModifier.weight(startSpacerWeight)) {}
- }
- }
-}
-
-private fun CurvedScope.curvedIndicator(
- page: Int,
- unselectedColor: Color,
- pagesState: PagesState,
- size: Dp
-) {
- curvedBox(
- CurvedModifier
- // Ideally we want sweepDegrees to be = 0f, because the circular shape is drawn
- // by the Round StrokeCap.
- // But it can't have 0f value due to limitations of underlying Canvas.
- // Values below 0.2f also give some artifacts b/291753164
- .size(0.2f, size * pagesState.sizeRatio(page))
- .background(
- color =
- unselectedColor.copy(alpha = unselectedColor.alpha * pagesState.alpha(page)),
- cap = StrokeCap.Round
- )
- ) {}
-}
-
-private fun CurvedScope.curvedSpacer(size: Dp) {
- curvedBox(CurvedModifier.angularSizeDp(size).radialSize(0.dp)) {}
-}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Padding.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Padding.kt
index 63f2325..89839e3 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Padding.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Padding.kt
@@ -16,6 +16,9 @@
package androidx.wear.compose.material3
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
internal object PaddingDefaults {
@@ -27,11 +30,31 @@
val verticalContentPaddingPercentage = 10f
/**
+ * Vertical padding between top and bottom edges of the screen and the content for full screen
+ * components, as a dp.
+ */
+ @Composable
+ fun verticalContentPadding(): Dp {
+ val screenHeight = LocalConfiguration.current.screenHeightDp
+ return screenHeight.dp * verticalContentPaddingPercentage / 100
+ }
+
+ /**
* Horizontal padding between start and end edges of the screen and the content for full screen
* components, as a percentage.
*/
val horizontalContentPaddingPercentage = 5.2f
+ /**
+ * Horizontal padding between start and end edges of the screen and the content for full screen
+ * components, as a dp.
+ */
+ @Composable
+ fun horizontalContentPadding(): Dp {
+ val screenWidth = LocalConfiguration.current.screenWidthDp
+ return screenWidth.dp * horizontalContentPaddingPercentage / 100
+ }
+
/** Default minimum padding between the edge of the screen and the content. */
val edgePadding = 2.dp
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PageIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PageIndicator.kt
new file mode 100644
index 0000000..77f72e3
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PageIndicator.kt
@@ -0,0 +1,757 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import androidx.wear.compose.foundation.CurvedAlignment
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedScope
+import androidx.wear.compose.foundation.angularSizeDp
+import androidx.wear.compose.foundation.background
+import androidx.wear.compose.foundation.curvedBox
+import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
+import androidx.wear.compose.foundation.pager.PagerState
+import androidx.wear.compose.foundation.radialSize
+import androidx.wear.compose.foundation.size
+import androidx.wear.compose.foundation.weight
+import androidx.wear.compose.material3.pager.HorizontalPager
+import androidx.wear.compose.material3.pager.VerticalPager
+import androidx.wear.compose.material3.tokens.ColorSchemeKeyTokens
+import androidx.wear.compose.materialcore.BoundsLimiter
+import androidx.wear.compose.materialcore.isLayoutDirectionRtl
+import androidx.wear.compose.materialcore.isRoundDevice
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+/**
+ * Horizontal page indicator for use with [HorizontalPager], representing the currently active page
+ * and the approximate number of pages. Pages are indicated as a Circle shape. The indicator shows
+ * up to six pages individually - if there are more than six pages, [HorizontalPageIndicator] shows
+ * a smaller indicator to the left and/or right to indicate that more pages are available.
+ *
+ * This is a full screen component and will occupy the whole screen. However it's not actionable, so
+ * it's not expected to interfere with anything on the screen.
+ *
+ * Here's how different positions 0..10 might be visually represented: "X" is selected item, "O" and
+ * "o" full and half size items respectively.
+ *
+ * O X O O O o - 2nd position out of 10. There are no more items on the left but more on the right.
+ *
+ * o O O O X o - current page could be 6, 7 or 8 out of 10, as there are more potential pages on the
+ * left and on the right.
+ *
+ * o O O O X O - current page is 9 out of 10, as there no more items on the right
+ *
+ * [HorizontalPageIndicator] can be linear or curved, depending on the screen shape of the device -
+ * for circular screens it will be curved, whilst for square screens it will be linear.
+ *
+ * Example usage with [HorizontalPager]:
+ *
+ * @sample androidx.wear.compose.material3.samples.HorizontalPageIndicatorWithPagerSample
+ * @param pagerState State of the [HorizontalPager] used to control this indicator
+ * @param modifier Modifier to be applied to the [HorizontalPageIndicator]
+ * @param selectedColor The color which will be used for a selected indicator item.
+ * @param unselectedColor The color which will be used for an unselected indicator item.
+ * @param backgroundColor The color which will be used for an indicator background.
+ */
+@Composable
+fun HorizontalPageIndicator(
+ pagerState: PagerState,
+ modifier: Modifier = Modifier,
+ selectedColor: Color = PageIndicatorDefaults.selectedColor,
+ unselectedColor: Color = PageIndicatorDefaults.unselectedColor,
+ backgroundColor: Color = PageIndicatorDefaults.backgroundColor,
+) {
+ PageIndicatorImpl(
+ pagerState = pagerState,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor,
+ modifier = modifier,
+ indicatorSize = PageIndicatorItemSize,
+ spacing = PageIndicatorSpacing,
+ isHorizontal = true,
+ )
+}
+
+/**
+ * Vertical page indicator for use with [VerticalPager], representing the currently active page and
+ * the approximate number of pages. Pages are indicated as a Circle shape. The indicator shows up to
+ * six pages individually - if there are more than six pages, [VerticalPageIndicator] shows a
+ * smaller indicator to the top and/or bottom to indicate that more pages are available.
+ *
+ * This is a full screen component and will occupy the whole screen. However it's not actionable, so
+ * it's not expected to interfere with anything on the screen.
+ *
+ * [VerticalPageIndicator] can be linear or curved, depending on the screen shape of the device -
+ * for circular screens it will be curved, whilst for square screens it will be linear.
+ *
+ * Example usage with [VerticalPager]:
+ *
+ * @sample androidx.wear.compose.material3.samples.VerticalPageIndicatorWithPagerSample
+ * @param pagerState State of the [VerticalPager] used to control this indicator
+ * @param modifier Modifier to be applied to the [VerticalPageIndicator]
+ * @param selectedColor The color which will be used for a selected indicator item.
+ * @param unselectedColor The color which will be used for an unselected indicator item.
+ * @param backgroundColor The color which will be used for an indicator background.
+ */
+@Composable
+fun VerticalPageIndicator(
+ pagerState: PagerState,
+ modifier: Modifier = Modifier,
+ selectedColor: Color = PageIndicatorDefaults.selectedColor,
+ unselectedColor: Color = PageIndicatorDefaults.unselectedColor,
+ backgroundColor: Color = PageIndicatorDefaults.backgroundColor,
+) {
+ PageIndicatorImpl(
+ pagerState = pagerState,
+ selectedColor = selectedColor,
+ unselectedColor = unselectedColor,
+ backgroundColor = backgroundColor,
+ modifier = modifier,
+ indicatorSize = PageIndicatorItemSize,
+ spacing = PageIndicatorSpacing,
+ isHorizontal = false,
+ )
+}
+
+/** Contains the default values used by [HorizontalPageIndicator] and [VerticalPageIndicator] */
+object PageIndicatorDefaults {
+
+ /**
+ * The recommended color to use for the selected indicator item in [VerticalPageIndicator] and
+ * [HorizontalPageIndicator].
+ */
+ val selectedColor: Color
+ @ReadOnlyComposable @Composable get() = ColorSchemeKeyTokens.OnBackground.value
+
+ /**
+ * The recommended color to use for the unselected indicator item in [VerticalPageIndicator] and
+ * [HorizontalPageIndicator].
+ */
+ val unselectedColor: Color
+ @ReadOnlyComposable
+ @Composable
+ get() = ColorSchemeKeyTokens.OnBackground.value.copy(alpha = 0.3f)
+
+ /**
+ * The recommended color to use for the background in [VerticalPageIndicator] and
+ * [HorizontalPageIndicator].
+ */
+ val backgroundColor: Color
+ @ReadOnlyComposable
+ @Composable
+ get() = ColorSchemeKeyTokens.Background.value.copy(alpha = 0.85f)
+}
+
+@Composable
+internal fun PageIndicatorImpl(
+ pagerState: PagerState,
+ selectedColor: Color,
+ unselectedColor: Color,
+ backgroundColor: Color,
+ modifier: Modifier,
+ indicatorSize: Dp,
+ spacing: Dp,
+ isHorizontal: Boolean,
+) {
+ val isScreenRound = isRoundDevice()
+ val layoutDirection = LocalLayoutDirection.current
+ val edgePadding = PaddingDefaults.edgePadding
+
+ // Converting offsetFraction into range 0..1f
+ val currentPageOffsetWithFraction =
+ pagerState.currentPage + pagerState.currentPageOffsetFraction
+ val selectedPage: Int = currentPageOffsetWithFraction.toInt()
+ val offset = currentPageOffsetWithFraction - selectedPage
+
+ val pagesOnScreen = Integer.min(MaxNumberOfIndicators, pagerState.pageCount)
+ val pagesState =
+ remember(pagerState.pageCount) {
+ PagesState(
+ totalPages = pagerState.pageCount,
+ pagesOnScreen = pagesOnScreen,
+ smallIndicatorSize = smallIndicatorSize
+ )
+ }
+ pagesState.recalculateState(selectedPage, offset)
+
+ val leftSpacerSize = (indicatorSize + spacing) * pagesState.leftSpacerSizeRatio
+ val rightSpacerSize = (indicatorSize + spacing) * pagesState.rightSpacerSizeRatio
+
+ if (isScreenRound) {
+ var containerSize by remember { mutableStateOf(IntSize.Zero) }
+
+ val boundsSize: Density.() -> IntSize = {
+ val width = ((indicatorSize + spacing).toPx() * pagesOnScreen).roundToInt()
+ val height = (indicatorSize * 2).roundToPx().coerceAtLeast(0)
+ val size =
+ IntSize(
+ width = if (isHorizontal) width else height,
+ height = if (isHorizontal) height else width
+ )
+ size
+ }
+
+ val boundsOffset: Density.() -> IntOffset = {
+ val measuredSize = boundsSize()
+ if (isHorizontal) {
+ // Offset here is the distance between top left corner of the outer container to
+ // the top left corner of the indicator. Its placement should look similar to
+ // Alignment.BottomCenter.
+ IntOffset(
+ x = (containerSize.width - measuredSize.width) / 2 - edgePadding.roundToPx(),
+ y = containerSize.height - measuredSize.height - edgePadding.roundToPx() * 2,
+ )
+ } else {
+ // Offset here is the distance between top left corner of the outer container to
+ // the top left corner of the indicator. Its placement should look similar to
+ // Alignment.CenterEnd.
+ IntOffset(
+ x =
+ if (layoutDirection == LayoutDirection.Ltr) {
+ containerSize.width - measuredSize.width - edgePadding.roundToPx() * 2
+ } else edgePadding.roundToPx(),
+ y = (containerSize.height - measuredSize.height) / 2 - edgePadding.roundToPx(),
+ )
+ }
+ }
+ // As we use an extra spacers to the start and end of horizontal indicator ( and higher and
+ // lower for vertical), we have to set their size in angular padding to compensate for that.
+ val angularPadding = -(spacing + indicatorSize)
+ BoundsLimiter(
+ offset = boundsOffset,
+ size = boundsSize,
+ modifier = modifier.padding(edgePadding),
+ onSizeChanged = { containerSize = it }
+ ) {
+ CurvedPageIndicator(
+ visibleDotIndex = pagesState.visibleDotIndex,
+ pagesOnScreen = pagesOnScreen,
+ indicator = { page ->
+ curvedIndicator(
+ page = page,
+ size = indicatorSize,
+ unselectedColor = unselectedColor,
+ pagesState = pagesState
+ )
+ },
+ itemsSpacer = { curvedSpacer(indicatorSize + spacing) },
+ selectedIndicator = {
+ curvedSelectedIndicator(
+ indicatorSize = indicatorSize,
+ spacing = spacing,
+ selectedColor = selectedColor,
+ progress = offset
+ )
+ },
+ spacerLeft = { curvedSpacer(leftSpacerSize) },
+ spacerRight = { curvedSpacer(rightSpacerSize) },
+ angularPadding = angularPadding,
+ isHorizontal = isHorizontal,
+ layoutDirection = layoutDirection,
+ backgroundColor = backgroundColor
+ )
+ }
+ } else {
+ LinearPageIndicator(
+ modifier = modifier.padding(vertical = edgePadding),
+ visibleDotIndex = pagesState.visibleDotIndex,
+ pagesOnScreen = pagesOnScreen,
+ indicator = { page ->
+ LinearIndicator(
+ page = page,
+ pagesState = pagesState,
+ unselectedColor = unselectedColor,
+ indicatorSize = indicatorSize,
+ spacing = spacing,
+ )
+ },
+ selectedIndicator = {
+ LinearSelectedIndicator(
+ indicatorSize = indicatorSize,
+ spacing = spacing,
+ selectedColor = selectedColor,
+ progress = offset
+ )
+ },
+ spacerStart = { LinearSpacer(leftSpacerSize) },
+ spacerEnd = { LinearSpacer(rightSpacerSize) },
+ isHorizontal = isHorizontal,
+ layoutDirection = layoutDirection,
+ background = {
+ Box(
+ modifier =
+ Modifier.align(Alignment.BottomCenter)
+ .size(
+ width =
+ (pagesOnScreen * indicatorSize.value +
+ (pagesOnScreen - 1) * spacing.value)
+ .dp + BackgroundRadius * 2,
+ height = indicatorSize + BackgroundRadius * 2
+ )
+ .background(color = backgroundColor, shape = RoundedCornerShape(50.dp))
+ )
+ },
+ )
+ }
+}
+
+// TODO(b/369535289) Fix a visual issue with linear indicator when there are more than 6 pages.
+@Composable
+private fun LinearPageIndicator(
+ modifier: Modifier,
+ visibleDotIndex: Int,
+ pagesOnScreen: Int,
+ indicator: @Composable (Int) -> Unit,
+ selectedIndicator: @Composable () -> Unit,
+ spacerStart: @Composable () -> Unit,
+ spacerEnd: @Composable () -> Unit,
+ isHorizontal: Boolean,
+ layoutDirection: LayoutDirection,
+ background: @Composable BoxScope.() -> Unit
+) {
+ val width = LocalConfiguration.current.screenWidthDp
+ val height = LocalConfiguration.current.screenHeightDp
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ modifier =
+ modifier.let {
+ if (isHorizontal) it.size(width = width.dp, height = height.dp)
+ // Flip width and height so that the indicator will fit into rectangular screen,
+ // rotate it -90 degrees, and flip it vertically
+ else
+ it.size(width = height.dp, height = width.dp)
+ .align(Alignment.Center)
+ .graphicsLayer {
+ rotationZ =
+ if (layoutDirection == LayoutDirection.Ltr) -90f else 90f
+ scaleX = -1f
+ scaleY = 1f
+ }
+ }
+ ) {
+ background()
+ Row(
+ modifier =
+ Modifier.padding(bottom = BackgroundRadius).align(Alignment.BottomCenter),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.Bottom,
+ ) {
+ // drawing 1 extra spacer for transition
+ spacerStart()
+ for (page in 0 until visibleDotIndex) {
+ indicator(page)
+ }
+ Box(contentAlignment = Alignment.Center) {
+ Row(verticalAlignment = Alignment.Bottom) {
+ indicator(visibleDotIndex)
+ indicator(visibleDotIndex + 1)
+ }
+ Box { selectedIndicator() }
+ }
+ for (page in visibleDotIndex + 2..pagesOnScreen) {
+ indicator(page)
+ }
+ spacerEnd()
+ }
+ }
+ }
+}
+
+@Composable
+private fun LinearSelectedIndicator(
+ indicatorSize: Dp,
+ spacing: Dp,
+ selectedColor: Color,
+ progress: Float
+) {
+ val horizontalPadding = spacing / 2
+ val isRtl = isLayoutDirectionRtl()
+ Spacer(
+ modifier =
+ Modifier.drawWithCache {
+ // Adding 2px to fully cover edges of non-selected indicators
+ val strokeWidth = indicatorSize.toPx() + 2
+ val startX = horizontalPadding.toPx() + strokeWidth / 2
+ val endX = this.size.width - horizontalPadding.toPx() - strokeWidth / 2
+ val drawWidth = endX - startX
+
+ val startSpacerWeight = (progress * 2 - 1).coerceAtLeast(0f)
+ val endSpacerWeight = (1 - progress * 2).coerceAtLeast(0f)
+
+ // Adding +1 or -1 for cases when start and end have the same coordinates -
+ // otherwise on APIs <= 26 line will not be drawn
+ val additionalPixel = if (isRtl) -1 else 1
+
+ val start =
+ Offset(
+ startX +
+ drawWidth * (if (isRtl) startSpacerWeight else endSpacerWeight) +
+ additionalPixel,
+ this.size.height / 2
+ )
+ val end =
+ Offset(
+ endX - drawWidth * (if (isRtl) endSpacerWeight else startSpacerWeight),
+ this.size.height / 2
+ )
+ onDrawBehind {
+ drawLine(
+ color = selectedColor,
+ start = start,
+ end = end,
+ cap = StrokeCap.Round,
+ strokeWidth = strokeWidth
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun LinearIndicator(
+ page: Int,
+ pagesState: PagesState,
+ unselectedColor: Color,
+ indicatorSize: Dp,
+ spacing: Dp,
+) {
+ Spacer(
+ modifier =
+ Modifier.padding(horizontal = spacing / 2).size(indicatorSize).drawWithCache {
+ val strokeWidth = indicatorSize.toPx() * pagesState.sizeRatio(page)
+ val start = Offset(strokeWidth / 2 + 1, this.size.height / 2)
+ val end = Offset(strokeWidth / 2, this.size.height / 2)
+ onDrawBehind {
+ drawLine(
+ color = unselectedColor,
+ start = start,
+ end = end,
+ cap = StrokeCap.Round,
+ alpha = pagesState.alpha(page),
+ strokeWidth = strokeWidth
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun LinearSpacer(leftSpacerSize: Dp) {
+ Spacer(Modifier.size(leftSpacerSize, 0.dp))
+}
+
+@Composable
+private fun CurvedPageIndicator(
+ visibleDotIndex: Int,
+ pagesOnScreen: Int,
+ indicator: CurvedScope.(Int) -> Unit,
+ itemsSpacer: CurvedScope.() -> Unit,
+ selectedIndicator: CurvedScope.() -> Unit,
+ spacerLeft: CurvedScope.() -> Unit,
+ spacerRight: CurvedScope.() -> Unit,
+ isHorizontal: Boolean,
+ layoutDirection: LayoutDirection,
+ angularPadding: Dp,
+ backgroundColor: Color,
+) {
+ val anchor =
+ if (isHorizontal) HorizontalPagerAnchor
+ else {
+ if (layoutDirection == LayoutDirection.Ltr) VerticalPagerAnchor
+ else VerticalPagerRtlAnchor
+ }
+ val angularDirection =
+ if (isHorizontal) CurvedDirection.Angular.Reversed else CurvedDirection.Angular.Normal
+
+ CurvedLayout(modifier = Modifier, anchor = anchor, angularDirection = angularDirection) {
+ // drawing 1 extra spacer for transition
+ curvedRow(
+ modifier =
+ CurvedModifier.background(backgroundColor, cap = StrokeCap.Round)
+ .padding(radial = BackgroundRadius, angular = angularPadding)
+ ) {
+ spacerLeft()
+ curvedRow(radialAlignment = CurvedAlignment.Radial.Center) {
+ for (page in 0 until visibleDotIndex) {
+ indicator(page)
+ itemsSpacer()
+ }
+ curvedBox(radialAlignment = CurvedAlignment.Radial.Center) {
+ curvedRow(radialAlignment = CurvedAlignment.Radial.Center) {
+ indicator(visibleDotIndex)
+ itemsSpacer()
+ indicator(visibleDotIndex + 1)
+ }
+ selectedIndicator()
+ }
+ for (page in visibleDotIndex + 2..pagesOnScreen) {
+ itemsSpacer()
+ indicator(page)
+ }
+ }
+ spacerRight()
+ }
+ }
+}
+
+private fun CurvedScope.curvedSelectedIndicator(
+ indicatorSize: Dp,
+ spacing: Dp,
+ selectedColor: Color,
+ progress: Float
+) {
+
+ val startSpacerWeight = (1 - progress * 2).coerceAtLeast(0f)
+ val endSpacerWeight = (progress * 2 - 1).coerceAtLeast(0f)
+ val blurbWeight = (1 - startSpacerWeight - endSpacerWeight).coerceAtLeast(0.01f)
+
+ // Add 0.5dp to cover the sweepDegrees of unselected indicators
+ curvedRow(CurvedModifier.angularSizeDp(spacing + indicatorSize + 0.5.dp)) {
+ if (endSpacerWeight > 0f) {
+ curvedRow(CurvedModifier.weight(endSpacerWeight)) {}
+ }
+ curvedRow(
+ CurvedModifier.background(selectedColor, cap = StrokeCap.Round)
+ .weight(blurbWeight)
+ // Adding 0.3dp to fully cover edges of non-selected indicators
+ .radialSize(indicatorSize + 0.3.dp)
+ ) {}
+ if (startSpacerWeight > 0f) {
+ curvedRow(CurvedModifier.weight(startSpacerWeight)) {}
+ }
+ }
+}
+
+private fun CurvedScope.curvedIndicator(
+ page: Int,
+ unselectedColor: Color,
+ pagesState: PagesState,
+ size: Dp
+) {
+ curvedBox(
+ CurvedModifier
+ // Ideally we want sweepDegrees to be = 0f, because the circular shape is drawn
+ // by the Round StrokeCap.
+ // But it can't have 0f value due to limitations of underlying Canvas.
+ // Values below 0.2f also give some artifacts b/291753164
+ .size(0.2f, size * pagesState.sizeRatio(page))
+ .background(
+ color =
+ unselectedColor.copy(alpha = unselectedColor.alpha * pagesState.alpha(page)),
+ cap = StrokeCap.Round
+ )
+ ) {}
+}
+
+private fun CurvedScope.curvedSpacer(size: Dp) {
+ curvedBox(CurvedModifier.angularSizeDp(size).radialSize(0.dp)) {}
+}
+
+/**
+ * Represents an internal state of pageIndicator. This state is responsible for keeping and
+ * recalculating alpha and size parameters of each indicator, and selected indicators as well.
+ */
+private class PagesState(
+ val totalPages: Int,
+ val pagesOnScreen: Int,
+ val smallIndicatorSize: Float
+) {
+ // Sizes and alphas of first and last indicators on the screen. Used to show that there're more
+ // pages on the left or on the right, and also for smooth transitions
+ private var firstAlpha = 1f
+ private var lastAlpha = 0f
+ private var firstSize = 1f
+ private var secondSize = 1f
+ private var lastSize = 1f
+ private var lastButOneSize = 1f
+
+ private var smoothProgress = 0f
+
+ // An offset in pages, basically meaning how many pages are hidden to the left.
+ private var hiddenPagesToTheLeft = 0
+
+ // A default size of spacers - invisible items to the left and to the right of
+ // visible indicators, used for smooth transitions
+
+ // Current visible position on the screen.
+ var visibleDotIndex = 0
+ private set
+
+ // A size of a left spacer used for smooth transitions
+ val leftSpacerSizeRatio
+ get() = 1 - smoothProgress
+
+ // A size of a right spacer used for smooth transitions
+ val rightSpacerSizeRatio
+ get() = smoothProgress
+
+ /**
+ * Depending on the page index, return an alpha for this indicator
+ *
+ * @param page Page index
+ * @return An alpha of page index- in range 0..1
+ */
+ fun alpha(page: Int): Float =
+ when (page) {
+ 0 -> firstAlpha
+ pagesOnScreen -> lastAlpha
+ else -> 1f
+ }
+
+ /**
+ * Depending on the page index, return a size ratio for this indicator
+ *
+ * @param page Page index
+ * @return An size ratio for page index - in range 0..1
+ */
+ fun sizeRatio(page: Int): Float =
+ when (page) {
+ 0 -> firstSize
+ 1 -> secondSize
+ pagesOnScreen - 1 -> lastButOneSize
+ pagesOnScreen -> lastSize
+ else -> 1f
+ }
+
+ /**
+ * Returns a value in the range 0..1 where 0 is unselected state, and 1 is selected. Used to
+ * show a smooth transition between page indicator items.
+ */
+ fun calculateSelectedRatio(targetPage: Int, offset: Float): Float =
+ (1 - abs(visibleDotIndex + offset - targetPage)).coerceAtLeast(0f)
+
+ // Main function responsible for recalculation of all parameters regarding
+ // to the [selectedPage] and [offset]
+ fun recalculateState(selectedPage: Int, offset: Float) {
+ val pageWithOffset = selectedPage + offset
+ // Calculating offsetInPages relating to the [selectedPage].
+
+ // For example, for [selectedPage] = 4 we will see this picture :
+ // O O O O X o. [offsetInPages] will be 0.
+ // But when [selectedPage] will be incremented to 5, it will be seen as
+ // o O O O X o, with [offsetInPages] = 1
+ if (selectedPage > hiddenPagesToTheLeft + pagesOnScreen - 2) {
+ // Set an offset as a difference between current page and pages on the screen,
+ // except if this is not the last page - then offsetInPages is not changed
+ hiddenPagesToTheLeft =
+ (selectedPage - (pagesOnScreen - 2)).coerceAtMost(totalPages - pagesOnScreen)
+ } else if (pageWithOffset <= hiddenPagesToTheLeft) {
+ hiddenPagesToTheLeft = (selectedPage - 1).coerceAtLeast(0)
+ }
+
+ // Condition for scrolling to the right. A smooth scroll to the right is only triggered
+ // when we have more than 2 pages to the right, and currently we're on the right edge.
+ // For example -> o O O O X o -> a small "o" shows that there're more pages to the right
+ val scrolledToTheRight =
+ pageWithOffset > hiddenPagesToTheLeft + pagesOnScreen - 2 &&
+ pageWithOffset < totalPages - 2
+
+ // Condition for scrolling to the left. A smooth scroll to the left is only triggered
+ // when we have more than 2 pages to the left, and currently we're on the left edge.
+ // For example -> o X O O O o -> a small "o" shows that there're more pages to the left
+ val scrolledToTheLeft = pageWithOffset > 1 && pageWithOffset < hiddenPagesToTheLeft + 1
+
+ smoothProgress = if (scrolledToTheLeft || scrolledToTheRight) offset else 0f
+
+ // Calculating exact parameters for border indicators like [firstAlpha], [lastSize], etc.
+ firstAlpha = 1 - smoothProgress
+ lastAlpha = smoothProgress
+ secondSize = 1 - (1 - smallIndicatorSize) * smoothProgress
+
+ // Depending on offsetInPages we'll either show a shrinked first indicator, or full-size
+ firstSize =
+ if (hiddenPagesToTheLeft == 0 || hiddenPagesToTheLeft == 1 && scrolledToTheLeft) {
+ 1 - smoothProgress
+ } else {
+ smallIndicatorSize * (1 - smoothProgress)
+ }
+
+ // Depending on offsetInPages and other parameters, we'll either show a shrinked
+ // last indicator, or full-size
+ lastSize =
+ if (
+ hiddenPagesToTheLeft == totalPages - pagesOnScreen - 1 && scrolledToTheRight ||
+ hiddenPagesToTheLeft == totalPages - pagesOnScreen && scrolledToTheLeft
+ ) {
+ smoothProgress
+ } else {
+ smallIndicatorSize * smoothProgress
+ }
+
+ lastButOneSize =
+ if (scrolledToTheRight || scrolledToTheLeft) {
+ lerp(smallIndicatorSize, 1f, smoothProgress)
+ } else if (hiddenPagesToTheLeft < totalPages - pagesOnScreen) smallIndicatorSize else 1f
+
+ // A visibleDot represents a currently selected page on the screen
+ // As we scroll to the left, we add an invisible indicator to the left, shifting all other
+ // indicators to the right. The shift is only possible when a visibleDot = 1,
+ // thus we have to leave it at 1 as we always add a positive offset
+ visibleDotIndex = if (scrolledToTheLeft) 1 else selectedPage - hiddenPagesToTheLeft
+ }
+}
+
+private const val smallIndicatorSize = 0.66f
+private const val MaxNumberOfIndicators = 6
+
+// 0 degrees equals to 3 o'clock position, at the right of the screen
+private val VerticalPagerAnchor = 0f
+// 180 degrees equals to 9 o'clock position, at the left of the screen
+private val VerticalPagerRtlAnchor = 180f
+// 90 degrees equals to 6 o'clock position, at the bottom of the screen
+private val HorizontalPagerAnchor = 90f
+/** The default size of the indicator */
+internal val PageIndicatorItemSize = 6.dp
+/** The default spacing between the indicators */
+internal val PageIndicatorSpacing = 4.dp
+internal val BackgroundRadius = 3.dp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
index 6326651..dd4ff33 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
@@ -28,8 +28,6 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
@@ -52,6 +50,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.SliderDefaults.MaxSegmentSteps
+import androidx.wear.compose.material3.internal.Icons
import androidx.wear.compose.material3.internal.Strings.Companion.SliderDecreaseButtonContentDescription
import androidx.wear.compose.material3.internal.Strings.Companion.SliderIncreaseButtonContentDescription
import androidx.wear.compose.material3.internal.getString
@@ -60,7 +59,6 @@
import androidx.wear.compose.materialcore.InlineSliderButton
import androidx.wear.compose.materialcore.RangeDefaults.calculateCurrentStepValue
import androidx.wear.compose.materialcore.RangeDefaults.snapValueToStep
-import androidx.wear.compose.materialcore.RangeIcons
import androidx.wear.compose.materialcore.directedValue
import kotlin.math.roundToInt
@@ -228,8 +226,9 @@
*
* The bar in the middle of control can have separators if [segmented] flag is set to true. A number
* of steps is calculated as the difference between max and min values of [valueProgression] divided
- * by [valueProgression].step - 1. For example, with a range of 100..120 and a step 5, number of
- * steps will be (120-100)/ 5 - 1 = 3. Steps are 100(first), 105, 110, 115, 120(last)
+ * by [valueProgression].step - 1. For example, with a range of 100..120 and a step 5, number of by
+ * [valueProgression].step - 1. For example, with a range of 100..120 and a step 5, number of steps
+ * will be (120-100)/ 5 - 1 = 3. Steps are 100(first), 105, 110, 115, 120(last)
*
* If [valueProgression] range is not equally divisible by [valueProgression].step, then
* [valueProgression].last will be adjusted to the closest divisible value in the range. For
@@ -306,7 +305,7 @@
@Composable
fun DecreaseIcon(modifier: Modifier = Modifier) =
Icon(
- RangeIcons.Minus,
+ Icons.Remove,
getString(SliderDecreaseButtonContentDescription),
modifier.size(IconSize)
)
@@ -314,11 +313,7 @@
/** Increase Icon. */
@Composable
fun IncreaseIcon(modifier: Modifier = Modifier) =
- Icon(
- Icons.Filled.Add,
- getString(SliderIncreaseButtonContentDescription),
- modifier.size(IconSize)
- )
+ Icon(Icons.Add, getString(SliderIncreaseButtonContentDescription), modifier.size(IconSize))
/**
* Creates a [SliderColors] that represents the default background and content colors used in an
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
index 5027491..1c27ebf 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
@@ -57,6 +57,7 @@
import androidx.wear.compose.foundation.RevealActionType
import androidx.wear.compose.foundation.RevealState
import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeDirection
import androidx.wear.compose.foundation.SwipeToReveal
import androidx.wear.compose.foundation.createAnchors
import androidx.wear.compose.material3.ButtonDefaults.buttonColors
@@ -311,6 +312,7 @@
* @param useAnchoredActions Whether the actions should stay revealed, or bounce back to hidden when
* the user stops swiping. This is relevant for SwipeToReveal components with a single action. If
* the developer wants a swipe to clear behaviour, this should be set to false.
+ * @param swipeDirection Direction of the swipe to reveal the actions.
*/
@OptIn(ExperimentalWearFoundationApi::class)
@Composable
@@ -318,6 +320,7 @@
initialValue: RevealValue = RevealValue.Covered,
anchorWidth: Dp = SwipeToRevealDefaults.SingleActionAnchorWidth,
useAnchoredActions: Boolean = true,
+ swipeDirection: SwipeDirection = SwipeDirection.RightToLeft,
): RevealState {
val anchorFraction = anchorWidth.value / screenWidthDp()
return androidx.wear.compose.foundation.rememberRevealState(
@@ -326,7 +329,8 @@
anchors =
createAnchors(
revealingAnchor = if (useAnchoredActions) anchorFraction else 0f,
- )
+ swipeDirection = swipeDirection,
+ ),
)
}
@@ -423,7 +427,13 @@
} else {
if (hasUndo || revealActionType == RevealActionType.PrimaryAction) {
revealState.lastActionType = revealActionType
- revealState.animateTo(RevealValue.Revealed)
+ revealState.animateTo(
+ if (revealState.offset > 0) {
+ RevealValue.LeftRevealed
+ } else {
+ RevealValue.RightRevealed
+ }
+ )
}
}
} finally {
@@ -463,7 +473,8 @@
}
primaryActionTextRevealed.value =
abs(revealState.offset) > minimumOffsetToRevealPx &&
- revealState.targetValue == RevealValue.Revealed
+ (revealState.targetValue == RevealValue.RightRevealed ||
+ revealState.targetValue == RevealValue.LeftRevealed)
}
}
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
index cffdb96..cb1daac 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
@@ -33,8 +33,6 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@@ -63,6 +61,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.wear.compose.material3.ButtonDefaults.buttonColors
+import androidx.wear.compose.material3.internal.Icons
import androidx.wear.compose.material3.internal.Plurals
import androidx.wear.compose.material3.internal.Strings
import androidx.wear.compose.material3.internal.getPlurals
@@ -333,7 +332,7 @@
}
.focusRequester(focusRequesterConfirmButton)
.focusable(),
- buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
+ preferredHeight = ButtonDefaults.EdgeButtonHeightSmall,
colors =
buttonColors(
contentColor = colors.confirmButtonContentColor,
@@ -341,7 +340,7 @@
),
) {
Icon(
- imageVector = Icons.Filled.Check,
+ imageVector = Icons.Check,
contentDescription = getString(Strings.PickerConfirmButtonContentDescription),
modifier = Modifier.size(24.dp).wrapContentSize(align = Alignment.Center),
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Icons.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Icons.kt
new file mode 100644
index 0000000..8825d23
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Icons.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2024 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.compose.material3.internal
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.PathBuilder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+internal object Icons {
+ internal val Add: ImageVector
+ get() {
+ if (_add != null) {
+ return _add!!
+ }
+ _add =
+ materialIcon(name = "Add") {
+ materialPath {
+ moveTo(440f, 520f)
+ lineTo(240f, 520f)
+ quadTo(223f, 520f, 211.5f, 508.5f)
+ quadTo(200f, 497f, 200f, 480f)
+ quadTo(200f, 463f, 211.5f, 451.5f)
+ quadTo(223f, 440f, 240f, 440f)
+ lineTo(440f, 440f)
+ lineTo(440f, 240f)
+ quadTo(440f, 223f, 451.5f, 211.5f)
+ quadTo(463f, 200f, 480f, 200f)
+ quadTo(497f, 200f, 508.5f, 211.5f)
+ quadTo(520f, 223f, 520f, 240f)
+ lineTo(520f, 440f)
+ lineTo(720f, 440f)
+ quadTo(737f, 440f, 748.5f, 451.5f)
+ quadTo(760f, 463f, 760f, 480f)
+ quadTo(760f, 497f, 748.5f, 508.5f)
+ quadTo(737f, 520f, 720f, 520f)
+ lineTo(520f, 520f)
+ lineTo(520f, 720f)
+ quadTo(520f, 737f, 508.5f, 748.5f)
+ quadTo(497f, 760f, 480f, 760f)
+ quadTo(463f, 760f, 451.5f, 748.5f)
+ quadTo(440f, 737f, 440f, 720f)
+ lineTo(440f, 520f)
+ close()
+ }
+ }
+ return _add!!
+ }
+
+ private var _add: ImageVector? = null
+
+ internal val Remove: ImageVector
+ get() {
+ if (_remove != null) {
+ return _remove!!
+ }
+ _remove =
+ materialIcon(name = "Remove") {
+ materialPath {
+ moveTo(240f, 520f)
+ quadTo(223f, 520f, 211.5f, 508.5f)
+ quadTo(200f, 497f, 200f, 480f)
+ quadTo(200f, 463f, 211.5f, 451.5f)
+ quadTo(223f, 440f, 240f, 440f)
+ lineTo(720f, 440f)
+ quadTo(737f, 440f, 748.5f, 451.5f)
+ quadTo(760f, 463f, 760f, 480f)
+ quadTo(760f, 497f, 748.5f, 508.5f)
+ quadTo(737f, 520f, 720f, 520f)
+ lineTo(240f, 520f)
+ close()
+ }
+ }
+ return _remove!!
+ }
+
+ private var _remove: ImageVector? = null
+
+ internal val Check: ImageVector
+ get() {
+ if (_check != null) {
+ return _check!!
+ }
+ _check =
+ materialIcon(name = "Check") {
+ materialPath {
+ moveTo(382f, 597.87f)
+ lineTo(716.7f, 263.17f)
+ quadTo(730.37f, 249.5f, 748.76f, 249.5f)
+ quadTo(767.15f, 249.5f, 780.83f, 263.17f)
+ quadTo(794.5f, 276.85f, 794.5f, 295.62f)
+ quadTo(794.5f, 314.39f, 780.83f, 328.07f)
+ lineTo(414.07f, 695.59f)
+ quadTo(400.39f, 709.26f, 382f, 709.26f)
+ quadTo(363.61f, 709.26f, 349.93f, 695.59f)
+ lineTo(178.41f, 524.07f)
+ quadTo(164.74f, 510.39f, 165.12f, 491.62f)
+ quadTo(165.5f, 472.85f, 179.17f, 459.17f)
+ quadTo(192.85f, 445.5f, 211.62f, 445.5f)
+ quadTo(230.39f, 445.5f, 244.07f, 459.17f)
+ lineTo(382f, 597.87f)
+ close()
+ }
+ }
+ return _check!!
+ }
+
+ private var _check: ImageVector? = null
+
+ internal object AutoMirrored {
+ internal val KeyboardArrowRight: ImageVector
+ get() {
+ if (_keyboardArrowRight != null) {
+ return _keyboardArrowRight!!
+ }
+ _keyboardArrowRight =
+ materialIcon(
+ name = "AutoMirrored.KeyboardArrowRight",
+ autoMirror = true,
+ ) {
+ materialPath {
+ moveTo(496.35f, 480f)
+ lineTo(344.17f, 327.83f)
+ quadTo(331.5f, 315.15f, 331.5f, 296f)
+ quadTo(331.5f, 276.85f, 344.17f, 264.17f)
+ quadTo(356.85f, 251.5f, 376f, 251.5f)
+ quadTo(395.15f, 251.5f, 407.83f, 264.17f)
+ lineTo(591.59f, 447.93f)
+ quadTo(598.3f, 454.65f, 601.4f, 462.85f)
+ quadTo(604.5f, 471.04f, 604.5f, 480f)
+ quadTo(604.5f, 488.96f, 601.4f, 497.15f)
+ quadTo(598.3f, 505.35f, 591.59f, 512.07f)
+ lineTo(407.83f, 695.83f)
+ quadTo(395.15f, 708.5f, 376f, 708.5f)
+ quadTo(356.85f, 708.5f, 344.17f, 695.83f)
+ quadTo(331.5f, 683.15f, 331.5f, 664f)
+ quadTo(331.5f, 644.85f, 344.17f, 632.17f)
+ lineTo(496.35f, 480f)
+ close()
+ }
+ }
+ return _keyboardArrowRight!!
+ }
+
+ private var _keyboardArrowRight: ImageVector? = null
+ }
+}
+
+private inline fun materialIcon(
+ name: String,
+ autoMirror: Boolean = false,
+ block: ImageVector.Builder.() -> ImageVector.Builder
+): ImageVector =
+ ImageVector.Builder(
+ name = name,
+ defaultWidth = MaterialIconDimension,
+ defaultHeight = MaterialIconDimension,
+ viewportWidth = MaterialIconViewPointDimension,
+ viewportHeight = MaterialIconViewPointDimension,
+ autoMirror = autoMirror
+ )
+ .block()
+ .build()
+
+private inline fun ImageVector.Builder.materialPath(pathBuilder: PathBuilder.() -> Unit) =
+ path(fill = SolidColor(Color.White), pathBuilder = pathBuilder)
+
+private val MaterialIconDimension = 24.dp
+private const val MaterialIconViewPointDimension = 960f
diff --git a/wear/compose/compose-material3/src/main/res/drawable/wear_m3c_open_on_phone_animation.xml b/wear/compose/compose-material3/src/main/res/drawable/wear_m3c_open_on_phone_animation.xml
index 26c7f42..d9f95e8 100644
--- a/wear/compose/compose-material3/src/main/res/drawable/wear_m3c_open_on_phone_animation.xml
+++ b/wear/compose/compose-material3/src/main/res/drawable/wear_m3c_open_on_phone_animation.xml
@@ -14,4 +14,4 @@
limitations under the License.
-->
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="800dp" android:width="800dp" android:viewportHeight="800" android:viewportWidth="800"><group android:name="_R_G"><group android:name="_R_G_L_2_G" android:translateX="372" android:translateY="396" android:scaleX="9.86" android:scaleY="9.86"><path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-7.44 23.29 C-8.45,23.29 -9.32,22.93 -10.04,22.21 C-10.76,21.49 -11.13,20.62 -11.13,19.61 C-11.13,19.61 -11.13,8.18 -11.13,8.18 C-11.13,7.71 -10.96,7.31 -10.64,6.99 C-10.31,6.66 -9.91,6.5 -9.45,6.5 C-8.98,6.5 -8.6,6.66 -8.31,6.99 C-7.98,7.31 -7.82,7.71 -7.82,8.18 C-7.82,8.18 -7.82,14.46 -7.82,14.46 C-7.82,14.46 14.82,14.46 14.82,14.46 C14.82,14.46 14.82,-14.46 14.82,-14.46 C14.82,-14.46 -7.82,-14.46 -7.82,-14.46 C-7.82,-14.46 -7.82,-8.18 -7.82,-8.18 C-7.82,-7.71 -7.98,-7.31 -8.31,-6.99 C-8.6,-6.66 -8.98,-6.5 -9.45,-6.5 C-9.91,-6.5 -10.31,-6.66 -10.64,-6.99 C-10.96,-7.31 -11.13,-7.71 -11.13,-8.18 C-11.13,-8.18 -11.13,-19.61 -11.13,-19.61 C-11.13,-20.62 -10.76,-21.49 -10.04,-22.21 C-9.32,-22.93 -8.45,-23.29 -7.44,-23.29 C-7.44,-23.29 14.44,-23.29 14.44,-23.29 C15.45,-23.29 16.32,-22.93 17.04,-22.21 C17.76,-21.49 18.12,-20.62 18.12,-19.61 C18.12,-19.61 18.12,19.61 18.12,19.61 C18.12,20.62 17.76,21.49 17.04,22.21 C16.32,22.93 15.45,23.29 14.44,23.29 C14.44,23.29 -7.44,23.29 -7.44,23.29c M-7.82 17.77 C-7.82,17.77 -7.82,19.61 -7.82,19.61 C-7.82,19.72 -7.78,19.81 -7.71,19.88 C-7.64,19.95 -7.55,19.99 -7.44,19.99 C-7.44,19.99 14.44,19.99 14.44,19.99 C14.55,19.99 14.64,19.95 14.71,19.88 C14.78,19.81 14.82,19.72 14.82,19.61 C14.82,19.61 14.82,17.77 14.82,17.77 C14.82,17.77 -7.82,17.77 -7.82,17.77c M-7.82 -17.77 C-7.82,-17.77 14.82,-17.77 14.82,-17.77 C14.82,-17.77 14.82,-19.61 14.82,-19.61 C14.82,-19.72 14.78,-19.81 14.71,-19.88 C14.64,-19.95 14.55,-19.99 14.44,-19.99 C14.44,-19.99 -7.44,-19.99 -7.44,-19.99 C-7.55,-19.99 -7.64,-19.95 -7.71,-19.88 C-7.78,-19.81 -7.82,-19.72 -7.82,-19.61 C-7.82,-19.61 -7.82,-17.77 -7.82,-17.77c "/></group><group android:name="_R_G_L_1_G" android:translateX="370" android:translateY="428" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#003352" android:fillAlpha="1" android:fillType="nonZero" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M-189 -33 C-189,-33 20,-33 20,-33 "/><path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="30" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M-189 -33 C-189,-33 20,-33 20,-33 "/></group><group android:name="_R_G_L_0_G_T_1" android:translateX="180" android:translateY="395" android:scaleY="0"><group android:name="_R_G_L_0_G" android:translateX="-20" android:translateY="33"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="32" android:strokeAlpha="1" android:trimPathStart="0.49" android:trimPathEnd="0.51" android:trimPathOffset="0" android:pathData=" M-36 -90 C-36,-90 21,-33 21,-33 C21,-33 -36,24 -36,24 "/></group></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="217" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="217" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateXY" android:duration="67" android:startOffset="0" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 370,428C 370,428 370,428 370,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="217" android:startOffset="67" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 370,428C 370,428 412,428 412,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="200" android:startOffset="283" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 412,428C 412,428 400,428 400,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.44,0 0.55,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="133" android:startOffset="0" android:valueFrom="0.49" android:valueTo="0.49" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathStart" android:duration="100" android:startOffset="133" android:valueFrom="0.49" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="133" android:startOffset="0" android:valueFrom="0.51" android:valueTo="0.51" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="100" android:startOffset="133" android:valueFrom="0.51" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_T_1"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateXY" android:duration="67" android:startOffset="0" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 180,395C 180,395 180,395 180,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="217" android:startOffset="67" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 180,395C 180,395 432,395 432,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="200" android:startOffset="283" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 432,395C 432,395 420,395 420,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.44,0 0.55,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_T_1"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="10017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="800dp" android:width="800dp" android:viewportHeight="800" android:viewportWidth="800"><group android:name="_R_G"><group android:name="_R_G_L_2_G" android:translateX="372" android:translateY="396" android:scaleX="9.86" android:scaleY="9.86"><path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-7.44 23.29 C-8.45,23.29 -9.32,22.93 -10.04,22.21 C-10.76,21.49 -11.13,20.62 -11.13,19.61 C-11.13,19.61 -11.13,8.18 -11.13,8.18 C-11.13,7.71 -10.96,7.31 -10.64,6.99 C-10.31,6.66 -9.91,6.5 -9.45,6.5 C-8.98,6.5 -8.6,6.66 -8.31,6.99 C-7.98,7.31 -7.82,7.71 -7.82,8.18 C-7.82,8.18 -7.82,14.46 -7.82,14.46 C-7.82,14.46 14.82,14.46 14.82,14.46 C14.82,14.46 14.82,-14.46 14.82,-14.46 C14.82,-14.46 -7.82,-14.46 -7.82,-14.46 C-7.82,-14.46 -7.82,-8.18 -7.82,-8.18 C-7.82,-7.71 -7.98,-7.31 -8.31,-6.99 C-8.6,-6.66 -8.98,-6.5 -9.45,-6.5 C-9.91,-6.5 -10.31,-6.66 -10.64,-6.99 C-10.96,-7.31 -11.13,-7.71 -11.13,-8.18 C-11.13,-8.18 -11.13,-19.61 -11.13,-19.61 C-11.13,-20.62 -10.76,-21.49 -10.04,-22.21 C-9.32,-22.93 -8.45,-23.29 -7.44,-23.29 C-7.44,-23.29 14.44,-23.29 14.44,-23.29 C15.45,-23.29 16.32,-22.93 17.04,-22.21 C17.76,-21.49 18.12,-20.62 18.12,-19.61 C18.12,-19.61 18.12,19.61 18.12,19.61 C18.12,20.62 17.76,21.49 17.04,22.21 C16.32,22.93 15.45,23.29 14.44,23.29 C14.44,23.29 -7.44,23.29 -7.44,23.29c M-7.82 17.77 C-7.82,17.77 -7.82,19.61 -7.82,19.61 C-7.82,19.72 -7.78,19.81 -7.71,19.88 C-7.64,19.95 -7.55,19.99 -7.44,19.99 C-7.44,19.99 14.44,19.99 14.44,19.99 C14.55,19.99 14.64,19.95 14.71,19.88 C14.78,19.81 14.82,19.72 14.82,19.61 C14.82,19.61 14.82,17.77 14.82,17.77 C14.82,17.77 -7.82,17.77 -7.82,17.77c M-7.82 -17.77 C-7.82,-17.77 14.82,-17.77 14.82,-17.77 C14.82,-17.77 14.82,-19.61 14.82,-19.61 C14.82,-19.72 14.78,-19.81 14.71,-19.88 C14.64,-19.95 14.55,-19.99 14.44,-19.99 C14.44,-19.99 -7.44,-19.99 -7.44,-19.99 C-7.55,-19.99 -7.64,-19.95 -7.71,-19.88 C-7.78,-19.81 -7.82,-19.72 -7.82,-19.61 C-7.82,-19.61 -7.82,-17.77 -7.82,-17.77c "/></group><group android:name="_R_G_L_1_G" android:translateX="213" android:translateY="400" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-28.25 -50.87 C-32.44,-45.82 -32.25,-34.12 -24.87,-27.12 C-14.62,-17.39 -17.69,-20.94 -11.87,-15.25 C-5.87,-9.37 -5.19,-2.07 -11.87,4.5 C-19.5,12 -20.19,12.69 -25.94,18.44 C-31.69,24.19 -31.37,37 -26.37,42 C-21.37,47 -9.56,49.44 -1.75,41.25 C6,33.13 23.88,13.5 29.88,7.5 C37.5,-0.13 38,-10.75 30.13,-19.12 C22.38,-27.37 11.21,-37.59 -1.25,-49.62 C-12.37,-60.37 -23.37,-56.75 -28.25,-50.87c "/></group><group android:name="_R_G_L_0_G" android:translateX="382.403" android:translateY="400" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-169.03 -4.95 C-169.03,3.12 -175.58,9.67 -183.65,9.67 C-183.65,9.67 -188.85,9.67 -188.85,9.67 C-196.93,9.67 -203.48,3.12 -203.48,-4.95 C-203.48,-13.03 -196.93,-19.58 -188.85,-19.58 C-188.85,-19.58 -183.65,-19.58 -183.65,-19.58 C-175.58,-19.58 -169.03,-13.03 -169.03,-4.95c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="117" android:startOffset="0" android:valueFrom="M-28.25 -50.87 C-32.44,-45.82 -32.25,-34.12 -24.87,-27.12 C-14.62,-17.39 -17.69,-20.94 -11.87,-15.25 C-5.87,-9.37 -5.19,-2.07 -11.87,4.5 C-19.5,12 -20.19,12.69 -25.94,18.44 C-31.69,24.19 -31.37,37 -26.37,42 C-21.37,47 -9.56,49.44 -1.75,41.25 C6,33.13 23.88,13.5 29.88,7.5 C37.5,-0.13 38,-10.75 30.13,-19.12 C22.38,-27.37 11.21,-37.59 -1.25,-49.62 C-12.37,-60.37 -23.37,-56.75 -28.25,-50.87c " android:valueTo="M-28.25 -50.87 C-32.44,-45.82 -32.25,-34.12 -24.87,-27.12 C-14.62,-17.39 -17.69,-20.94 -11.87,-15.25 C-5.87,-9.37 -5.19,-2.07 -11.87,4.5 C-19.5,12 -20.19,12.69 -25.94,18.44 C-31.69,24.19 -31.37,37 -26.37,42 C-21.37,47 -9.56,49.44 -1.75,41.25 C6,33.13 23.88,13.5 29.88,7.5 C37.5,-0.13 38,-10.75 30.13,-19.12 C22.38,-27.37 11.21,-37.59 -1.25,-49.62 C-12.37,-60.37 -23.37,-56.75 -28.25,-50.87c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="117" android:startOffset="117" android:valueFrom="M-28.25 -50.87 C-32.44,-45.82 -32.25,-34.12 -24.87,-27.12 C-14.62,-17.39 -17.69,-20.94 -11.87,-15.25 C-5.87,-9.37 -5.19,-2.07 -11.87,4.5 C-19.5,12 -20.19,12.69 -25.94,18.44 C-31.69,24.19 -31.37,37 -26.37,42 C-21.37,47 -9.56,49.44 -1.75,41.25 C6,33.13 23.88,13.5 29.88,7.5 C37.5,-0.13 38,-10.75 30.13,-19.12 C22.38,-27.37 11.21,-37.59 -1.25,-49.62 C-12.37,-60.37 -23.37,-56.75 -28.25,-50.87c " android:valueTo="M-48.62 -71.62 C-53.69,-65.57 -52.25,-55.12 -44.87,-48.12 C-34.62,-38.39 -17.69,-20.94 -11.87,-15.25 C-5.87,-9.37 -5.19,-2.07 -11.87,4.5 C-19.5,12 -41.69,33.69 -47.44,39.44 C-53.19,45.19 -52.87,58 -47.87,63 C-42.87,68 -31.06,70.44 -23.25,62.25 C-15.5,54.13 23.88,13.5 29.88,7.5 C37.5,-0.13 38,-10.75 30.13,-19.12 C22.38,-27.37 -8.79,-58.59 -21.25,-70.62 C-32.37,-81.37 -43.5,-77.75 -48.62,-71.62c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="67" android:startOffset="0" android:valueFrom="213" android:valueTo="213" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateX" android:duration="267" android:startOffset="67" android:valueFrom="213" android:valueTo="416" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateX" android:duration="333" android:startOffset="333" android:valueFrom="416" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateY" android:duration="67" android:startOffset="0" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateY" android:duration="267" android:startOffset="67" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateY" android:duration="333" android:startOffset="333" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="117" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-169.03 -4.95 C-169.03,3.12 -175.58,9.67 -183.65,9.67 C-183.65,9.67 -188.85,9.67 -188.85,9.67 C-196.93,9.67 -203.48,3.12 -203.48,-4.95 C-203.48,-13.03 -196.93,-19.58 -188.85,-19.58 C-188.85,-19.58 -183.65,-19.58 -183.65,-19.58 C-175.58,-19.58 -169.03,-13.03 -169.03,-4.95c " android:valueTo="M-169.03 -4.95 C-169.03,3.12 -175.58,9.67 -183.65,9.67 C-183.65,9.67 -188.85,9.67 -188.85,9.67 C-196.93,9.67 -203.48,3.12 -203.48,-4.95 C-203.48,-13.03 -196.93,-19.58 -188.85,-19.58 C-188.85,-19.58 -183.65,-19.58 -183.65,-19.58 C-175.58,-19.58 -169.03,-13.03 -169.03,-4.95c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-169.03 -4.95 C-169.03,3.12 -175.58,9.67 -183.65,9.67 C-183.65,9.67 -188.85,9.67 -188.85,9.67 C-196.93,9.67 -203.48,3.12 -203.48,-4.95 C-203.48,-13.03 -196.93,-19.58 -188.85,-19.58 C-188.85,-19.58 -183.65,-19.58 -183.65,-19.58 C-175.58,-19.58 -169.03,-13.03 -169.03,-4.95c " android:valueTo="M35.97 -4.95 C35.97,3.12 29.42,9.67 21.35,9.67 C21.35,9.67 -188.85,9.67 -188.85,9.67 C-196.93,9.67 -203.48,3.12 -203.48,-4.95 C-203.48,-13.03 -196.93,-19.58 -188.85,-19.58 C-188.85,-19.58 21.35,-19.58 21.35,-19.58 C29.42,-19.58 35.97,-13.03 35.97,-4.95c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="67" android:startOffset="0" android:valueFrom="382.403" android:valueTo="382.403" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateX" android:duration="267" android:startOffset="67" android:valueFrom="382.403" android:valueTo="398.403" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.3,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateX" android:duration="333" android:startOffset="333" android:valueFrom="398.403" android:valueTo="382.403" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateY" android:duration="67" android:startOffset="0" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateY" android:duration="267" android:startOffset="67" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateY" android:duration="333" android:startOffset="333" android:valueFrom="400" android:valueTo="400" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="10017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/res/values-af/strings.xml b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
index 292ca6d..05c0230 100644
--- a/wear/compose/compose-material3/src/main/res/values-af/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestig"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Verminder"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Vermeerder"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Het misluk"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sukses"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Maak op foon oop"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-am/strings.xml b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
index 1f165fd..e9ff4a7 100644
--- a/wear/compose/compose-material3/src/main/res/values-am/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ዓመት"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"አረጋግጥ"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ቀጣይ"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"ቀንስ"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"ጨምር"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"አልተሳካም"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ተሳክቷል"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ስልክ ላይ ክፈት"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
index b39dc3c6..53273dd 100644
--- a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
@@ -50,10 +50,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"السنة"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأكيد"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"التالي"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"تقليل"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"زيادة"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"تعذر الإجراء"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"نجحَ الإجراء"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"فتح على الهاتف"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
index 88e265dc..4e4afdb 100644
--- a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Smanji"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Povećaj"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspelo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspelo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-be/strings.xml b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
index dd3df95..4e4b539 100644
--- a/wear/compose/compose-material3/src/main/res/values-be/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Пацвердзіць"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далей"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Паменшыць"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Павялічыць"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Памылка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Выканана"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На тэлефоне"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
index df9c2d1..c4df263 100644
--- a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потвърждаване"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Напред"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Намаляване"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Увеличаване"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Неуспешно"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успешно"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Отв. на тел."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
index 82bc2f2..d963380 100644
--- a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrđivanje"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprijed"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Smanjivanje"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Povećavanje"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neuspješno"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspješno"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvor. na tel."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
index 1a58fc7fa..9f2ef3e 100644
--- a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Any"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Següent"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Redueix"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Augmenta"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ha fallat"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Correcte"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Obre al telèfon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
index 63526fc..2265857 100644
--- a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdit"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Další"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Snížit"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Zvýšit"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nezdařilo se"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Hotovo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otevřít v telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-da/strings.xml b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
index fc69382..874dff6 100644
--- a/wear/compose/compose-material3/src/main/res/values-da/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekræft"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Næste"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Dæmp"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Øg"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislykket"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Gennemført"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åbn på telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-el/strings.xml b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
index 625ca9a..9f594ae 100644
--- a/wear/compose/compose-material3/src/main/res/values-el/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Έτος"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Επιβεβαίωση"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Επόμενο"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Μείωση"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Αύξηση"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Αποτυχία"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Επιτυχία"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Στο τηλέφωνο"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
index 36aabdf..bdc4036 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Decrease"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Increase"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
index 36aabdf..bdc4036 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Decrease"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Increase"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
index 36aabdf..bdc4036 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Decrease"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Increase"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-es/strings.xml b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
index ec62941..1502faf 100644
--- a/wear/compose/compose-material3/src/main/res/values-es/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Reducir"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Aumentar"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Todo correcto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ábrelo en el teléfono"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
index 4ed7b8a..e92b365 100644
--- a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأیید کردن"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"بعدی"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"کاهش دادن"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"افزایش دادن"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"انجام نشد"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"انجام شد"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"باز کردن در تلفن"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
index 04460eb..6759232 100644
--- a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Vuosi"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Vahvista"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seuraava"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Vähennä"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Lisää"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Epäonnistui"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Onnistui"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Puhelimella"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
index b3e47a1..a1fbf8e 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuer"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Augmenter"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Réussite"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouv. ds tél."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
index df054a0..7af8ee6 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuer"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Augmenter"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Opération réussie"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouvrir sur le téléphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
index f36d824..d2e3e8e 100644
--- a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Smanji"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Povećaj"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspjelo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspjeh"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
index fd95148..9fb9621 100644
--- a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Év"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Megerősítés"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Következő"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Csökkentés"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Növelés"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Sikertelen"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sikerült"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Nyissa meg mobilon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
index 7b98efb..453d5f5 100644
--- a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Տարի"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Հաստատել"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Հաջորդը"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Նվազեցնել"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Ավելացնել"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ձախողվել է"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Պատրաստ է"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Բացեք հեռախոսում"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-in/strings.xml b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
index b41351a..f4f1f2c 100644
--- a/wear/compose/compose-material3/src/main/res/values-in/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmasi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Berikutnya"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Turunkan"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Tingkatkan"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Gagal"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Berhasil"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buka di ponsel"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-it/strings.xml b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
index 3a0f0b5..5b48a38 100644
--- a/wear/compose/compose-material3/src/main/res/values-it/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Anno"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Conferma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avanti"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuisci"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Aumenta"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Non riuscita"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Riuscita"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Su smartph."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
index d0a9318..239c255 100644
--- a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"שנה"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"אישור"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"הבא"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"הפחתה"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"הגברה"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"הפעולה נכשלה"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"הפעולה הצליחה"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"פתיחה בטלפון"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
index 1021184..8cbdd56 100644
--- a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"次へ"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"下げる"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"上げる"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"スマホで開く"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
index ac02463..ed13a53 100644
--- a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"წელი"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"დადასტურება"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"შემდეგი"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"შემცირება"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"გაზრდა"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ვერ შესრულდა"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"შესრულდა"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ტელეფონში გახსნა"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
index 05e8014..4ada781 100644
--- a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Растау"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Келесі"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Азайту"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Көбейту"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Расталмады."</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Расталды."</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефоннан ашыңыз."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
index 1643534..1710e88 100644
--- a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"년"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"확인"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"다음"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"감소"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"증가"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"실패"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"성공"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"휴대전화에서 열기"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
index c312a62..3a9abba 100644
--- a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Ырастоо"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Кийинки"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Төмөндөтүү"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Жогорулатуу"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ишке ашпады"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Ийгилик"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефондо ачуу"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
index 8397c2a..474becb 100644
--- a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Metai"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Patvirtinti"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Kitas"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Sumažinti"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Padidinti"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nepavyko"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pavyko"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atidaryti telefone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
index c440c8b..fc3777f 100644
--- a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Gads"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Apstiprināt"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tālāk"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Samazināt"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Palielināt"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neizdevās"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Izdevās"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atvērt tālrunī"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-my/strings.xml b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
index c0393c2..1c2fc6e1 100644
--- a/wear/compose/compose-material3/src/main/res/values-my/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"နှစ်"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"အတည်ပြုရန်"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ရှေ့သို့"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"လျှော့ရန်"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"တိုးရန်"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"မအောင်မြင်ပါ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"အောင်မြင်သည်"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ဖုန်း၌ဖွင့်ရန်"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
index d827ac5..6e3d0e6 100644
--- a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekreft"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Neste"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Reduser"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Øk"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislyktes"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Vellykket"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åpne på tlf."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
index d89b6e9..a72911c 100644
--- a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestigen"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Verlagen"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Verhogen"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislukt"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Geslaagd"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Openen op telefoon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
index 5484b3e..b958497 100644
--- a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potwierdź"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalej"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Zmniejsz"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Zwiększ"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Niepowodzenie"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Udało się"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otwórz na telefonie"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
index 12c2766..41b3ee7 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuir"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Aumentar"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
index c4d5152..3370f8b 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuir"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Aumentar"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falhou"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Concluído"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
index 12c2766..41b3ee7 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Diminuir"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Aumentar"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
index 03c63d69..10fbc69 100644
--- a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"An"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmă"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Înainte"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Redu"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Crește"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Eroare"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Succes"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Pe telefon"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
index 84d7feb..2cffbc8 100644
--- a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Подтвердить"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далее"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Уменьшить"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Увеличить"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ошибка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Открыть на телефоне"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
index 0b391a8..4922721 100644
--- a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdiť"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Ďalej"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Znížiť"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Zvýšiť"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neúspešné"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Podarilo sa"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvorte v telefóne"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
index c5bdeb3..07713ae 100644
--- a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
@@ -41,10 +41,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Даље"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Смањи"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Повећај"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Није успело"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успело"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефону"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
index 8e24d46..a4c4e6a 100644
--- a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekräfta"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Nästa"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Minska"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Öka"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Misslyckades"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Klart"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"På telefonen"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
index c2ad8cb..942c959 100644
--- a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Mwaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Thibitisha"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Endelea"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Punguza"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Ongeza"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Imeshindwa"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Imemaliza"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Fungua kwenye simu"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
index 4f93096..fd278fc 100644
--- a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ஆண்டு"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"உறுதிசெய்யும்"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"அடுத்ததற்குச் செல்லும்"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"குறைக்கும்"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"அதிகரிக்கும்"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"தோல்வி"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"முடிந்தது"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"மொபைலில் திற"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-th/strings.xml b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
index d4825e4..8576aa4 100644
--- a/wear/compose/compose-material3/src/main/res/values-th/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ปี"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ยืนยัน"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ถัดไป"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"ลด"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"เพิ่ม"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ไม่สำเร็จ"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"สำเร็จ"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"เปิดในโทรศัพท์"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
index 186c0ed..f8cf9bf7a 100644
--- a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Taon"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kumpirmahin"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Susunod"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Bawasan"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Dagdagan"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nabigo"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Matagumpay"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buksan"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
index c978fe0..a8a32e6 100644
--- a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yıl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Onayla"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Sonraki"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Azalt"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Artır"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Başarısız"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Başarılı"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda aç"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
index 3fa6994..0c45993 100644
--- a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
@@ -44,10 +44,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Рік"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Підтвердити"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далі"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Зменшити"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Збільшити"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Помилка"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефоні"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
index d99a721..2d2f2d3 100644
--- a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yil"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Tasdiqlash"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Keyingisi"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Pasaytirish"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Oshirish"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Bajarilmadi"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Bajarildi"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
index 65f5eb1..093ccf7 100644
--- a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Năm"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Xác nhận"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tiếp theo"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Giảm"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Tăng"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Lỗi"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Thành công"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Mở trên điện thoại"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
index c395e20..81d4b75 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"确认"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一个"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"降低"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"增加"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失败"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手机上打开"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
index 7306379..64d4bdb 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一步"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"調低"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"調高"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機開啟"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
index dfe78d7..ef57e4c 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一個"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"調低"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"調高"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機上開啟"</string>
diff --git a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
index 259ef34..c14db45 100644
--- a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
@@ -38,10 +38,8 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Unyaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Qinisekisa"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Okulandelayo"</string>
- <!-- no translation found for wear_m3c_slider_decrease_content_description (8242572466064289486) -->
- <skip />
- <!-- no translation found for wear_m3c_slider_increase_content_description (3329631766954416834) -->
- <skip />
+ <string name="wear_m3c_slider_decrease_content_description" msgid="8242572466064289486">"Yehlisa"</string>
+ <string name="wear_m3c_slider_increase_content_description" msgid="3329631766954416834">"Khulisa"</string>
<string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Yehlulekile"</string>
<string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Impumelelo"</string>
<string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Vula efonini"</string>
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
index fc26ae4..7d53524 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
@@ -90,11 +90,11 @@
val coroutineScope = rememberCoroutineScope()
val deleteItem: () -> Unit = {
coroutineScope.launch {
- revealState.animateTo(RevealValue.Revealed)
+ revealState.animateTo(RevealValue.RightRevealed)
// hide the content after some time if the state is still revealed
delay(1500)
- if (revealState.currentValue == RevealValue.Revealed) {
+ if (revealState.currentValue == RevealValue.RightRevealed) {
// Undo should no longer be triggered
undoActionEnabled = false
currentState.expanded = false
@@ -103,12 +103,12 @@
}
val addItem: () -> Unit = {
coroutineScope.launch {
- revealState.animateTo(RevealValue.Revealed)
+ revealState.animateTo(RevealValue.RightRevealed)
itemCount++
// reset the state after some delay if the state is still revealed
delay(2000)
- if (revealState.currentValue == RevealValue.Revealed) {
+ if (revealState.currentValue == RevealValue.RightRevealed) {
revealState.animateTo(RevealValue.Covered)
}
}
@@ -268,7 +268,7 @@
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
LaunchedEffect(revealState.currentValue) {
- if (revealState.currentValue == RevealValue.Revealed) {
+ if (revealState.currentValue == RevealValue.RightRevealed) {
delay(2000)
expandableState.expanded = false
}
@@ -290,7 +290,9 @@
customActions =
listOf(
CustomAccessibilityAction("Delete") {
- coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) }
+ coroutineScope.launch {
+ revealState.animateTo(RevealValue.RightRevealed)
+ }
true
},
CustomAccessibilityAction("More Options") {
@@ -300,13 +302,17 @@
)
},
revealState = revealState,
- onFullSwipe = { coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) } },
+ onFullSwipe = {
+ coroutineScope.launch { revealState.animateTo(RevealValue.RightRevealed) }
+ },
primaryAction = {
SwipeToRevealPrimaryAction(
revealState = revealState,
icon = { Icon(SwipeToRevealDefaults.Delete, contentDescription = "Delete") },
label = { Text(text = "Delete") },
- onClick = { coroutineScope.launch { revealState.animateTo(RevealValue.Revealed) } }
+ onClick = {
+ coroutineScope.launch { revealState.animateTo(RevealValue.RightRevealed) }
+ }
)
},
secondaryAction = {