Merge "Fix split-screen tests in API 34" into androidx-main
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index d972613..4fef3f4 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -32,6 +32,7 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
@@ -3323,6 +3324,18 @@
}
@Test
+ public void testRemoveByQuery_withJoinSpec_throwsException() {
+ Exception e = assertThrows(IllegalArgumentException.class,
+ () -> mAppSearchImpl.removeByQuery("", "", "",
+ new SearchSpec.Builder()
+ .setJoinSpec(new JoinSpec.Builder("childProp").build())
+ .build(),
+ null));
+ assertThat(e.getMessage()).isEqualTo(
+ "JoinSpec not allowed in removeByQuery, but JoinSpec was provided");
+ }
+
+ @Test
public void testLimitConfig_Replace() throws Exception {
// Create a new mAppSearchImpl with a lower limit
mAppSearchImpl.close();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2307cde..f445af7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -43,6 +43,7 @@
import androidx.appsearch.app.GetByDocumentIdRequest;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchSpec;
@@ -1825,18 +1826,26 @@
*
* <p>This method belongs to mutate group.
*
+ * <p> {@link SearchSpec} objects containing a {@link JoinSpec} are not allowed here.
+ *
* @param packageName The package name that owns the documents.
* @param databaseName The databaseName the document is in.
* @param queryExpression Query String to search.
* @param searchSpec Defines what and how to remove
* @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
* @throws AppSearchException on IcingSearchEngine error.
+ * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
*/
public void removeByQuery(@NonNull String packageName, @NonNull String databaseName,
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec,
@Nullable RemoveStats.Builder removeStatsBuilder)
throws AppSearchException {
+ if (searchSpec.getJoinSpec() != null) {
+ throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+ + "JoinSpec was provided");
+ }
+
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
mReadWriteLock.writeLock().lock();
try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index f66b650..36644b4 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -508,6 +508,12 @@
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpec);
+
+ if (searchSpec.getJoinSpec() != null) {
+ throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+ + "JoinSpec was provided.");
+ }
+
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
ListenableFuture<Void> future = execute(() -> {
RemoveStats.Builder removeStatsBuilder = null;
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 76e6450..a4621c2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -204,6 +204,11 @@
Preconditions.checkNotNull(searchSpec);
ResolvableFuture<Void> future = ResolvableFuture.create();
+ if (searchSpec.getJoinSpec() != null) {
+ throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+ + "JoinSpec was provided.");
+ }
+
if (!BuildCompat.isAtLeastT() && !searchSpec.getFilterNamespaces().isEmpty()) {
// This is a patch for b/197361770, framework-appsearch in Android S will
// disable the given namespace filter if it is not empty and none of given namespaces
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index bac049f..fb31d2e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -3220,6 +3220,19 @@
}
@Test
+ public void testRemoveQueryWithJoinSpecThrowsException() {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> mDb2.removeAsync("",
+ new SearchSpec.Builder()
+ .setJoinSpec(new JoinSpec.Builder("entityId").build())
+ .build()));
+ assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, "
+ + "but JoinSpec was provided.");
+ }
+
+ @Test
public void testCloseAndReopen() throws Exception {
// Schema registration
mDb1.setSchemaAsync(
@@ -3483,20 +3496,20 @@
public void testIndexNestedDocuments() throws Exception {
// Schema registration
mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
- .addSchemas(AppSearchEmail.SCHEMA)
- .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
- .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
- "prop", AppSearchEmail.SCHEMA_TYPE)
- .setShouldIndexNestedProperties(true)
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "prop", AppSearchEmail.SCHEMA_TYPE)
+ .setShouldIndexNestedProperties(true)
+ .build())
+ .build())
+ .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+ "prop", AppSearchEmail.SCHEMA_TYPE)
+ .setShouldIndexNestedProperties(false)
+ .build())
.build())
.build())
- .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
- .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
- "prop", AppSearchEmail.SCHEMA_TYPE)
- .setShouldIndexNestedProperties(false)
- .build())
- .build())
- .build())
.get();
// Index the documents.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 06a0d6b..16098ebc 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -312,6 +312,9 @@
* indicates how document will be removed. All specific about how to
* scoring, ordering, snippeting and resulting will be ignored.
* @return The pending result of performing this operation.
+ * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
+ * {@link JoinSpec} lets you join docs that are not owned by the caller, so the semantics of
+ * failures from this method would be complex.
*/
@NonNull
ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
diff --git a/arch/core/core-common/api/2.2.0-beta01.txt b/arch/core/core-common/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+ public interface Function<I, O> {
+ method public O! apply(I!);
+ }
+
+}
+
diff --git a/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+ public interface Function<I, O> {
+ method public O! apply(I!);
+ }
+
+}
+
diff --git a/arch/core/core-common/api/restricted_2.2.0-beta01.txt b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..4fbc435
--- /dev/null
+++ b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,41 @@
+// Signature format: 4.0
+package androidx.arch.core.internal {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap<K, V> extends androidx.arch.core.internal.SafeIterableMap<K,V> {
+ ctor public FastSafeIterableMap();
+ method public java.util.Map.Entry<K!,V!>? ceil(K!);
+ method public boolean contains(K!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap<K, V> implements java.lang.Iterable<java.util.Map.Entry<K,V>> {
+ ctor public SafeIterableMap();
+ method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> descendingIterator();
+ method public java.util.Map.Entry<K!,V!>? eldest();
+ method protected androidx.arch.core.internal.SafeIterableMap.Entry<K!,V!>? get(K!);
+ method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> iterator();
+ method public androidx.arch.core.internal.SafeIterableMap.IteratorWithAdditions iteratorWithAdditions();
+ method public java.util.Map.Entry<K!,V!>? newest();
+ method public V! putIfAbsent(K, V);
+ method public V! remove(K);
+ method public int size();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+ method public boolean hasNext();
+ method public java.util.Map.Entry<K!,V!>! next();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove<K, V> {
+ ctor public SafeIterableMap.SupportRemove();
+ }
+
+}
+
+package androidx.arch.core.util {
+
+ public interface Function<I, O> {
+ method public O! apply(I!);
+ }
+
+}
+
diff --git a/arch/core/core-runtime/api/2.2.0-beta01.txt b/arch/core/core-runtime/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/res-2.2.0-beta01.txt b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..9958884
--- /dev/null
+++ b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ArchTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+ method public void executeOnDiskIO(Runnable);
+ method public static java.util.concurrent.Executor getIOThreadExecutor();
+ method public static androidx.arch.core.executor.ArchTaskExecutor getInstance();
+ method public static java.util.concurrent.Executor getMainThreadExecutor();
+ method public boolean isMainThread();
+ method public void postToMainThread(Runnable);
+ method public void setDelegate(androidx.arch.core.executor.TaskExecutor?);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DefaultTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+ ctor public DefaultTaskExecutor();
+ method public void executeOnDiskIO(Runnable);
+ method public boolean isMainThread();
+ method public void postToMainThread(Runnable);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class TaskExecutor {
+ ctor public TaskExecutor();
+ method public abstract void executeOnDiskIO(Runnable);
+ method public void executeOnMainThread(Runnable);
+ method public abstract boolean isMainThread();
+ method public abstract void postToMainThread(Runnable);
+ }
+
+}
+
diff --git a/arch/core/core-testing/api/2.2.0-beta01.txt b/arch/core/core-testing/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+ public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public CountingTaskExecutorRule();
+ method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+ method public boolean isIdle();
+ method protected void onIdle();
+ }
+
+ public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public InstantTaskExecutorRule();
+ }
+
+}
+
diff --git a/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+ public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public CountingTaskExecutorRule();
+ method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+ method public boolean isIdle();
+ method protected void onIdle();
+ }
+
+ public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public InstantTaskExecutorRule();
+ }
+
+}
+
diff --git a/arch/core/core-testing/api/res-2.2.0-beta01.txt b/arch/core/core-testing/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/arch/core/core-testing/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-testing/api/restricted_2.2.0-beta01.txt b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..8113a1d
--- /dev/null
+++ b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class JunitTaskExecutorRule implements org.junit.rules.TestRule {
+ ctor public JunitTaskExecutorRule(int, boolean);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement, org.junit.runner.Description);
+ method public void drainTasks(int) throws java.lang.InterruptedException;
+ method public androidx.arch.core.executor.TaskExecutor getTaskExecutor();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class TaskExecutorWithFakeMainThread extends androidx.arch.core.executor.TaskExecutor {
+ ctor public TaskExecutorWithFakeMainThread(int);
+ method public void drainTasks(int) throws java.lang.InterruptedException;
+ method public void executeOnDiskIO(Runnable);
+ method public boolean isMainThread();
+ method public void postToMainThread(Runnable);
+ }
+
+}
+
+package androidx.arch.core.executor.testing {
+
+ public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public CountingTaskExecutorRule();
+ method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+ method public boolean isIdle();
+ method protected void onIdle();
+ }
+
+ public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public InstantTaskExecutorRule();
+ }
+
+}
+
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
index b8d9b49..12ffb92 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
@@ -171,10 +171,8 @@
private val states = mutableListOf<CameraState>()
private var index = 0
- override fun onChanged(state: CameraState?) {
- if (state != null) {
- states.add(state)
- }
+ override fun onChanged(value: CameraState) {
+ states.add(value)
}
fun assertHasState(expectedState: CameraState): StateObserver {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index fddf7c4..46e04fa 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -299,8 +299,8 @@
val cameraClosedLatch = CountDownLatch(1)
withContext(Dispatchers.Main) {
camera.cameraInfo.cameraState.observeForever(object : Observer<CameraState?> {
- override fun onChanged(cameraState: CameraState?) {
- if (cameraState?.type == CameraState.Type.CLOSED) {
+ override fun onChanged(value: CameraState?) {
+ if (value?.type == CameraState.Type.CLOSED) {
cameraClosedLatch.countDown()
camera.cameraInfo.cameraState.removeObserver(this)
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index 2f630d2..d8201f8 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -16,6 +16,13 @@
package androidx.camera.testing.fakes;
+import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
+import static com.google.common.primitives.Ints.asList;
+
import android.util.Size;
import androidx.annotation.NonNull;
@@ -26,9 +33,12 @@
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/** A CameraDeviceSurfaceManager which has no supported SurfaceConfigs. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -39,6 +49,8 @@
private final Map<String, Map<Class<? extends UseCaseConfig<?>>, Size>> mDefinedResolutions =
new HashMap<>();
+ private final Set<List<Integer>> mValidSurfaceCombos = createDefaultValidSurfaceCombos();
+
/**
* Sets the given suggested resolutions for the specified camera Id and use case type.
*/
@@ -77,6 +89,7 @@
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
+ checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
Map<UseCaseConfig<?>, Size> suggestedSizes = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
Size resolution = MAX_OUTPUT_SIZE;
@@ -94,4 +107,54 @@
return suggestedSizes;
}
+
+ /**
+ * Checks if the surface combinations is supported.
+ *
+ * <p> Throws {@link IllegalArgumentException} if not supported.
+ */
+ private void checkSurfaceCombo(List<AttachedSurfaceInfo> existingSurfaceInfos,
+ @NonNull List<UseCaseConfig<?>> newSurfaceConfigs) {
+ // Combine existing Surface with new Surface
+ List<Integer> currentCombo = new ArrayList<>();
+ for (UseCaseConfig<?> useCaseConfig : newSurfaceConfigs) {
+ currentCombo.add(useCaseConfig.getInputFormat());
+ }
+ for (AttachedSurfaceInfo surfaceInfo : existingSurfaceInfos) {
+ currentCombo.add(surfaceInfo.getImageFormat());
+ }
+ // Loop through valid combinations and return early if the combo is supported.
+ for (List<Integer> validCombo : mValidSurfaceCombos) {
+ if (isComboSupported(currentCombo, validCombo)) {
+ return;
+ }
+ }
+ // Throw IAE if none of the valid combos supports the current combo.
+ throw new IllegalArgumentException("Surface combo not supported");
+ }
+
+ /**
+ * Checks if the app combination in covered by the given valid combination.
+ */
+ private boolean isComboSupported(@NonNull List<Integer> appCombo,
+ @NonNull List<Integer> validCombo) {
+ List<Integer> combo = new ArrayList<>(validCombo);
+ for (Integer format : appCombo) {
+ if (!combo.remove(format)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * The default combination is similar to LEGACY level devices.
+ */
+ private static Set<List<Integer>> createDefaultValidSurfaceCombos() {
+ Set<List<Integer>> validCombos = new HashSet<>();
+ validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, YUV_420_888, JPEG));
+ validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+ return validCombos;
+ }
}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 07528fb..c6b7329 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -16,13 +16,24 @@
package androidx.camera.testing.fakes;
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.SurfaceConfig.ConfigSize.PREVIEW;
+import static androidx.camera.core.impl.SurfaceConfig.ConfigType.YUV;
+
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
import android.os.Build;
+import android.util.Range;
import android.util.Size;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
import org.junit.Before;
@@ -36,6 +47,9 @@
import java.util.List;
import java.util.Map;
+/**
+ * Unit tests for {@link FakeCameraDeviceSurfaceManager}.
+ */
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -59,14 +73,44 @@
@Before
public void setUp() {
mFakeCameraDeviceSurfaceManager = new FakeCameraDeviceSurfaceManager();
- mFakeUseCaseConfig = mock(FakeUseCaseConfig.class);
+ mFakeUseCaseConfig = new FakeUseCaseConfig.Builder().getUseCaseConfig();
mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID0,
mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID1,
mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
- mUseCaseConfigList = Collections.singletonList((UseCaseConfig<?>) mFakeUseCaseConfig);
+ mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
+ }
+
+ @Test
+ public void validSurfaceCombination_noException() {
+ UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+ UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+ mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ emptyList(), asList(preview, analysis));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void invalidSurfaceAndConfigCombination_throwException() {
+ UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+ UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+ AttachedSurfaceInfo analysis = AttachedSurfaceInfo.create(
+ SurfaceConfig.create(YUV, PREVIEW),
+ YUV_420_888,
+ new Size(1, 1),
+ new Range<>(30, 30));
+ mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ singletonList(analysis), asList(preview, video));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void invalidConfigCombination_throwException() {
+ UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+ UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+ UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+ mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ Collections.emptyList(), asList(preview, video, analysis));
}
@Test
@@ -82,7 +126,6 @@
new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
assertThat(suggestedSizesCamera1.get(mFakeUseCaseConfig)).isEqualTo(
new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
-
}
}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
index 819e0e1..ca96202 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.newtext.text.modifiers.SelectableTextAnnotatedStringElement
import androidx.compose.foundation.newtext.text.modifiers.TextAnnotatedStringElement
import androidx.compose.foundation.newtext.text.modifiers.SelectionController
+import androidx.compose.foundation.newtext.text.modifiers.TextStringSimpleElement
import androidx.compose.foundation.newtext.text.modifiers.validateMinMaxLines
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.runtime.Composable
@@ -52,13 +53,14 @@
/**
* Rewrite of BasicText
*/
+@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalTextApi
@Composable
fun TextUsingModifier(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
- onTextLayout: (TextLayoutResult) -> Unit = {},
+ onTextLayout: ((TextLayoutResult) -> Unit)? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
@@ -77,8 +79,8 @@
} else {
null
}
- Layout(
- modifier = modifier.textModifier(
+ val finalModifier = if (selectionController != null || onTextLayout != null) {
+ modifier.textModifier(
AnnotatedString(text),
style = style,
onTextLayout = onTextLayout,
@@ -90,9 +92,19 @@
placeholders = null,
onPlaceholderLayout = null,
selectionController = selectionController
- ),
- EmptyMeasurePolicy
- )
+ )
+ } else {
+ modifier then TextStringSimpleElement(
+ text,
+ style,
+ LocalFontFamilyResolver.current,
+ overflow,
+ softWrap,
+ maxLines,
+ minLines
+ )
+ }
+ Layout(finalModifier, EmptyMeasurePolicy)
}
/**
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
new file mode 100644
index 0000000..45330d0
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.foundation.newtext.text.ceilToIntPx
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.MultiParagraphIntrinsics
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.ParagraphIntrinsics
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrain
+
+internal class ParagraphLayoutCache(
+ private var text: String,
+ private var style: TextStyle,
+ private var fontFamilyResolver: FontFamily.Resolver,
+ private var overflow: TextOverflow = TextOverflow.Clip,
+ private var softWrap: Boolean = true,
+ private var maxLines: Int = Int.MAX_VALUE,
+ private var minLines: Int = DefaultMinLines,
+) {
+ internal var density: Density? = null
+ set(value) {
+ val localField = field
+ if (localField == null) {
+ field = value
+ return
+ }
+
+ if (value == null) {
+ field = value
+ markDirty()
+ return
+ }
+
+ if (localField.density != value.density || localField.fontScale != value.fontScale) {
+ field = value
+ // none of our results are correct if density changed
+ markDirty()
+ }
+ }
+ internal val observeFontChanges: Unit
+ get() {
+ paragraphIntrinsics?.hasStaleResolvedFonts
+ }
+
+ internal var paragraph: Paragraph? = null
+ internal var didOverflow: Boolean = false
+ internal var layoutSize: IntSize = IntSize(0, 0)
+
+ private var minMaxLinesCoercer: MinMaxLinesCoercer? = null
+ private var paragraphIntrinsics: ParagraphIntrinsics? = null
+
+ private var intrinsicsLayoutDirection: LayoutDirection? = null
+ private var prevConstraints: Constraints = Constraints.fixed(0, 0)
+
+ private var cachedIntrinsicWidth: Int = -1
+ private var cachedIntrinsicHeight: Int = -1
+
+ private val nonNullIntrinsics: ParagraphIntrinsics
+ get() = paragraphIntrinsics ?: throw IllegalStateException(
+ "MeasureScope.measure() must be called first to query text intrinsics"
+ )
+
+ /**
+ * The width for text if all soft wrap opportunities were taken.
+ *
+ * Valid only after [layoutWithConstraints] has been called.
+ */
+ val minIntrinsicWidth: Int get() = nonNullIntrinsics.minIntrinsicWidth.ceilToIntPx()
+
+ /**
+ * The width at which increasing the width of the text no lonfger decreases the height.
+ *
+ * Valid only after [layoutWithConstraints] has been called.
+ */
+ val maxIntrinsicWidth: Int get() = nonNullIntrinsics.maxIntrinsicWidth.ceilToIntPx()
+
+ /**
+ * Update layout constraints for this text
+ *
+ * @return true if constraints caused a text layout invalidation
+ */
+ fun layoutWithConstraints(
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): Boolean {
+ val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
+ val localMinMax = MinMaxLinesCoercer.from(
+ minMaxLinesCoercer,
+ layoutDirection,
+ style,
+ density!!,
+ fontFamilyResolver
+ ).also {
+ minMaxLinesCoercer = it
+ }
+ localMinMax.coerceMaxMinLines(
+ inConstraints = constraints,
+ minLines = minLines,
+ maxLines = maxLines
+ )
+ } else {
+ constraints
+ }
+ if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+ return false
+ }
+ paragraph = layoutText(finalConstraints, layoutDirection).also {
+ prevConstraints = finalConstraints
+ val localSize = finalConstraints.constrain(
+ IntSize(
+ it.width.ceilToIntPx(),
+ it.height.ceilToIntPx()
+ )
+ )
+ layoutSize = localSize
+ didOverflow = overflow != TextOverflow.Visible &&
+ (localSize.width < it.width || localSize.height < it.height)
+ }
+ return true
+ }
+
+ fun intrinsicHeightAt(width: Int, layoutDirection: LayoutDirection): Int {
+ val localWidth = cachedIntrinsicWidth
+ val localHeght = cachedIntrinsicHeight
+ if (width == localWidth && localWidth != -1) return localHeght
+ val result = layoutText(
+ Constraints(0, width, 0, Constraints.Infinity),
+ layoutDirection
+ ).height.ceilToIntPx()
+
+ cachedIntrinsicWidth = width
+ cachedIntrinsicHeight = result
+ return result
+ }
+
+ fun update(
+ text: String,
+ style: TextStyle,
+ fontFamilyResolver: FontFamily.Resolver,
+ overflow: TextOverflow,
+ softWrap: Boolean,
+ maxLines: Int,
+ minLines: Int
+ ) {
+ this.text = text
+ this.style = style
+ this.fontFamilyResolver = fontFamilyResolver
+ this.overflow = overflow
+ this.softWrap = softWrap
+ this.maxLines = maxLines
+ this.minLines = minLines
+ markDirty()
+ }
+
+ private fun setLayoutDirection(layoutDirection: LayoutDirection) {
+ val localIntrinsics = paragraphIntrinsics
+ val intrinsics = if (
+ localIntrinsics == null ||
+ layoutDirection != intrinsicsLayoutDirection ||
+ localIntrinsics.hasStaleResolvedFonts
+ ) {
+ intrinsicsLayoutDirection = layoutDirection
+ ParagraphIntrinsics(
+ text = text,
+ style = resolveDefaults(style, layoutDirection),
+ density = density!!,
+ fontFamilyResolver = fontFamilyResolver
+ )
+ } else {
+ localIntrinsics
+ }
+ paragraphIntrinsics = intrinsics
+ }
+
+ /**
+ * Computes the visual position of the glyphs for painting the text.
+ *
+ * The text will layout with a width that's as close to its max intrinsic width as possible
+ * while still being greater than or equal to `minWidth` and less than or equal to `maxWidth`.
+ */
+ private fun layoutText(
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): Paragraph {
+ setLayoutDirection(layoutDirection)
+
+ val minWidth = constraints.minWidth
+ val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+ constraints.maxWidth
+ } else {
+ Constraints.Infinity
+ }
+
+ // This is a fallback behavior because native text layout doesn't support multiple
+ // ellipsis in one text layout.
+ // When softWrap is turned off and overflow is ellipsis, it's expected that each line
+ // that exceeds maxWidth will be ellipsized.
+ // For example,
+ // input text:
+ // "AAAA\nAAAA"
+ // maxWidth:
+ // 3 * fontSize that only allow 3 characters to be displayed each line.
+ // expected output:
+ // AA…
+ // AA…
+ // Here we assume there won't be any '\n' character when softWrap is false. And make
+ // maxLines 1 to implement the similar behavior.
+ val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
+
+ // if minWidth == maxWidth the width is fixed.
+ // therefore we can pass that value to our paragraph and use it
+ // if minWidth != maxWidth there is a range
+ // then we should check if the max intrinsic width is in this range to decide the
+ // width to be passed to Paragraph
+ // if max intrinsic width is between minWidth and maxWidth
+ // we can use it to layout
+ // else if max intrinsic width is greater than maxWidth, we can only use maxWidth
+ // else if max intrinsic width is less than minWidth, we should use minWidth
+ val width = if (minWidth == maxWidth) {
+ maxWidth
+ } else {
+ maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+ }
+
+ val finalConstraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight)
+ return Paragraph(
+ paragraphIntrinsics = nonNullIntrinsics,
+ constraints = finalConstraints,
+ // This is a fallback behavior for ellipsis. Native
+ maxLines = finalMaxLines,
+ ellipsis = overflow == TextOverflow.Ellipsis
+ )
+ }
+
+ private fun newLayoutWillBeDifferent(
+ constraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): Boolean {
+ val localParagraph = paragraph ?: return true
+ val localParagraphIntrinsics = paragraphIntrinsics ?: return true
+ // no layout yet
+
+ // async typeface changes
+ if (localParagraphIntrinsics.hasStaleResolvedFonts) return true
+
+ // layout direction changed
+ if (layoutDirection != intrinsicsLayoutDirection) return true
+
+ // if we were passed identical constraints just skip more work
+ if (constraints == prevConstraints) return false
+
+ // only be clever if we can predict line break behavior exactly, which is only possible with
+ // simple geometry math for the greedy layout case
+ if (style.lineBreak != LineBreak.Simple) {
+ return true
+ }
+
+ // see if width would produce the same wraps (greedy wraps only)
+ val canWrap = softWrap && maxLines > 1
+ if (canWrap && layoutSize.width != localParagraph.maxIntrinsicWidth.ceilToIntPx()) {
+ // some soft wrapping happened, check to see if we're between the previous measure and
+ // the next wrap
+ val prevActualMaxWidth = maxWidth(prevConstraints)
+ val newMaxWidth = maxWidth(constraints)
+ if (newMaxWidth > prevActualMaxWidth) {
+ // we've grown the potential layout area, and may break longer lines
+ return true
+ }
+ if (newMaxWidth <= layoutSize.width) {
+ // it's possible to shrink this text (possible opt: check minIntrinsicWidth
+ return true
+ }
+ }
+
+ // check any constraint width changes for single line text
+ if (!canWrap &&
+ (constraints.maxWidth != prevConstraints.maxWidth ||
+ (constraints.minWidth != prevConstraints.minWidth))) {
+ // no soft wrap and width is different, always invalidate
+ return true
+ }
+
+ // if we get here width won't change, height may be clipped
+ if (constraints.maxHeight < localParagraph.height) {
+ // vertical clip changes
+ return true
+ }
+
+ // breaks can't change, height can't change
+ return false
+ }
+
+ private fun maxWidth(constraints: Constraints): Int {
+ val minWidth = constraints.minWidth
+ val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+ constraints.maxWidth
+ } else {
+ Constraints.Infinity
+ }
+ return if (minWidth == maxWidth) {
+ maxWidth
+ } else {
+ maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+ }
+ }
+
+ private fun markDirty() {
+ paragraph = null
+ paragraphIntrinsics = null
+ intrinsicsLayoutDirection = null
+ cachedIntrinsicWidth = -1
+ cachedIntrinsicHeight = -1
+ prevConstraints = Constraints.fixed(0, 0)
+ layoutSize = IntSize(0, 0)
+ didOverflow = false
+ }
+
+ /**
+ * This does an entire Text layout to produce the result, it is slow
+ */
+ fun slowCreateTextLayoutResultOrNull(): TextLayoutResult? {
+ // make sure we're in a valid place
+ val localLayoutDirection = intrinsicsLayoutDirection ?: return null
+ val localDensity = density ?: return null
+ val annotatedString = AnnotatedString(text)
+ paragraph ?: return null
+ paragraphIntrinsics ?: return null
+
+ // and redo layout with MultiParagraph
+ return TextLayoutResult(
+ TextLayoutInput(
+ annotatedString,
+ style,
+ emptyList(),
+ maxLines,
+ softWrap,
+ overflow,
+ localDensity,
+ localLayoutDirection,
+ fontFamilyResolver,
+ prevConstraints
+ ),
+ MultiParagraph(
+ MultiParagraphIntrinsics(
+ annotatedString = annotatedString,
+ style = style,
+ placeholders = emptyList(),
+ density = localDensity,
+ fontFamilyResolver = fontFamilyResolver
+ ),
+ prevConstraints,
+ maxLines,
+ overflow == TextOverflow.Ellipsis
+ ),
+ layoutSize
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
new file mode 100644
index 0000000..2e2ed43
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+
+@ExperimentalComposeUiApi
+internal class TextStringSimpleElement(
+ private val text: String,
+ private val style: TextStyle,
+ private val fontFamilyResolver: FontFamily.Resolver,
+ private val overflow: TextOverflow = TextOverflow.Clip,
+ private val softWrap: Boolean = true,
+ private val maxLines: Int = Int.MAX_VALUE,
+ private val minLines: Int = DefaultMinLines,
+) : ModifierNodeElement<TextStringSimpleNode>(inspectorInfo = debugInspectorInfo { }) {
+ override fun create(): TextStringSimpleNode = TextStringSimpleNode(
+ text,
+ style,
+ fontFamilyResolver,
+ overflow,
+ softWrap,
+ maxLines,
+ minLines
+ )
+
+ override fun update(node: TextStringSimpleNode): TextStringSimpleNode {
+ node.doInvalidations(
+ textChanged = node.updateText(
+ text = text
+ ),
+ layoutChanged = node.updateLayoutRelatedArgs(
+ style = style,
+ minLines = minLines,
+ maxLines = maxLines,
+ softWrap = softWrap,
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = overflow
+ )
+ )
+ return node
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+
+ if (other !is TextStringSimpleElement) return false
+
+ // these three are most likely to actually change
+ if (text != other.text) return false
+ if (style != other.style) return false
+
+ // these are equally unlikely to change
+ if (fontFamilyResolver != other.fontFamilyResolver) return false
+ if (overflow != other.overflow) return false
+ if (softWrap != other.softWrap) return false
+ if (maxLines != other.maxLines) return false
+ if (minLines != other.minLines) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = 31 * result + text.hashCode()
+ result = 31 * result + style.hashCode()
+ result = 31 * result + fontFamilyResolver.hashCode()
+ result = 31 * result + overflow.hashCode()
+ result = 31 * result + softWrap.hashCode()
+ result = 31 * result + maxLines
+ result = 31 * result + minLines
+ return result
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
new file mode 100644
index 0000000..a90ff59
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.invalidateMeasurements
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class TextStringSimpleNode(
+ private var text: String,
+ private var style: TextStyle,
+ private var fontFamilyResolver: FontFamily.Resolver,
+ private var overflow: TextOverflow = TextOverflow.Clip,
+ private var softWrap: Boolean = true,
+ private var maxLines: Int = Int.MAX_VALUE,
+ private var minLines: Int = DefaultMinLines
+) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+ private var baselineCache: Map<AlignmentLine, Int>? = null
+
+ private var _layoutCache: ParagraphLayoutCache? = null
+ private val layoutCache: ParagraphLayoutCache
+ get() {
+ if (_layoutCache == null) {
+ _layoutCache = ParagraphLayoutCache(
+ text,
+ style,
+ fontFamilyResolver,
+ overflow,
+ softWrap,
+ maxLines,
+ minLines,
+ )
+ }
+ return _layoutCache!!
+ }
+
+ private fun getLayoutCache(density: Density): ParagraphLayoutCache {
+ return layoutCache.also { it.density = density }
+ }
+
+ fun updateText(text: String): Boolean {
+ if (this.text == text) return false
+ this.text = text
+ return true
+ }
+
+ fun updateLayoutRelatedArgs(
+ style: TextStyle,
+ minLines: Int,
+ maxLines: Int,
+ softWrap: Boolean,
+ fontFamilyResolver: FontFamily.Resolver,
+ overflow: TextOverflow
+ ): Boolean {
+ var changed = false
+ if (this.style != style) {
+ this.style = style
+ changed = true
+ }
+
+ if (this.minLines != minLines) {
+ this.minLines = minLines
+ changed = true
+ }
+
+ if (this.maxLines != maxLines) {
+ this.maxLines != maxLines
+ changed = true
+ }
+
+ if (this.softWrap != softWrap) {
+ this.softWrap = softWrap
+ changed = true
+ }
+
+ if (this.fontFamilyResolver != fontFamilyResolver) {
+ this.fontFamilyResolver = fontFamilyResolver
+ changed = true
+ }
+
+ if (this.overflow != overflow) {
+ this.overflow = overflow
+ changed = true
+ }
+
+ return changed
+ }
+
+ fun doInvalidations(
+ textChanged: Boolean,
+ layoutChanged: Boolean
+ ) {
+ if (textChanged) {
+ _semanticsConfiguration = null
+ invalidateSemantics()
+ }
+
+ if (textChanged || layoutChanged) {
+ layoutCache.update(
+ text = text,
+ style = style,
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ minLines = minLines
+ )
+ invalidateMeasurements()
+ invalidateDraw()
+ }
+ }
+
+ private var _semanticsConfiguration: SemanticsConfiguration? = null
+
+ private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
+
+ private fun generateSemantics(text: String): SemanticsConfiguration {
+ var localSemanticsTextLayoutResult = semanticsTextLayoutResult
+ if (localSemanticsTextLayoutResult == null) {
+ localSemanticsTextLayoutResult = { textLayoutResult ->
+ val layout = layoutCache.slowCreateTextLayoutResultOrNull()?.also {
+ textLayoutResult.add(it)
+ }
+ layout != null
+ false
+ }
+ semanticsTextLayoutResult = localSemanticsTextLayoutResult
+ }
+ return SemanticsConfiguration().also {
+ it.isMergingSemanticsOfDescendants = false
+ it.isClearingSemantics = false
+ it.text = AnnotatedString(text)
+ it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
+ }
+ }
+
+ override val semanticsConfiguration: SemanticsConfiguration
+ get() {
+ var localSemantics = _semanticsConfiguration
+ if (localSemantics == null) {
+ localSemantics = generateSemantics(text)
+ _semanticsConfiguration = localSemantics
+ }
+ return localSemantics
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val layoutCache = getLayoutCache(this)
+
+ val didChangeLayout = layoutCache.layoutWithConstraints(constraints, layoutDirection)
+ // ensure measure restarts when hasStaleResolvedFonts by reading in measure
+ layoutCache.observeFontChanges
+ val paragraph = layoutCache.paragraph!!
+ val layoutSize = layoutCache.layoutSize
+
+ if (didChangeLayout) {
+ invalidateLayer()
+ baselineCache = mapOf(
+ FirstBaseline to paragraph.firstBaseline.roundToInt(),
+ LastBaseline to paragraph.lastBaseline.roundToInt()
+ )
+ }
+
+ // then allow children to measure _inside_ our final box, with the above placeholders
+ val placeable = measurable.measure(
+ Constraints.fixed(
+ layoutSize.width,
+ layoutSize.height
+ )
+ )
+
+ return layout(
+ layoutSize.width,
+ layoutSize.height,
+ baselineCache!!
+ ) {
+ // this is basically a graphicsLayer
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int {
+ return getLayoutCache(this).minIntrinsicWidth
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = getLayoutCache(this).maxIntrinsicWidth
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+ @OptIn(ExperimentalTextApi::class)
+ override fun ContentDrawScope.draw() {
+ val localParagraph = requireNotNull(layoutCache.paragraph)
+ drawIntoCanvas { canvas ->
+ val willClip = layoutCache.didOverflow
+ if (willClip) {
+ val width = layoutCache.layoutSize.width.toFloat()
+ val height = layoutCache.layoutSize.height.toFloat()
+ val bounds = Rect(Offset.Zero, Size(width, height))
+ canvas.save()
+ canvas.clipRect(bounds)
+ }
+ try {
+ val textDecoration = style.textDecoration ?: TextDecoration.None
+ val shadow = style.shadow ?: Shadow.None
+ val drawStyle = style.drawStyle ?: Fill
+ val brush = style.brush
+ if (brush != null) {
+ val alpha = style.alpha
+ localParagraph.paint(
+ canvas = canvas,
+ brush = brush,
+ alpha = alpha,
+ shadow = shadow,
+ drawStyle = drawStyle,
+ textDecoration = textDecoration
+ )
+ } else {
+ val color = style.color
+ localParagraph.paint(
+ canvas = canvas,
+ color = color,
+ shadow = shadow,
+ textDecoration = textDecoration,
+ drawStyle = drawStyle
+ )
+ }
+ } finally {
+ if (willClip) {
+ canvas.restore()
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 1ccb795..d087b0b 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -505,9 +505,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPinnableContainerProviderKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -718,6 +715,9 @@
public final class LazyNearestItemsRangeKt {
}
+ public final class LazyPinnableContainerProviderKt {
+ }
+
public final class LazySaveableStateHolderKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 272d94d..8b4e97f 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -599,9 +599,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPinnableContainerProviderKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -874,6 +871,9 @@
method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
}
+ public final class LazyPinnableContainerProviderKt {
+ }
+
public final class LazySaveableStateHolderKt {
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 1ccb795..d087b0b 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -505,9 +505,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPinnableContainerProviderKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -718,6 +715,9 @@
public final class LazyNearestItemsRangeKt {
}
+ public final class LazyPinnableContainerProviderKt {
+ }
+
public final class LazySaveableStateHolderKt {
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
index bfbb956..b191349a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
@@ -16,12 +16,21 @@
package androidx.compose.foundation.demos.text
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
/**
* These demos are for using the memory profiler to observe initial compo and recompo memory
@@ -32,8 +41,12 @@
@Composable
fun MemoryAllocsSetText() {
Column {
- Text("Run in memory profiler to emulate setting text value when observable loads")
- Text("This is designed to be used in the Android Studio memory profiler")
+ Preamble("""
+ @Composable
+ fun SetText(text: State<String>) {
+ Text(text.value)
+ }""".trimIndent()
+ )
SetText(textToggler())
}
}
@@ -47,13 +60,37 @@
@Composable
fun MemoryAllocsIfNotEmptyText() {
Column {
- Text("Run in memory profiler to emulate calling Text after an observable loads")
- Text("This is designed to be used in the Android Studio memory profiler")
+ Preamble("""
+ @Composable
+ fun IfNotEmptyText(text: State<String>) {
+ if (text.value.isNotEmpty()) {
+ Text(text.value)
+ }
+ }""".trimIndent()
+ )
IfNotEmptyText(textToggler())
}
}
@Composable
+fun Preamble(sourceCode: String) {
+ Text("Run in memory profiler to emulate text behavior during observable loads")
+ Text(text = sourceCode,
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color(220, 230, 240)),
+ fontFamily = FontFamily.Monospace,
+ color = Color(41, 17, 27),
+ fontSize = 10.sp
+ )
+ Divider(
+ Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp))
+ Text("\uD83D\uDC47 running here \uD83D\uDC47")
+}
+
+@Composable
fun IfNotEmptyText(text: State<String>) {
if (text.value.isNotEmpty()) {
Text(text.value)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 62e4dbf..258991f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -117,10 +117,10 @@
)
),
DemoCategory(
- "⚠️️ Memory benchmark ⚠️️",
+ "\uD83D\uDD75️️️ Memory allocs",
listOf(
- ComposableDemo("SetText") { MemoryAllocsSetText() },
- ComposableDemo("IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
+ ComposableDemo("\uD83D\uDD75️ SetText") { MemoryAllocsSetText() },
+ ComposableDemo("\uD83D\uDD75️ IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
)
)
)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
index 8f5638a..37cd6eb 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
@@ -25,7 +25,6 @@
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -43,6 +42,7 @@
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
@@ -55,7 +55,6 @@
import com.google.common.truth.Truth.assertThat
import kotlin.math.roundToInt
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -73,8 +72,13 @@
private val BackgroundColor = Color.White
}
+ private val motionDurationScale = object : MotionDurationScale {
+ override var scaleFactor: Float by mutableStateOf(1f)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
@get:Rule
- val rule = createComposeRule()
+ val rule = createComposeRule(effectContext = motionDurationScale)
/**
* Converts pxPerFrame to dps per second. The frame delay is 16ms, which means there are
@@ -119,14 +123,12 @@
}
}
- @Ignore("b/265177763: Not currently possible to inject a MotionDurationScale in tests.")
+ @Suppress("UnnecessaryOptInAnnotation")
+ @OptIn(ExperimentalTestApi::class)
@Test fun animates_whenAnimationsDisabledBySystem() {
- // TODO(b/265177763) Inject 0 duration scale.
+ motionDurationScale.scaleFactor = 0f
rule.setContent {
- LaunchedEffect(Unit) {
- assertThat(coroutineContext[MotionDurationScale]?.scaleFactor).isEqualTo(0f)
- }
TestMarqueeContent(
Modifier.basicMarqueeWithTestParams(
iterations = 1,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
new file mode 100644
index 0000000..91e6a5b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
@@ -0,0 +1,666 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class LazyGridPinnableContainerTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var pinnableContainer: PinnableContainer? = null
+
+ private val itemSizePx = 10
+ private var itemSize = Dp.Unspecified
+
+ private val composed = mutableSetOf<Int>()
+
+ @Before
+ fun setup() {
+ itemSize = with(rule.density) { itemSizePx.toDp() }
+ }
+
+ @Composable
+ fun Item(index: Int) {
+ Box(
+ Modifier
+ .size(itemSize)
+ .testTag("$index")
+ )
+ DisposableEffect(index) {
+ composed.add(index)
+ onDispose {
+ composed.remove(index)
+ }
+ }
+ }
+
+ @Test
+ fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+ val state = LazyGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // not visible items were disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ // item 1 is still pinned
+ assertThat(composed).contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+ val state = LazyGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(4)
+ }
+ }
+
+ rule.waitUntil {
+ // not visible items were disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).doesNotContain(0)
+ assertThat(composed).contains(1)
+ assertThat(composed).doesNotContain(2)
+ assertThat(composed).doesNotContain(3)
+ assertThat(composed).contains(4)
+ }
+ }
+
+ @Test
+ fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+ val state = LazyGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 4) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(4)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ assertThat(composed).contains(5)
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(5)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(0)
+ assertThat(composed).contains(1)
+ assertThat(composed).doesNotContain(2)
+ assertThat(composed).doesNotContain(3)
+ assertThat(composed).contains(4)
+ assertThat(composed).doesNotContain(5)
+ }
+ }
+
+ @Test
+ fun pinnedItemCanBeUnpinned() {
+ val state = LazyGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ handle.unpin()
+ }
+
+ rule.waitUntil {
+ // wait for unpinned item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+ val state = LazyGridState()
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 3),
+ state = state
+ ) {
+ items(list, key = { it }) { index ->
+ if (index == 2) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).containsExactly(0, 1, 2)
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 3, 4, 1, 2)
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun unpinnedWhenLazyGridStateChanges() {
+ var state by mutableStateOf(LazyGridState(firstVisibleItemIndex = 2))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 2) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(3)
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(3)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(2)
+ state = LazyGridState()
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(2)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinAfterLazyGridStateChange() {
+ var state by mutableStateOf(LazyGridState())
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 0) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state = LazyGridState()
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ runBlocking {
+ state.scrollToItem(2)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(0)
+ }
+ }
+
+ @Test
+ fun itemsArePinnedBasedOnGlobalIndexes() {
+ val state = LazyGridState(firstVisibleItemIndex = 3)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ repeat(100) { index ->
+ item {
+ if (index == 3) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(4)
+ runBlocking {
+ state.scrollToItem(6)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(4)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(3)
+ }
+
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsRemovedWhenNotVisible() {
+ val state = LazyGridState(3)
+ var itemCount by mutableStateOf(10)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(itemCount) { index ->
+ if (index == 3) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ assertThat(composed).contains(4)
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(4)
+ }
+
+ rule.runOnIdle {
+ itemCount = 3
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(3)
+ }
+
+ rule.onNodeWithTag("3")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsRemovedWhenVisible() {
+ val state = LazyGridState(0)
+ var items by mutableStateOf(listOf(0, 1, 2))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(items) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ items = listOf(0, 2)
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedMultipleTimes() {
+ val state = LazyGridState(0)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ val handles = mutableListOf<PinnedHandle>()
+ rule.runOnIdle {
+ handles.add(requireNotNull(pinnableContainer).pin())
+ handles.add(requireNotNull(pinnableContainer).pin())
+ }
+
+ rule.runOnIdle {
+ // pinned 3 times in total
+ handles.add(requireNotNull(pinnableContainer).pin())
+ assertThat(composed).contains(0)
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(0)
+ }
+
+ while (handles.isNotEmpty()) {
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ handles.removeFirst().unpin()
+ }
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(1)
+ }
+ }
+
+ @Test
+ fun pinningIsPropagatedToParentContainer() {
+ var parentPinned = false
+ val parentContainer = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parentPinned = true
+ return PinnedHandle { parentPinned = false }
+ }
+ }
+ // Arrange.
+ rule.setContent {
+ CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+ LazyVerticalGrid(GridCells.Fixed(1)) {
+ item {
+ pinnableContainer = LocalPinnableContainer.current
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parentPinned).isTrue()
+ handle.unpin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parentPinned).isFalse()
+ }
+ }
+
+ @Test
+ fun parentContainerChange_pinningIsMaintained() {
+ var parent1Pinned = false
+ val parent1Container = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parent1Pinned = true
+ return PinnedHandle { parent1Pinned = false }
+ }
+ }
+ var parent2Pinned = false
+ val parent2Container = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parent2Pinned = true
+ return PinnedHandle { parent2Pinned = false }
+ }
+ }
+ var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+ // Arrange.
+ rule.setContent {
+ CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+ LazyVerticalGrid(GridCells.Fixed(1)) {
+ item {
+ pinnableContainer = LocalPinnableContainer.current
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parent1Pinned).isTrue()
+ assertThat(parent2Pinned).isFalse()
+ parentContainer = parent2Container
+ }
+
+ rule.runOnIdle {
+ assertThat(parent1Pinned).isFalse()
+ assertThat(parent2Pinned).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
new file mode 100644
index 0000000..7d688a4
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
@@ -0,0 +1,668 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+class LazyStaggeredGridPinnableContainerTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var pinnableContainer: PinnableContainer? = null
+
+ private val itemSizePx = 10
+ private var itemSize = Dp.Unspecified
+
+ private val composed = mutableSetOf<Int>()
+
+ @Before
+ fun setup() {
+ itemSize = with(rule.density) { itemSizePx.toDp() }
+ }
+
+ @Composable
+ fun Item(index: Int) {
+ Box(
+ Modifier
+ .size(itemSize)
+ .testTag("$index")
+ )
+ DisposableEffect(index) {
+ composed.add(index)
+ onDispose {
+ composed.remove(index)
+ }
+ }
+ }
+
+ @Test
+ fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+ val state = LazyStaggeredGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // not visible items were disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ // item 1 is still pinned
+ assertThat(composed).contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+ val state = LazyStaggeredGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(4)
+ }
+ }
+
+ rule.waitUntil {
+ // not visible items were disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).doesNotContain(0)
+ assertThat(composed).contains(1)
+ assertThat(composed).doesNotContain(2)
+ assertThat(composed).doesNotContain(3)
+ assertThat(composed).contains(4)
+ }
+ }
+
+ @Test
+ fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+ val state = LazyStaggeredGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 4) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(4)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ assertThat(composed).contains(5)
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(5)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(0)
+ assertThat(composed).contains(1)
+ assertThat(composed).doesNotContain(2)
+ assertThat(composed).doesNotContain(3)
+ assertThat(composed).contains(4)
+ assertThat(composed).doesNotContain(5)
+ }
+ }
+
+ @Test
+ fun pinnedItemCanBeUnpinned() {
+ val state = LazyStaggeredGridState()
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible items to be disposed
+ !composed.contains(0)
+ }
+
+ rule.runOnIdle {
+ handle.unpin()
+ }
+
+ rule.waitUntil {
+ // wait for unpinned item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+ val state = LazyStaggeredGridState()
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 3),
+ state = state
+ ) {
+ items(list, key = { it }) { index ->
+ if (index == 2) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).containsExactly(0, 1, 2)
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ list = listOf(0, 3, 4, 1, 2)
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun unpinnedWhenLazyStaggeredGridStateChanges() {
+ var state by mutableStateOf(LazyStaggeredGridState(initialFirstVisibleItemIndex = 2))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 2) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(3)
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(3)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(2)
+ state = LazyStaggeredGridState()
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(2)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinAfterLazyStaggeredGridStateChange() {
+ var state by mutableStateOf(LazyStaggeredGridState())
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 0) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state = LazyStaggeredGridState()
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ runBlocking {
+ state.scrollToItem(2)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(0)
+ }
+ }
+
+ @Test
+ fun itemsArePinnedBasedOnGlobalIndexes() {
+ val state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 3)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ repeat(100) { index ->
+ item {
+ if (index == 3) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(4)
+ runBlocking {
+ state.scrollToItem(6)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(4)
+ }
+
+ rule.runOnIdle {
+ assertThat(composed).contains(3)
+ }
+
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+ .assertIsPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsRemovedWhenNotVisible() {
+ val state = LazyStaggeredGridState(3)
+ var itemCount by mutableStateOf(10)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(itemCount) { index ->
+ if (index == 3) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ assertThat(composed).contains(4)
+ runBlocking {
+ state.scrollToItem(0)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(4)
+ }
+
+ rule.runOnIdle {
+ itemCount = 3
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(3)
+ }
+
+ rule.onNodeWithTag("3")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedItemIsRemovedWhenVisible() {
+ val state = LazyStaggeredGridState(0)
+ var items by mutableStateOf(listOf(0, 1, 2))
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(items) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ items = listOf(0, 2)
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(1)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsNotPlaced()
+ }
+
+ @Test
+ fun pinnedMultipleTimes() {
+ val state = LazyStaggeredGridState(0)
+ // Arrange.
+ rule.setContent {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(1),
+ modifier = Modifier.size(itemSize * 2),
+ state = state
+ ) {
+ items(100) { index ->
+ if (index == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Item(index)
+ }
+ }
+ }
+
+ val handles = mutableListOf<PinnedHandle>()
+ rule.runOnIdle {
+ handles.add(requireNotNull(pinnableContainer).pin())
+ handles.add(requireNotNull(pinnableContainer).pin())
+ }
+
+ rule.runOnIdle {
+ // pinned 3 times in total
+ handles.add(requireNotNull(pinnableContainer).pin())
+ assertThat(composed).contains(0)
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.waitUntil {
+ // wait for not visible item to be disposed
+ !composed.contains(0)
+ }
+
+ while (handles.isNotEmpty()) {
+ rule.runOnIdle {
+ assertThat(composed).contains(1)
+ handles.removeFirst().unpin()
+ }
+ }
+
+ rule.waitUntil {
+ // wait for pinned item to be disposed
+ !composed.contains(1)
+ }
+ }
+
+ @Test
+ fun pinningIsPropagatedToParentContainer() {
+ var parentPinned = false
+ val parentContainer = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parentPinned = true
+ return PinnedHandle { parentPinned = false }
+ }
+ }
+ // Arrange.
+ rule.setContent {
+ CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+ LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+ item {
+ pinnableContainer = LocalPinnableContainer.current
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+ }
+
+ val handle = rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parentPinned).isTrue()
+ handle.unpin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parentPinned).isFalse()
+ }
+ }
+
+ @Test
+ fun parentContainerChange_pinningIsMaintained() {
+ var parent1Pinned = false
+ val parent1Container = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parent1Pinned = true
+ return PinnedHandle { parent1Pinned = false }
+ }
+ }
+ var parent2Pinned = false
+ val parent2Container = object : PinnableContainer {
+ override fun pin(): PinnedHandle {
+ parent2Pinned = true
+ return PinnedHandle { parent2Pinned = false }
+ }
+ }
+ var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+ // Arrange.
+ rule.setContent {
+ CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+ LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+ item {
+ pinnableContainer = LocalPinnableContainer.current
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ assertThat(parent1Pinned).isTrue()
+ assertThat(parent2Pinned).isFalse()
+ parentContainer = parent2Container
+ }
+
+ rule.runOnIdle {
+ assertThat(parent1Pinned).isFalse()
+ assertThat(parent2Pinned).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index 3e2c1f1..c3a20e6e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
@@ -38,6 +39,7 @@
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.createComposeRule
@@ -58,15 +60,19 @@
import com.google.common.truth.Truth.assertThat
import kotlin.math.ceil
import kotlin.math.floor
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@LargeTest
class TextFieldCursorTest {
+ private val motionDurationScale = object : MotionDurationScale {
+ override var scaleFactor: Float by mutableStateOf(1f)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
@get:Rule
- val rule = createComposeRule().also {
+ val rule = createComposeRule(effectContext = motionDurationScale).also {
it.mainClock.autoAdvance = false
}
@@ -87,8 +93,11 @@
private val bgModifier = Modifier.background(textFieldBgColor)
private val focusModifier = Modifier.onFocusChanged { if (it.isFocused) isFocused = true }
private val sizeModifier = Modifier.size(textFieldWidth, textFieldHeight)
+
// default TextFieldModifier
- private val textFieldModifier = sizeModifier.then(bgModifier).then(focusModifier)
+ private val textFieldModifier = sizeModifier
+ .then(bgModifier)
+ .then(focusModifier)
// default onTextLayout to capture cursor boundaries.
private val onTextLayout: (TextLayoutResult) -> Unit = { cursorRect = it.getCursorRect(0) }
@@ -206,11 +215,13 @@
)
}
- // TODO(b/265177763) Update this test to set MotionDurationScale to 0 when that's supported.
- @Ignore("b/265177763")
+ @Suppress("UnnecessaryOptInAnnotation")
+ @OptIn(ExperimentalTestApi::class)
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorBlinkingAnimation_whenSystemDisablesAnimations() = with(rule.density) {
+ motionDurationScale.scaleFactor = 0f
+
rule.setContent {
// The padding helps if the test is run accidentally in landscape. Landscape makes
// the cursor to be next to the navigation bar which affects the red color to be a bit
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index ae6229e..f2b57ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -79,7 +80,7 @@
intervals = intervals,
nearestItemsRange = nearestItemsRange,
itemContent = { interval, index ->
- LazyListPinnableContainerProvider(state, index) {
+ LazyPinnableContainerProvider(state.pinnedItems, index) {
interval.value.item.invoke(itemScope, index - interval.startIndex)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 104832d7..5c9ea54 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.fastFilter
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
@@ -56,7 +57,7 @@
placementAnimator: LazyListItemPlacementAnimator,
beyondBoundsInfo: LazyListBeyondBoundsInfo,
beyondBoundsItemCount: Int,
- pinnedItems: List<LazyListPinnedItem>,
+ pinnedItems: List<LazyPinnedItem>,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyListMeasureResult {
require(beforeContentPadding >= 0)
@@ -337,7 +338,7 @@
itemProvider: LazyMeasuredItemProvider,
itemsCount: Int,
beyondBoundsItemCount: Int,
- pinnedItems: List<LazyListPinnedItem>
+ pinnedItems: List<LazyPinnedItem>
): List<LazyMeasuredItem> {
fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
@@ -377,7 +378,7 @@
itemProvider: LazyMeasuredItemProvider,
itemsCount: Int,
beyondBoundsItemCount: Int,
- pinnedItems: List<LazyListPinnedItem>
+ pinnedItems: List<LazyPinnedItem>
): List<LazyMeasuredItem> {
fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 906b051..d1507e2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -24,11 +24,11 @@
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
@@ -224,7 +224,7 @@
/**
* List of extra items to compose during the measure pass.
*/
- internal val pinnedItems = mutableStateListOf<LazyListPinnedItem>()
+ internal val pinnedItems = LazyPinnedItemContainer()
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index e643447..7ecc15a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -261,7 +261,7 @@
this,
spaceBetweenLines
) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
- LazyMeasuredItem(
+ LazyGridMeasuredItem(
index = index,
key = key,
isVertical = isVertical,
@@ -285,7 +285,7 @@
measuredItemProvider,
spanLayoutProvider
) { index, items, spans, mainAxisSpacing ->
- LazyMeasuredLine(
+ LazyGridMeasuredLine(
index = index,
items = items,
spans = spans,
@@ -345,6 +345,7 @@
density = this,
placementAnimator = placementAnimator,
spanLayoutProvider = spanLayoutProvider,
+ pinnedItems = state.pinnedItems,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index f7a2499..1e5f46c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -58,8 +58,8 @@
private val movingAwayKeys = LinkedHashSet<Any>()
private val movingInFromStartBound = mutableListOf<LazyGridPositionedItem>()
private val movingInFromEndBound = mutableListOf<LazyGridPositionedItem>()
- private val movingAwayToStartBound = mutableListOf<LazyMeasuredItem>()
- private val movingAwayToEndBound = mutableListOf<LazyMeasuredItem>()
+ private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
+ private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
/**
* Should be called after the measuring so we can detect position changes and start animations.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index 57d3aed..562daad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -56,7 +57,8 @@
LazyGridItemProviderImpl(
gridScope.intervals,
gridScope.hasCustomSpans,
- nearestItemsRangeState.value
+ state,
+ nearestItemsRangeState.value,
)
}
@@ -80,12 +82,15 @@
private class LazyGridItemProviderImpl(
private val intervals: IntervalList<LazyGridIntervalContent>,
override val hasCustomSpans: Boolean,
+ state: LazyGridState,
nearestItemsRange: IntRange
) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
intervals = intervals,
nearestItemsRange = nearestItemsRange,
itemContent = { interval, index ->
- interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+ LazyPinnableContainerProvider(state.pinnedItems, index) {
+ interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+ }
}
) {
override val spanLayoutProvider: LazyGridSpanLayoutProvider =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 7f4538a..12c9769 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -16,9 +16,10 @@
package androidx.compose.foundation.lazy.grid
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.fastFilter
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
@@ -56,6 +57,7 @@
density: Density,
placementAnimator: LazyGridItemPlacementAnimator,
spanLayoutProvider: LazyGridSpanLayoutProvider,
+ pinnedItems: List<LazyPinnedItem>,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyGridMeasureResult {
require(beforeContentPadding >= 0)
@@ -94,7 +96,7 @@
}
// this will contain all the MeasuredItems representing the visible lines
- val visibleLines = mutableListOf<LazyMeasuredLine>()
+ val visibleLines = mutableListOf<LazyGridMeasuredLine>()
// define min and max offsets
val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0
@@ -201,6 +203,22 @@
val visibleLinesScrollOffset = -currentFirstLineScrollOffset
var firstLine = visibleLines.first()
+ val firstItemIndex = firstLine.items.firstOrNull()?.index?.value ?: 0
+ val lastItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
+ val extraItemsBefore = calculateExtraItems(
+ pinnedItems,
+ measuredItemProvider,
+ itemConstraints = { measuredLineProvider.itemConstraints(it) },
+ filter = { it in 0 until firstItemIndex }
+ )
+
+ val extraItemsAfter = calculateExtraItems(
+ pinnedItems,
+ measuredItemProvider,
+ itemConstraints = { measuredLineProvider.itemConstraints(it) },
+ filter = { it in (lastItemIndex + 1) until itemsCount }
+ )
+
// even if we compose lines to fill before content padding we should ignore lines fully
// located there for the state's scroll position calculation (first line + first offset)
if (beforeContentPadding > 0 || spaceBetweenLines < 0) {
@@ -229,6 +247,8 @@
val positionedItems = calculateItemsOffsets(
lines = visibleLines,
+ itemsBefore = extraItemsBefore,
+ itemsAfter = extraItemsAfter,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
finalMainAxisOffset = currentMainAxisOffset,
@@ -250,19 +270,24 @@
spanLayoutProvider = spanLayoutProvider
)
- val lastVisibleItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
return LazyGridMeasureResult(
firstVisibleLine = firstLine,
firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
canScrollForward =
- lastVisibleItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
+ lastItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
},
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
- visibleItemsInfo = positionedItems,
+ visibleItemsInfo = if (extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) {
+ positionedItems
+ } else {
+ positionedItems.fastFilter {
+ it.index in firstItemIndex..lastItemIndex
+ }
+ },
totalItemsCount = itemsCount,
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
@@ -271,12 +296,36 @@
}
}
+private inline fun calculateExtraItems(
+ pinnedItems: List<LazyPinnedItem>,
+ itemProvider: LazyMeasuredItemProvider,
+ itemConstraints: (ItemIndex) -> Constraints,
+ filter: (Int) -> Boolean
+): List<LazyGridMeasuredItem> {
+ var items: MutableList<LazyGridMeasuredItem>? = null
+
+ pinnedItems.fastForEach {
+ val index = ItemIndex(it.index)
+ if (filter(it.index)) {
+ val constraints = itemConstraints(index)
+ val item = itemProvider.getAndMeasure(index, constraints = constraints)
+ if (items == null) {
+ items = mutableListOf()
+ }
+ items?.add(item)
+ }
+ }
+
+ return items ?: emptyList()
+}
+
/**
- * Calculates [LazyMeasuredLine]s offsets.
+ * Calculates [LazyGridMeasuredLine]s offsets.
*/
-@OptIn(ExperimentalFoundationApi::class)
private fun calculateItemsOffsets(
- lines: List<LazyMeasuredLine>,
+ lines: List<LazyGridMeasuredLine>,
+ itemsBefore: List<LazyGridMeasuredItem>,
+ itemsAfter: List<LazyGridMeasuredItem>,
layoutWidth: Int,
layoutHeight: Int,
finalMainAxisOffset: Int,
@@ -297,6 +346,7 @@
val positionedItems = ArrayList<LazyGridPositionedItem>(lines.fastSumBy { it.items.size })
if (hasSpareSpace) {
+ require(itemsBefore.isEmpty() && itemsAfter.isEmpty())
val linesCount = lines.size
fun Int.reverseAware() =
if (!reverseLayout) this else linesCount - this - 1
@@ -335,10 +385,36 @@
}
} else {
var currentMainAxis = firstLineScrollOffset
+
+ itemsBefore.fastForEach {
+ currentMainAxis -= it.mainAxisSizeWithSpacings
+ positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+ }
+
+ currentMainAxis = firstLineScrollOffset
lines.fastForEach {
positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
currentMainAxis += it.mainAxisSizeWithSpacings
}
+
+ itemsAfter.fastForEach {
+ positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += it.mainAxisSizeWithSpacings
+ }
}
return positionedItems
}
+
+private fun LazyGridMeasuredItem.positionExtraItem(
+ mainAxisOffset: Int,
+ layoutWidth: Int,
+ layoutHeight: Int
+): LazyGridPositionedItem =
+ position(
+ mainAxisOffset = mainAxisOffset,
+ crossAxisOffset = 0,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ row = 0,
+ column = 0
+ )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
index 8218fac..769a7da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
@@ -28,7 +28,7 @@
internal class LazyGridMeasureResult(
// properties defining the scroll position:
/** The new first visible line of items.*/
- val firstVisibleLine: LazyMeasuredLine?,
+ val firstVisibleLine: LazyGridMeasuredLine?,
/** The new value for [LazyGridState.firstVisibleItemScrollOffset].*/
val firstVisibleLineScrollOffset: Int,
/** True if there is some space available to continue scrolling in the forward direction.*/
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
similarity index 99%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
index dcf4450..eff4278 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -29,7 +29,7 @@
* if the user emit multiple layout nodes in the item callback.
*/
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredItem(
+internal class LazyGridMeasuredItem(
val index: ItemIndex,
val key: Any,
private val isVertical: Boolean,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
similarity index 96%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
index 2c9279f..efc095e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
@@ -24,9 +24,9 @@
* multiple placeables if the user emit multiple layout nodes in the item callback.
*/
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredLine constructor(
+internal class LazyGridMeasuredLine constructor(
val index: LineIndex,
- val items: Array<LazyMeasuredItem>,
+ val items: Array<LazyGridMeasuredItem>,
private val spans: List<GridItemSpan>,
private val isVertical: Boolean,
private val slotsPerLine: Int,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index f7d777b..e4d0e7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -210,7 +210,7 @@
return LineIndex(currentLine)
}
- private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
+ fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
with(LazyGridItemSpanScopeImpl) {
maxCurrentLineSpan = maxSpan
maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index a19e3d2..89ac78a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.lazy.AwaitFirstLayoutModifier
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -81,6 +82,7 @@
firstVisibleItemIndex: Int = 0,
firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
+
/**
* The holder class for the current scroll position.
*/
@@ -226,6 +228,11 @@
private val animateScrollScope = LazyGridAnimateScrollScope(this)
/**
+ * Pinned items are measured and placed even when they are beyond bounds of lazy layout.
+ */
+ internal val pinnedItems = LazyPinnedItemContainer()
+
+ /**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
*
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
index 68d6504..651aa4f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -33,13 +33,13 @@
) {
/**
* Used to subcompose individual items of lazy grids. Composed placeables will be measured
- * with the provided [constraints] and wrapped into [LazyMeasuredItem].
+ * with the provided [constraints] and wrapped into [LazyGridMeasuredItem].
*/
fun getAndMeasure(
index: ItemIndex,
mainAxisSpacing: Int = defaultMainAxisSpacing,
constraints: Constraints
- ): LazyMeasuredItem {
+ ): LazyGridMeasuredItem {
val key = itemProvider.getKey(index.value)
val placeables = measureScope.measure(index.value, constraints)
val crossAxisSize = if (constraints.hasFixedWidth) {
@@ -72,5 +72,5 @@
crossAxisSize: Int,
mainAxisSpacing: Int,
placeables: List<Placeable>
- ): LazyMeasuredItem
+ ): LazyGridMeasuredItem
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 588ae51..0b7296e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -46,11 +46,19 @@
}
}
+ fun itemConstraints(itemIndex: ItemIndex): Constraints {
+ val span = spanLayoutProvider.spanOf(
+ itemIndex.value,
+ spanLayoutProvider.slotsPerLine
+ )
+ return childConstraints(0, span)
+ }
+
/**
* Used to subcompose items on lines of lazy grids. Composed placeables will be measured
- * with the correct constraints and wrapped into [LazyMeasuredLine].
+ * with the correct constraints and wrapped into [LazyGridMeasuredLine].
*/
- fun getAndMeasure(lineIndex: LineIndex): LazyMeasuredLine {
+ fun getAndMeasure(lineIndex: LineIndex): LazyGridMeasuredLine {
val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex.value)
val lineItemsCount = lineConfiguration.spans.size
@@ -93,8 +101,8 @@
internal fun interface MeasuredLineFactory {
fun createLine(
index: LineIndex,
- items: Array<LazyMeasuredItem>,
+ items: Array<LazyGridMeasuredItem>,
spans: List<GridItemSpan>,
mainAxisSpacing: Int
- ): LazyMeasuredLine
+ ): LazyGridMeasuredLine
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
similarity index 78%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
index 6d8ffb8..a3e794a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy
+package androidx.compose.foundation.lazy.layout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -24,20 +24,33 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.layout.LocalPinnableContainer
import androidx.compose.ui.layout.PinnableContainer
-internal interface LazyListPinnedItem {
+internal interface LazyPinnedItem {
val index: Int
}
+internal class LazyPinnedItemContainer(
+ private val pinnedItems: MutableList<LazyPinnedItem> = SnapshotStateList()
+) : List<LazyPinnedItem> by pinnedItems {
+ fun pin(item: LazyPinnedItem) {
+ pinnedItems.add(item)
+ }
+
+ fun unpin(item: LazyPinnedItem) {
+ pinnedItems.remove(item)
+ }
+}
+
@Composable
-internal fun LazyListPinnableContainerProvider(
- state: LazyListState,
+internal fun LazyPinnableContainerProvider(
+ owner: LazyPinnedItemContainer,
index: Int,
content: @Composable () -> Unit
) {
- val pinnableItem = remember(state) { LazyListPinnableItem(state) }
+ val pinnableItem = remember(owner) { LazyPinnableItem(owner) }
pinnableItem.index = index
pinnableItem.parentPinnableContainer = LocalPinnableContainer.current
DisposableEffect(pinnableItem) { onDispose { pinnableItem.onDisposed() } }
@@ -46,9 +59,9 @@
)
}
-private class LazyListPinnableItem(
- private val state: LazyListState,
-) : PinnableContainer, PinnableContainer.PinnedHandle, LazyListPinnedItem {
+private class LazyPinnableItem(
+ private val owner: LazyPinnedItemContainer,
+) : PinnableContainer, PinnableContainer.PinnedHandle, LazyPinnedItem {
/**
* Current index associated with this item.
*/
@@ -86,7 +99,7 @@
override fun pin(): PinnableContainer.PinnedHandle {
if (pinsCount == 0) {
- state.pinnedItems.add(this)
+ owner.pin(this)
parentHandle = parentPinnableContainer?.pin()
}
pinsCount++
@@ -97,7 +110,7 @@
check(pinsCount > 0) { "Unpin should only be called once" }
pinsCount--
if (pinsCount == 0) {
- state.pinnedItems.remove(this)
+ owner.unpin(this)
parentHandle?.unpin()
parentHandle = null
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
index be07e927..7ce5cff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -48,11 +49,13 @@
object : LazyLayoutItemProvider by LazyLayoutItemProvider(
scope.intervals,
nearestItemsRangeState.value,
- { interval, index ->
- interval.value.item.invoke(
- LazyStaggeredGridItemScopeImpl,
- index - interval.startIndex
- )
+ itemContent = { interval, index ->
+ LazyPinnableContainerProvider(state.pinnedItems, index) {
+ interval.value.item.invoke(
+ LazyStaggeredGridItemScopeImpl,
+ index - interval.startIndex
+ )
+ }
}
), LazyStaggeredGridItemProvider {
override val spanProvider = LazyStaggeredGridSpanProvider(scope.intervals)
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 28210ac..c24b674 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
@@ -21,9 +21,9 @@
import androidx.compose.foundation.fastMaxOfOrNull
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
-import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
@@ -636,6 +636,65 @@
// end measure
+ var extraItemOffset = itemScrollOffsets[0]
+ val extraItemsBefore = calculateExtraItems(
+ state.pinnedItems,
+ position = {
+ extraItemOffset -= it.sizeWithSpacings
+ it.position(0, extraItemOffset, 0)
+ }
+ ) { itemIndex ->
+ val lane = laneInfo.getLane(itemIndex)
+ when (lane) {
+ Unset, FullSpan -> {
+ firstItemIndices.all { it > itemIndex }
+ }
+ else -> {
+ firstItemIndices[lane] > itemIndex
+ }
+ }
+ }
+
+ val positionedItems = calculatePositionedItems(
+ measuredItems,
+ itemScrollOffsets
+ )
+
+ extraItemOffset = itemScrollOffsets[0]
+ val extraItemsAfter = calculateExtraItems(
+ state.pinnedItems,
+ position = {
+ val positionedItem = it.position(0, extraItemOffset, 0)
+ extraItemOffset += it.sizeWithSpacings
+ positionedItem
+ }
+ ) { itemIndex ->
+ if (itemIndex >= itemCount) {
+ return@calculateExtraItems false
+ }
+ val lane = laneInfo.getLane(itemIndex)
+ when (lane) {
+ Unset, FullSpan -> {
+ currentItemIndices.all { it < itemIndex }
+ }
+ else -> {
+ currentItemIndices[lane] < itemIndex
+ }
+ }
+ }
+
+ debugLog {
+ @Suppress("ListIterator")
+ "| positioned: ${positionedItems.map { "${it.index} at ${it.offset}" }.toList()}"
+ }
+ debugLog {
+ "========== MEASURE COMPLETED ==========="
+ }
+
+ // todo: reverse layout support
+
+ // End placement
+
val layoutWidth = if (isVertical) {
constraints.maxWidth
} else {
@@ -646,54 +705,6 @@
} else {
constraints.maxHeight
}
-
- // Placement
- val positionedItems = MutableVector<LazyStaggeredGridPositionedItem>(
- capacity = measuredItems.sumOf { it.size }
- )
- while (measuredItems.any { it.isNotEmpty() }) {
- // find the next item to position
- val laneIndex = measuredItems.indexOfMinBy {
- it.firstOrNull()?.index ?: Int.MAX_VALUE
- }
- val item = measuredItems[laneIndex].removeFirst()
-
- if (item.lane != laneIndex) {
- continue
- }
-
- // todo(b/182882362): arrangement support
- val spanRange = SpanRange(item.lane, item.span)
- val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
- val crossAxisOffset =
- if (laneIndex == 0) {
- 0
- } else {
- resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
- }
-
- if (item.placeables.isEmpty()) {
- // nothing to place, ignore spacings
- continue
- }
-
- positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
- spanRange.forEach { lane ->
- itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
- }
- }
-
- debugLog {
- "| positioned: ${positionedItems.map { "${it.index} at ${it.offset}" }.toList()}"
- }
- debugLog {
- "========== MEASURE DONE ==========="
- }
-
- // todo: reverse layout support
-
- // End placement
-
// only scroll backward if the first item is not on screen or fully visible
val canScrollBackward = !(firstItemIndices[0] == 0 && firstItemOffsets[0] <= 0)
// only scroll forward if the last item is not on screen or fully visible
@@ -706,14 +717,22 @@
firstVisibleItemScrollOffsets = firstItemOffsets,
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
- positionedItems.forEach { item ->
+ extraItemsBefore.fastForEach { item ->
+ item.place(this)
+ }
+
+ positionedItems.fastForEach { item ->
+ item.place(this)
+ }
+
+ extraItemsAfter.fastForEach { item ->
item.place(this)
}
},
canScrollForward = canScrollForward,
canScrollBackward = canScrollBackward,
isVertical = isVertical,
- visibleItemsInfo = positionedItems.asMutableList(),
+ visibleItemsInfo = positionedItems,
totalItemsCount = itemCount,
viewportSize = IntSize(layoutWidth, layoutHeight),
viewportStartOffset = minOffset,
@@ -724,6 +743,68 @@
}
}
+private fun LazyStaggeredGridMeasureContext.calculatePositionedItems(
+ measuredItems: Array<ArrayDeque<LazyStaggeredGridMeasuredItem>>,
+ itemScrollOffsets: IntArray,
+): List<LazyStaggeredGridPositionedItem> {
+ val positionedItems = ArrayList<LazyStaggeredGridPositionedItem>(
+ measuredItems.sumOf { it.size }
+ )
+ while (measuredItems.any { it.isNotEmpty() }) {
+ // find the next item to position
+ val laneIndex = measuredItems.indexOfMinBy {
+ it.firstOrNull()?.index ?: Int.MAX_VALUE
+ }
+ val item = measuredItems[laneIndex].removeFirst()
+
+ if (item.lane != laneIndex) {
+ continue
+ }
+
+ // todo(b/182882362): arrangement support
+ val spanRange = SpanRange(item.lane, item.span)
+ val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
+ val crossAxisOffset =
+ if (laneIndex == 0) {
+ 0
+ } else {
+ resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
+ }
+
+ if (item.placeables.isEmpty()) {
+ // nothing to place, ignore spacings
+ continue
+ }
+
+ positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
+ spanRange.forEach { lane ->
+ itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
+ }
+ }
+ return positionedItems
+}
+
+private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
+ pinnedItems: List<LazyPinnedItem>,
+ position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem,
+ filter: (itemIndex: Int) -> Boolean
+): List<LazyStaggeredGridPositionedItem> {
+ var result: MutableList<LazyStaggeredGridPositionedItem>? = null
+
+ pinnedItems.fastForEach {
+ if (filter(it.index)) {
+ val spanRange = itemProvider.getSpanRange(it.index, 0)
+ if (result == null) {
+ result = mutableListOf()
+ }
+ val measuredItem = measuredItemProvider.getAndMeasure(it.index, spanRange)
+ result?.add(position(measuredItem))
+ }
+ }
+
+ return result ?: emptyList()
+}
+
@JvmInline
private value class SpanRange private constructor(val packedValue: Long) {
constructor(lane: Int, span: Int) : this(packInts(lane, lane + span))
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index bf95c2e..82c3e4b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -26,6 +26,7 @@
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
import androidx.compose.runtime.Composable
@@ -206,6 +207,11 @@
internal val mutableInteractionSource = MutableInteractionSource()
/**
+ * List of extra items to compose during the measure pass.
+ */
+ internal val pinnedItems = LazyPinnedItemContainer()
+
+ /**
* Call this function to take control of scrolling and gain the ability to send scroll events
* via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
* performed within a [scroll] block (even if they don't call any other methods on this
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 42befbe..d891805 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -139,7 +139,7 @@
const val DEMO_NAME = "demoname"
internal fun requireDemo(demoName: String, demo: Demo?) = requireNotNull(demo) {
- "No demo called \"$demoName\" could be found."
+ "No demo called \"$demoName\" could be found. Note substring matches are allowed."
}
}
}
@@ -239,20 +239,26 @@
restore = { restored ->
require(restored.isNotEmpty())
val backStack = restored.mapTo(mutableListOf()) {
- requireNotNull(findDemo(rootDemo, it))
+ requireNotNull(findDemo(rootDemo, it, exact = true))
}
val initial = backStack.removeAt(backStack.lastIndex)
Navigator(backDispatcher, launchActivityDemo, rootDemo, initial, backStack)
}
)
- fun findDemo(demo: Demo, title: String): Demo? {
- if (demo.title == title) {
- return demo
+ fun findDemo(demo: Demo, title: String, exact: Boolean = false): Demo? {
+ if (exact) {
+ if (demo.title == title) {
+ return demo
+ }
+ } else {
+ if (demo.title.contains(title)) {
+ return demo
+ }
}
if (demo is DemoCategory) {
demo.demos.forEach { child ->
- findDemo(child, title)
+ findDemo(child, title, exact)
?.let { return it }
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
index c17e736..a22a0342 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
@@ -16,10 +16,15 @@
package androidx.compose.material3
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
-import androidx.compose.ui.test.assertIsOff
-import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
@@ -31,6 +36,7 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import java.util.Calendar
+import java.util.Locale
import java.util.TimeZone
import org.junit.Rule
import org.junit.Test
@@ -58,7 +64,7 @@
}
// Select the 11th day of the displayed month is selected.
- rule.onNodeWithText("11").assertIsOn()
+ rule.onNodeWithText("11").assertIsSelected()
rule.onNodeWithText("May 11, 2010").assertExists()
}
@@ -78,7 +84,7 @@
rule.onNodeWithText(defaultHeadline).assertExists()
// Select the 27th day of the displayed month.
- rule.onNodeWithText("27").assertIsOff()
+ rule.onNodeWithText("27").assertIsNotSelected()
rule.onNodeWithText("27").performClick()
rule.runOnIdle {
@@ -93,7 +99,7 @@
rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
rule.onNodeWithText("Jan 27, 2019").assertExists()
- rule.onNodeWithText("27").assertIsOn()
+ rule.onNodeWithText("27").assertIsSelected()
}
@Test
@@ -136,7 +142,7 @@
}
rule.onNodeWithText("January 2019").performClick()
- rule.onNodeWithText("2019").assertIsOn()
+ rule.onNodeWithText("2019").assertIsSelected()
rule.onNodeWithText("2020").performClick()
// Select the 15th day of the displayed month in 2020.
rule.onAllNodesWithText("15").onFirst().performClick()
@@ -153,8 +159,8 @@
// Check that if the years are opened again, the last selected year is still marked as such
rule.onNodeWithText("January 2020").performClick()
- rule.onNodeWithText("2019").assertIsOff()
- rule.onNodeWithText("2020").assertIsOn()
+ rule.onNodeWithText("2019").assertIsNotSelected()
+ rule.onNodeWithText("2020").assertIsSelected()
}
@Test
@@ -366,6 +372,43 @@
}
}
+ @Test
+ fun defaultSemantics() {
+ val selectedDateInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+ val monthInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 1)
+ lateinit var expectedHeadlineStringFormat: String
+ rule.setMaterialContent(lightColorScheme()) {
+ // e.g. "Current selection: %1$s"
+ expectedHeadlineStringFormat = getString(Strings.DatePickerHeadlineDescription)
+ DatePicker(
+ datePickerState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedDateInUtcMillis,
+ initialDisplayedMonthMillis = monthInUtcMillis
+ )
+ )
+ }
+
+ val fullDateDescription = formatWithSkeleton(
+ selectedDateInUtcMillis,
+ DatePickerDefaults.YearMonthWeekdayDaySkeleton,
+ Locale.US
+ )
+
+ rule.onNodeWithContentDescription(label = "next", substring = true, ignoreCase = true)
+ .assert(expectValue(SemanticsProperties.Role, Role.Button))
+ rule.onNodeWithContentDescription(label = "previous", substring = true, ignoreCase = true)
+ .assert(expectValue(SemanticsProperties.Role, Role.Button))
+ rule.onNodeWithText("May 2010")
+ .assert(expectValue(SemanticsProperties.Role, Role.Button))
+ rule.onNodeWithText("11")
+ .assert(expectValue(SemanticsProperties.Role, Role.Button))
+ .assertContentDescriptionEquals(fullDateDescription)
+ rule.onNodeWithText("May 11, 2010")
+ .assertContentDescriptionEquals(
+ expectedHeadlineStringFormat.format(fullDateDescription)
+ )
+ }
+
// Returns the given date's day as milliseconds from epoch. The returned value is for the day's
// start on midnight.
private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
index cc8b1ea..ae58d4b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -35,9 +35,9 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import kotlinx.coroutines.launch
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -75,7 +75,7 @@
.assertWidthIsEqualTo(customWidth)
}
- @FlakyTest(bugId = 264907895)
+ @Ignore // b/264907895
@Test
fun plainTooltip_content_padding() {
rule.setMaterialContent(lightColorScheme()) {
@@ -94,7 +94,7 @@
.assertTopPositionInRootIsEqualTo(4.dp)
}
- @FlakyTest(bugId = 264887805)
+ @Ignore // b/264887805
@Test
fun plainTooltip_behavior() {
rule.setMaterialContent(lightColorScheme()) {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index f3ca92b..9059aad 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -50,6 +50,9 @@
Strings.DatePickerHeadline -> resources.getString(
androidx.compose.material3.R.string.date_picker_headline
)
+ Strings.DatePickerYearPickerPaneTitle -> resources.getString(
+ androidx.compose.material3.R.string.date_picker_year_picker_pane_title
+ )
Strings.DatePickerSwitchToYearSelection -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_year_selection
)
@@ -62,6 +65,15 @@
Strings.DatePickerSwitchToPreviousMonth -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_previous_month
)
+ Strings.DatePickerNavigateToYearDescription -> resources.getString(
+ androidx.compose.material3.R.string.date_picker_navigate_to_year_description
+ )
+ Strings.DatePickerHeadlineDescription -> resources.getString(
+ androidx.compose.material3.R.string.date_picker_headline_description
+ )
+ Strings.DatePickerNoSelectionDescription -> resources.getString(
+ androidx.compose.material3.R.string.date_picker_no_selection_description
+ )
else -> ""
}
}
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 9284a12..6949cde 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -32,7 +32,13 @@
<string name="date_picker_title">"Select date"</string>
<string name="date_picker_headline">"Selected date"</string>
<string name="date_picker_switch_to_year_selection">"Switch to selecting a year"</string>
- <string name="date_picker_switch_to_day_selection">"Switch to selecting a day"</string>
+ <string name="date_picker_switch_to_day_selection">
+ "Swipe to select a year, or tap to switch back to selecting a day"
+ </string>
<string name="date_picker_switch_to_next_month">"Change to next month"</string>
<string name="date_picker_switch_to_previous_month">"Change to previous month"</string>
+ <string name="date_picker_navigate_to_year_description">Navigate to year %1$s</string>
+ <string name="date_picker_headline_description">Current selection: %1$s</string>
+ <string name="date_picker_no_selection_description">None</string>
+ <string name="date_picker_year_picker_pane_title">Year picker visible</string>
</resources>
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 602bf70..850dbfc9 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
@@ -82,8 +82,17 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
@@ -385,11 +394,24 @@
date = state.selectedDate,
calendarModel = state.calendarModel
)
- if (formattedDate == null) {
- Text(getString(string = Strings.DatePickerHeadline), maxLines = 1)
- } else {
- Text(formattedDate, maxLines = 1)
- }
+ val verboseDateDescription = dateFormatter.formatDate(
+ date = state.selectedDate,
+ calendarModel = state.calendarModel,
+ forContentDescription = true
+ ) ?: getString(Strings.DatePickerNoSelectionDescription)
+
+ val headlineText = formattedDate ?: getString(string = Strings.DatePickerHeadline)
+ val headlineDescription =
+ getString(Strings.DatePickerHeadlineDescription).format(verboseDateDescription)
+
+ Text(
+ text = headlineText,
+ modifier = Modifier.semantics {
+ liveRegion = LiveRegionMode.Polite
+ contentDescription = headlineDescription
+ },
+ maxLines = 1
+ )
}
/**
@@ -645,10 +667,18 @@
internal fun formatDate(
date: CalendarDate?,
calendarModel: CalendarModel,
+ forContentDescription: Boolean = false,
locale: Locale = Locale.getDefault()
): String? {
if (date == null) return null
- return calendarModel.formatWithSkeleton(date, selectedDateSkeleton, locale)
+ return calendarModel.formatWithSkeleton(
+ date, if (forContentDescription) {
+ selectedDateDescriptionSkeleton
+ } else {
+ selectedDateSkeleton
+ },
+ locale
+ )
}
override fun equals(other: Any?): Boolean {
@@ -737,6 +767,7 @@
onDateSelected = onDateSelected,
datePickerState = datePickerState,
lazyListState = monthsListState,
+ dateFormatter = dateFormatter,
dateValidator = dateValidator,
colors = colors
)
@@ -747,7 +778,12 @@
enter = expandVertically() + fadeIn(initialAlpha = 0.6f),
exit = shrinkVertically() + fadeOut()
) {
- Column {
+ // Apply a paneTitle to make the screen reader focus on a relevant node after this
+ // column is hidden and disposed.
+ // TODO(b/186443263): Have the screen reader focus on a year in the list when the
+ // list is revealed.
+ val yearsPaneTitle = getString(Strings.DatePickerYearPickerPaneTitle)
+ Column(modifier = Modifier.semantics { paneTitle = yearsPaneTitle }) {
YearPicker(
// Keep the height the same as the monthly calendar + weekdays height, and
// take into account the thickness of the divider that will be composed
@@ -830,6 +866,7 @@
onDateSelected: (dateInMillis: Long) -> Unit,
datePickerState: DatePickerState,
lazyListState: LazyListState,
+ dateFormatter: DatePickerFormatter,
dateValidator: (Long) -> Boolean,
colors: DatePickerColors,
) {
@@ -841,6 +878,12 @@
)
}
LazyRow(
+ // Apply this to prevent the screen reader from scrolling to the next or previous month, and
+ // instead, traverse outside the Month composable when swiping from a focused first or last
+ // day of the month.
+ modifier = Modifier.semantics {
+ horizontalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+ },
state = lazyListState,
// TODO(b/264687693): replace with the framework's rememberSnapFlingBehavior(lazyListState)
// when promoted to stable
@@ -860,6 +903,7 @@
onDateSelected = onDateSelected,
today = today,
selectedDate = datePickerState.selectedDate,
+ dateFormatter = dateFormatter,
dateValidator = dateValidator,
colors = colors
)
@@ -946,6 +990,7 @@
today: CalendarDate,
selectedDate: CalendarDate?,
dateValidator: (Long) -> Boolean,
+ dateFormatter: DatePickerFormatter,
colors: DatePickerColors
) {
ProvideTextStyle(
@@ -982,16 +1027,29 @@
val dateInMillis = month.startUtcTimeMillis +
(dayNumber * MillisecondsIn24Hours)
Day(
- checked = dateInMillis == selectedDate?.utcTimeMillis,
- onCheckedChange = { onDateSelected(dateInMillis) },
+ modifier = Modifier.semantics { role = Role.Button },
+ selected = dateInMillis == selectedDate?.utcTimeMillis,
+ onClick = { onDateSelected(dateInMillis) },
animateChecked = true,
enabled = remember(dateInMillis) {
dateValidator.invoke(dateInMillis)
},
today = dateInMillis == today.utcTimeMillis,
- text = (dayNumber + 1).toLocalString(),
colors = colors
- )
+ ) {
+ Text(
+ text = (dayNumber + 1).toLocalString(),
+ modifier = Modifier.semantics {
+ contentDescription =
+ formatWithSkeleton(
+ dateInMillis,
+ dateFormatter.selectedDateDescriptionSkeleton,
+ Locale.getDefault()
+ )
+ },
+ textAlign = TextAlign.Center
+ )
+ }
}
cellsCount++
}
@@ -1004,18 +1062,19 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Day(
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier,
+ selected: Boolean,
+ onClick: () -> Unit,
animateChecked: Boolean,
enabled: Boolean,
today: Boolean,
- text: String,
- colors: DatePickerColors
+ colors: DatePickerColors,
+ content: @Composable () -> Unit
) {
Surface(
- checked = checked,
- onCheckedChange = onCheckedChange,
- modifier = Modifier
+ selected = selected,
+ onClick = onClick,
+ modifier = modifier
.minimumInteractiveComponentSize()
.requiredSize(
DatePickerModalTokens.DateStateLayerWidth,
@@ -1024,16 +1083,16 @@
enabled = enabled,
shape = DatePickerModalTokens.DateContainerShape.toShape(),
color = colors.dayContainerColor(
- selected = checked,
+ selected = selected,
enabled = enabled,
animate = animateChecked
).value,
contentColor = colors.dayContentColor(
today = today,
- selected = checked,
+ selected = selected,
enabled = enabled,
).value,
- border = if (today && !checked) {
+ border = if (today && !selected) {
BorderStroke(
DatePickerModalTokens.DateTodayContainerOutlineWidth,
colors.todayDateBorderColor
@@ -1043,10 +1102,7 @@
}
) {
Box(contentAlignment = Alignment.Center) {
- Text(
- text = text,
- textAlign = TextAlign.Center
- )
+ content()
}
}
}
@@ -1080,7 +1136,13 @@
}
LazyVerticalGrid(
columns = GridCells.Fixed(YearsInRow),
- modifier = modifier.background(containerColor),
+ modifier = modifier
+ .background(containerColor)
+ // Apply this to have the screen reader traverse outside the visible list of years
+ // and not scroll them by default.
+ .semantics {
+ verticalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+ },
state = lazyGridState,
horizontalArrangement = Arrangement.SpaceEvenly,
verticalArrangement = Arrangement.spacedBy(YearsVerticalPadding)
@@ -1088,21 +1150,27 @@
items(datePickerState.yearRange.count()) {
val selectedYear = it + datePickerState.yearRange.first
Year(
- checked = selectedYear == displayedYear,
+ modifier = Modifier
+ .requiredSize(
+ width = DatePickerModalTokens.SelectionYearContainerWidth,
+ height = DatePickerModalTokens.SelectionYearContainerHeight
+ )
+ .semantics {
+ role = Role.Button
+ },
+ selected = selectedYear == displayedYear,
currentYear = selectedYear == currentYear,
- onCheckedChange = { checked ->
- if (checked) {
- onYearSelected(selectedYear)
- }
- },
- modifier = Modifier.requiredSize(
- width = DatePickerModalTokens.SelectionYearContainerWidth,
- height = DatePickerModalTokens.SelectionYearContainerHeight
- ),
+ onClick = { onYearSelected(selectedYear) },
colors = colors
) {
+ val localizedYear = selectedYear.toLocalString()
+ val description =
+ getString(Strings.DatePickerNavigateToYearDescription).format(localizedYear)
Text(
- text = selectedYear.toLocalString(),
+ text = localizedYear,
+ modifier = Modifier.semantics {
+ contentDescription = description
+ },
textAlign = TextAlign.Center
)
}
@@ -1114,15 +1182,15 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Year(
- checked: Boolean,
- currentYear: Boolean,
- onCheckedChange: (Boolean) -> Unit,
modifier: Modifier,
+ selected: Boolean,
+ currentYear: Boolean,
+ onClick: () -> Unit,
colors: DatePickerColors,
content: @Composable () -> Unit
) {
- val border = remember(currentYear, checked) {
- if (currentYear && !checked) {
+ val border = remember(currentYear, selected) {
+ if (currentYear && !selected) {
// Use the day's spec to draw a border around the current year.
BorderStroke(
DatePickerModalTokens.DateTodayContainerOutlineWidth,
@@ -1133,14 +1201,14 @@
}
}
Surface(
- checked = checked,
- onCheckedChange = onCheckedChange,
+ selected = selected,
+ onClick = onClick,
modifier = modifier,
shape = DatePickerModalTokens.SelectionYearStateLayerShape.toShape(),
- color = colors.yearContainerColor(selected = checked).value,
+ color = colors.yearContainerColor(selected = selected).value,
contentColor = colors.yearContentColor(
currentYear = currentYear,
- selected = checked
+ selected = selected
).value,
border = border
) {
@@ -1180,7 +1248,13 @@
onClick = onYearPickerButtonClicked,
expanded = yearPickerVisible
) {
- Text(yearPickerText)
+ Text(text = yearPickerText,
+ modifier = Modifier.semantics {
+ // Make the screen reader read out updates to the menu button text as the user
+ // navigates the arrows or scrolls to change the displayed month.
+ liveRegion = LiveRegionMode.Polite
+ contentDescription = yearPickerText
+ })
}
// Show arrows for traversing months (only visible when the year selection is off)
if (!yearPickerVisible) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index b4a1da0..0d58497 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -38,10 +38,14 @@
val SuggestionsAvailable = Strings(12)
val DatePickerTitle = Strings(13)
val DatePickerHeadline = Strings(14)
- val DatePickerSwitchToYearSelection = Strings(15)
- val DatePickerSwitchToDaySelection = Strings(16)
- val DatePickerSwitchToNextMonth = Strings(17)
- val DatePickerSwitchToPreviousMonth = Strings(18)
+ val DatePickerYearPickerPaneTitle = Strings(15)
+ val DatePickerSwitchToYearSelection = Strings(16)
+ val DatePickerSwitchToDaySelection = Strings(17)
+ val DatePickerSwitchToNextMonth = Strings(18)
+ val DatePickerSwitchToPreviousMonth = Strings(19)
+ val DatePickerNavigateToYearDescription = Strings(20)
+ val DatePickerHeadlineDescription = Strings(21)
+ val DatePickerNoSelectionDescription = Strings(22)
}
}
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index e2d8ba4..398da25 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -35,10 +35,15 @@
Strings.SuggestionsAvailable -> "Suggestions below"
Strings.DatePickerTitle -> "Select date"
Strings.DatePickerHeadline -> "Selected date"
+ Strings.DatePickerYearPickerPaneTitle -> "Year picker visible"
Strings.DatePickerSwitchToYearSelection -> "Switch to selecting a year"
- Strings.DatePickerSwitchToDaySelection -> "Switch to selecting a day"
+ Strings.DatePickerSwitchToDaySelection -> "Swipe to select a year, or tap to switch " +
+ "back to selecting a day"
Strings.DatePickerSwitchToNextMonth -> "Change to next month"
Strings.DatePickerSwitchToPreviousMonth -> "Change to previous month"
+ Strings.DatePickerNavigateToYearDescription -> "Navigate to year %1$"
+ Strings.DatePickerHeadlineDescription -> "Current selection: %1$"
+ Strings.DatePickerNoSelectionDescription -> "None"
else -> ""
}
}
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index f9e2d8a..4d3f223 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -7,7 +7,7 @@
}
@androidx.compose.ui.test.ExperimentalTestApi public abstract class AndroidComposeUiTestEnvironment<A extends androidx.activity.ComponentActivity> {
- ctor public AndroidComposeUiTestEnvironment();
+ ctor public AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext);
method protected abstract A? getActivity();
method public final androidx.compose.ui.test.AndroidComposeUiTest<A> getTest();
method public final <R> R! runTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends R> block);
@@ -31,14 +31,14 @@
}
public final class ComposeUiTestKt {
- method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
}
public final class ComposeUiTest_androidKt {
- method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(kotlin.jvm.functions.Function0<? extends A> activityProvider);
- method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
- method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
- method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function0<? extends A> activityProvider);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
method @androidx.compose.ui.test.ExperimentalTestApi public static void runEmptyComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
}
@@ -54,6 +54,7 @@
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeContentTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
+ ctor @androidx.compose.ui.test.ExperimentalTestApi public AndroidComposeTestRule(R activityRule, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public A getActivity();
@@ -82,9 +83,13 @@
public final class AndroidComposeTestRule_androidKt {
method public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext);
method public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule();
+ method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
method public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule();
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule(kotlin.coroutines.CoroutineContext effectContext);
method public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule();
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
}
public final class AndroidSynchronization_androidKt {
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index d4b8514..5e4501d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -141,6 +141,15 @@
implementation(libs.truth)
implementation(libs.skiko)
}
+
+ desktopTest.dependencies {
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.kotlinTest)
+ implementation(libs.skikoCurrentOs)
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ }
}
}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
index 8d2b530..f820217 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -17,11 +17,12 @@
package androidx.compose.ui.test
import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -41,22 +42,34 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.espresso.IdlingPolicies
import androidx.test.espresso.IdlingPolicy
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -64,6 +77,18 @@
class ComposeUiTestTest {
private var idlingPolicy: IdlingPolicy? = null
+ private lateinit var testDescription: Description
+
+ /**
+ * Records the current [testDescription] for tests that need to invoke the compose test rule
+ * directly.
+ */
+ @get:Rule
+ val testWatcher = object : TestWatcher() {
+ override fun starting(description: Description) {
+ testDescription = description
+ }
+ }
@Before
fun setup() {
@@ -130,7 +155,12 @@
setContent {
val offset = animateFloatAsState(target)
Box(Modifier.fillMaxSize()) {
- Box(Modifier.size(10.dp).offset(x = offset.value.dp).testTag("box"))
+ Box(
+ Modifier
+ .size(10.dp)
+ .offset(x = offset.value.dp)
+ .testTag("box")
+ )
}
}
onNodeWithTag("box").assertLeftPositionInRootIsEqualTo(0.dp)
@@ -158,13 +188,20 @@
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Box(Modifier.fillMaxSize()) {
Column(
- Modifier.requiredSize(200.dp).verticalScroll(
- scrollState,
- flingBehavior = flingBehavior
- ).testTag("list")
+ Modifier
+ .requiredSize(200.dp)
+ .verticalScroll(
+ scrollState,
+ flingBehavior = flingBehavior
+ )
+ .testTag("list")
) {
repeat(n) {
- Spacer(Modifier.fillMaxWidth().height(30.dp))
+ Spacer(
+ Modifier
+ .fillMaxWidth()
+ .height(30.dp)
+ )
}
}
}
@@ -217,4 +254,124 @@
fun getActivityTest() = runAndroidComposeUiTest<ComponentActivity> {
assertThat(activity).isNotNull()
}
+
+ @Test
+ fun effectContextPropagatedToComposition_runComposeUiTest() {
+ val testElement = TestCoroutineContextElement()
+ runComposeUiTest(effectContext = testElement) {
+ lateinit var compositionScope: CoroutineScope
+ setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+
+ runOnIdle {
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+ }
+ }
+
+ @Test
+ fun effectContextPropagatedToComposition_createComposeRule() {
+ val testElement = TestCoroutineContextElement()
+ lateinit var compositionScope: CoroutineScope
+ val rule = createComposeRule(testElement)
+ val baseStatement = object : Statement() {
+ override fun evaluate() {
+ rule.setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+ rule.waitForIdle()
+ }
+ }
+ rule.apply(baseStatement, testDescription)
+ .evaluate()
+
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+
+ @Test
+ fun effectContextPropagatedToComposition_createAndroidComposeRule() {
+ val testElement = TestCoroutineContextElement()
+ lateinit var compositionScope: CoroutineScope
+ val rule = createAndroidComposeRule<ComponentActivity>(testElement)
+ val baseStatement = object : Statement() {
+ override fun evaluate() {
+ rule.setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+ rule.waitForIdle()
+ }
+ }
+ rule.apply(baseStatement, testDescription)
+ .evaluate()
+
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+
+ @Test
+ fun effectContextPropagatedToComposition_createEmptyComposeRule() {
+ val testElement = TestCoroutineContextElement()
+ lateinit var compositionScope: CoroutineScope
+ val composeRule = createEmptyComposeRule(testElement)
+ val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+ val baseStatement = object : Statement() {
+ override fun evaluate() {
+ activityRule.scenario.onActivity {
+ it.setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+ }
+ composeRule.waitForIdle()
+ }
+ }
+ activityRule.apply(composeRule.apply(baseStatement, testDescription), testDescription)
+ .evaluate()
+
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+
+ @Test
+ fun motionDurationScale_defaultValue() = runComposeUiTest {
+ var lastRecordedMotionDurationScale: Float? = null
+ setContent {
+ val context = rememberCoroutineScope().coroutineContext
+ lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+ }
+
+ runOnIdle {
+ assertThat(lastRecordedMotionDurationScale).isNull()
+ }
+ }
+
+ @Test
+ fun motionDurationScale_propagatedToCoroutines() {
+ val motionDurationScale = object : MotionDurationScale {
+ override val scaleFactor: Float get() = 0f
+ }
+ runComposeUiTest(effectContext = motionDurationScale) {
+ var lastRecordedMotionDurationScale: Float? = null
+ setContent {
+ val context = rememberCoroutineScope().coroutineContext
+ lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+ }
+
+ runOnIdle {
+ assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+ }
+ }
+ }
+
+ private class TestCoroutineContextElement : CoroutineContext.Element {
+ override val key: CoroutineContext.Key<*> get() = Key
+
+ companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+ }
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 5453449..7b88012 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -48,6 +48,8 @@
import androidx.compose.ui.unit.Density
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -59,8 +61,8 @@
import kotlinx.coroutines.test.runTest
@ExperimentalTestApi
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
- runAndroidComposeUiTest(ComponentActivity::class.java, block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+ runAndroidComposeUiTest(ComponentActivity::class.java, effectContext, block)
}
/**
@@ -71,12 +73,16 @@
*
* @param A The Activity type to be launched, which typically (but not necessarily) hosts the
* Compose content
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
*/
@ExperimentalTestApi
inline fun <reified A : ComponentActivity> runAndroidComposeUiTest(
+ effectContext: CoroutineContext = EmptyCoroutineContext,
noinline block: AndroidComposeUiTest<A>.() -> Unit
) {
- runAndroidComposeUiTest(A::class.java, block)
+ runAndroidComposeUiTest(A::class.java, effectContext, block)
}
/**
@@ -87,16 +93,21 @@
*
* @param A The Activity type to be launched, which typically (but not necessarily) hosts the
* Compose content
+ * @param activityClass The [Class] of the Activity type to be launched, corresponding to [A].
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
*/
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityClass: Class<A>,
+ effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
// Don't start the scenario now, wait until we're inside runTest { },
// in case the Activity's onCreate/Start/Resume calls setContent
var scenario: ActivityScenario<A>? = null
- val environment = AndroidComposeUiTestEnvironment {
+ val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
@@ -195,13 +206,16 @@
* @param activityProvider A lambda that should return the current Activity instance of type [A],
* if it is available. If it is not available, it should return `null`.
* @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
*/
@ExperimentalTestApi
inline fun <A : ComponentActivity> AndroidComposeUiTestEnvironment(
+ effectContext: CoroutineContext = EmptyCoroutineContext,
crossinline activityProvider: () -> A?
): AndroidComposeUiTestEnvironment<A> {
- return object : AndroidComposeUiTestEnvironment<A>() {
+ return object : AndroidComposeUiTestEnvironment<A>(effectContext) {
override val activity: A?
get() = activityProvider.invoke()
}
@@ -213,11 +227,15 @@
* as they require that the environment has been set up.
*
* @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
*/
@ExperimentalTestApi
@OptIn(InternalTestApi::class, ExperimentalCoroutinesApi::class, ExperimentalComposeUiApi::class)
-abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity> {
+abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity>(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+) {
private val idlingResourceRegistry = IdlingResourceRegistry()
internal val composeRootRegistry = ComposeRootRegistry()
@@ -259,8 +277,12 @@
}
}
recomposerCoroutineScope = CoroutineScope(
- recomposerContinuationInterceptor + frameClock + infiniteAnimationPolicy +
- coroutineExceptionHandler + Job()
+ effectContext +
+ recomposerContinuationInterceptor +
+ frameClock +
+ infiniteAnimationPolicy +
+ coroutineExceptionHandler +
+ Job()
)
recomposer = Recomposer(recomposerCoroutineScope.coroutineContext)
composeIdlingResource = ComposeIdlingResource(
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 887eb84..ec2ea21 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -27,6 +27,8 @@
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.rules.ActivityScenarioRule
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -34,6 +36,10 @@
actual fun createComposeRule(): ComposeContentTestRule =
createAndroidComposeRule<ComponentActivity>()
+@ExperimentalTestApi
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+ createAndroidComposeRule<ComponentActivity>(effectContext)
+
/**
* Factory method to provide android specific implementation of [createComposeRule], for a given
* activity class type [A].
@@ -51,12 +57,38 @@
*/
inline fun <reified A : ComponentActivity> createAndroidComposeRule():
AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
- // TODO(b/138993381): By launching custom activities we are losing control over what content is
- // already there. This is issue in case the user already set some compose content and decides
- // to set it again via our API. In such case we won't be able to dispose the old composition.
- // Other option would be to provide a smaller interface that does not expose these methods.
- return createAndroidComposeRule(A::class.java)
- }
+ // TODO(b/138993381): By launching custom activities we are losing control over what content is
+ // already there. This is issue in case the user already set some compose content and decides
+ // to set it again via our API. In such case we won't be able to dispose the old composition.
+ // Other option would be to provide a smaller interface that does not expose these methods.
+ return createAndroidComposeRule(A::class.java)
+}
+
+/**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * activity class type [A].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+inline fun <reified A : ComponentActivity> createAndroidComposeRule(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
+ // TODO(b/138993381): By launching custom activities we are losing control over what content is
+ // already there. This is issue in case the user already set some compose content and decides
+ // to set it again via our API. In such case we won't be able to dispose the old composition.
+ // Other option would be to provide a smaller interface that does not expose these methods.
+ return createAndroidComposeRule(A::class.java, effectContext)
+}
/**
* Factory method to provide android specific implementation of [createComposeRule], for a given
@@ -81,6 +113,31 @@
)
/**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * [activityClass].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+fun <A : ComponentActivity> createAndroidComposeRule(
+ activityClass: Class<A>,
+ effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
+ activityRule = ActivityScenarioRule(activityClass),
+ activityProvider = ::getActivityFromTestRule,
+ effectContext = effectContext
+)
+
+/**
* Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
* host for you in which you can set content.
*
@@ -103,6 +160,35 @@
}
)
+/**
+ * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
+ * host for you in which you can set content.
+ *
+ * This method is useful for tests that need to create their own compose host during the test.
+ * The returned test rule will not create a host, and consequently does not provide a
+ * `setContent` method. To set content in tests using this rule, use the appropriate `setContent`
+ * methods from your compose host.
+ *
+ * A typical use case on Android is when the test needs to launch an Activity (the compose host)
+ * after one or more dependencies have been injected.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+fun createEmptyComposeRule(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeTestRule = AndroidComposeTestRule<TestRule, ComponentActivity>(
+ activityRule = TestRule { base, _ -> base },
+ effectContext = effectContext,
+ activityProvider = {
+ error(
+ "createEmptyComposeRule() does not provide an Activity to set Compose content in." +
+ " Launch and use the Activity yourself, or use createAndroidComposeRule()."
+ )
+ }
+)
+
@OptIn(ExperimentalTestApi::class)
class AndroidComposeTestRule<R : TestRule, A : ComponentActivity> private constructor(
val activityRule: R,
@@ -128,9 +214,43 @@
* @param activityRule Test rule to use to launch the Activity.
* @param activityProvider Function to retrieve the Activity from the given [activityRule].
*/
- constructor(activityRule: R, activityProvider: (R) -> A) : this(
+ constructor(
+ activityRule: R,
+ activityProvider: (R) -> A
+ ) : this(
+ activityRule = activityRule,
+ effectContext = EmptyCoroutineContext,
+ activityProvider = activityProvider,
+ )
+
+ /**
+ * Android specific implementation of [ComposeContentTestRule], where compose content is hosted
+ * by an Activity.
+ *
+ * The Activity is normally launched by the given [activityRule] before the test starts, but it
+ * is possible to pass a test rule that chooses to launch an Activity on a later time. The
+ * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can be
+ * thought of as a getter for the Activity on the [activityRule]. If you use an [activityRule]
+ * that launches an Activity on a later time, you should make sure that the Activity is launched
+ * by the time or while the [activityProvider] is called.
+ *
+ * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity
+ * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control
+ * and monitor the compose content.
+ *
+ * @param activityRule Test rule to use to launch the Activity.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param activityProvider Function to retrieve the Activity from the given [activityRule].
+ */
+ @ExperimentalTestApi
+ constructor(
+ activityRule: R,
+ effectContext: CoroutineContext = EmptyCoroutineContext,
+ activityProvider: (R) -> A,
+ ) : this(
activityRule,
- AndroidComposeUiTestEnvironment { activityProvider(activityRule) }
+ AndroidComposeUiTestEnvironment(effectContext) { activityProvider(activityRule) },
)
/**
diff --git a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 7fd77b0..e7900ea 100644
--- a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -18,6 +18,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
/**
* Sets up the test environment, runs the given [test][block] and then tears down the test
@@ -34,9 +36,16 @@
* launch the host from within the test lambda as well depends on the platform.
*
* Keeping a reference to the [ComposeUiTest] outside of this function is an error.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
*/
@ExperimentalTestApi
-expect fun runComposeUiTest(block: ComposeUiTest.() -> Unit)
+expect fun runComposeUiTest(
+ effectContext: CoroutineContext = EmptyCoroutineContext,
+ block: ComposeUiTest.() -> Unit
+)
/**
* A test environment that allows you to test and control composables, either in isolation or in
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index 84471eb..159e7d1 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -30,6 +30,8 @@
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -40,14 +42,20 @@
@ExperimentalTestApi
@OptIn(InternalTestApi::class)
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
- DesktopComposeUiTest().runTest(block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+ DesktopComposeUiTest(effectContext).runTest(block)
}
+/**
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
@InternalTestApi
@ExperimentalTestApi
@OptIn(ExperimentalComposeUiApi::class, ExperimentalCoroutinesApi::class)
-class DesktopComposeUiTest : ComposeUiTest {
+class DesktopComposeUiTest(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+) : ComposeUiTest {
override val density = Density(1f, 1f)
@@ -65,7 +73,11 @@
}
}
private val coroutineContext =
- coroutineDispatcher + uncaughtExceptionHandler + infiniteAnimationPolicy
+ effectContext +
+ coroutineDispatcher +
+ uncaughtExceptionHandler +
+ infiniteAnimationPolicy
+
private val surface = Surface.makeRasterN32Premul(1024, 768)
lateinit var scene: ComposeScene
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
index e9933a0..f9ca8ad 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
@@ -27,12 +27,19 @@
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(InternalTestApi::class)
actual fun createComposeRule(): ComposeContentTestRule = DesktopComposeTestRule()
+@ExperimentalTestApi
+@OptIn(InternalTestApi::class)
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+ DesktopComposeTestRule(effectContext)
+
@InternalTestApi
@OptIn(ExperimentalTestApi::class)
class DesktopComposeTestRule private constructor(
@@ -41,6 +48,11 @@
constructor() : this(DesktopComposeUiTest())
+ @ExperimentalTestApi
+ constructor(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+ ) : this(DesktopComposeUiTest(effectContext))
+
var scene: ComposeScene
get() = composeTest.scene
set(value) {
diff --git a/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
new file mode 100644
index 0000000..aef2e84
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.test.junit4.createComposeRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class ComposeUiTestTest {
+
+ private lateinit var testDescription: Description
+
+ /**
+ * Records the current [testDescription] for tests that need to invoke the compose test rule
+ * directly.
+ */
+ @get:Rule
+ val testWatcher = object : TestWatcher() {
+ override fun starting(description: Description) {
+ testDescription = description
+ }
+ }
+
+ @Test
+ fun effectContextPropagatedToComposition_runComposeUiTest() {
+ val testElement = TestCoroutineContextElement()
+ runComposeUiTest(effectContext = testElement) {
+ lateinit var compositionScope: CoroutineScope
+ setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+
+ runOnIdle {
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+ }
+ }
+
+ @Test
+ fun effectContextPropagatedToComposition_createComposeRule() {
+ val testElement = TestCoroutineContextElement()
+ lateinit var compositionScope: CoroutineScope
+ val rule = createComposeRule(testElement)
+ val baseStatement = object : Statement() {
+ override fun evaluate() {
+ rule.setContent {
+ compositionScope = rememberCoroutineScope()
+ }
+ rule.waitForIdle()
+ }
+ }
+ rule.apply(baseStatement, testDescription)
+ .evaluate()
+
+ val elementFromComposition =
+ compositionScope.coroutineContext[TestCoroutineContextElement]
+ assertThat(elementFromComposition).isSameInstanceAs(testElement)
+ }
+
+ @Test
+ fun motionDurationScale_defaultValue() = runComposeUiTest {
+ var lastRecordedMotionDurationScale: Float? = null
+ setContent {
+ val context = rememberCoroutineScope().coroutineContext
+ lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+ }
+
+ runOnIdle {
+ assertThat(lastRecordedMotionDurationScale).isNull()
+ }
+ }
+
+ @Test
+ fun motionDurationScale_propagatedToCoroutines() {
+ val motionDurationScale = object : MotionDurationScale {
+ override val scaleFactor: Float get() = 0f
+ }
+ runComposeUiTest(effectContext = motionDurationScale) {
+ var lastRecordedMotionDurationScale: Float? = null
+ setContent {
+ val context = rememberCoroutineScope().coroutineContext
+ lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+ }
+
+ runOnIdle {
+ assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+ }
+ }
+ }
+
+ private class TestCoroutineContextElement : CoroutineContext.Element {
+ override val key: CoroutineContext.Key<*> get() = Key
+
+ companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
index 54e9aa7..b0d353a 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
@@ -18,12 +18,14 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.MainTestClock
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.rules.TestRule
-import kotlin.jvm.JvmDefaultWithCompatibility
/**
* A [TestRule] that allows you to test and control composables and applications using Compose.
@@ -179,3 +181,24 @@
* launched, see [createAndroidComposeRule].
*/
expect fun createComposeRule(): ComposeContentTestRule
+
+/**
+ * Factory method to provide an implementation of [ComposeContentTestRule].
+ *
+ * This method is useful for tests in compose libraries where it is irrelevant where the compose
+ * content is hosted (e.g. an Activity on Android). Such tests typically set compose content
+ * themselves via [setContent][ComposeContentTestRule.setContent] and only instrument and assert
+ * that content.
+ *
+ * For Android this will use the default Activity (android.app.Activity). You need to add a
+ * reference to this activity into the manifest file of the corresponding tests (usually in
+ * androidTest/AndroidManifest.xml). If your Android test requires a specific Activity to be
+ * launched, see [createAndroidComposeRule].
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+expect fun createComposeRule(
+ effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeContentTestRule
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index df13e31..c265249 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -307,7 +307,6 @@
method public int getEmojiSupportMatch();
method @Deprecated public boolean getIncludeFontPadding();
method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
- method public void setEmojiSupportMatch(int);
property public final int emojiSupportMatch;
property @Deprecated public final boolean includeFontPadding;
field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index b27caf4..04f88e9 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -328,7 +328,6 @@
method public int getEmojiSupportMatch();
method @Deprecated public boolean getIncludeFontPadding();
method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
- method public void setEmojiSupportMatch(int);
property public final int emojiSupportMatch;
property @Deprecated public final boolean includeFontPadding;
field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index df13e31..c265249 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -307,7 +307,6 @@
method public int getEmojiSupportMatch();
method @Deprecated public boolean getIncludeFontPadding();
method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
- method public void setEmojiSupportMatch(int);
property public final int emojiSupportMatch;
property @Deprecated public final boolean includeFontPadding;
field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
index 9e35c4f..88efda3 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
@@ -32,6 +32,12 @@
*/
actual val paragraphStyle: PlatformParagraphStyle?
+ /**
+ * Convenience constructor for when you already have a [spanStyle] and [paragraphStyle].
+ *
+ * @param spanStyle platform specific span styling
+ * @param paragraphStyle platform specific paragraph styling
+ */
constructor(
spanStyle: PlatformSpanStyle?,
paragraphStyle: PlatformParagraphStyle?
@@ -70,7 +76,11 @@
)
/**
- * Set EmojiMatchSupport on [PlatformParagraphStyle]
+ * [EmojiSupportMatch] allows you to control emoji support replacement behavior.
+ *
+ * You can disable emoji support matches by passing [EmojiSupportMatch.None]
+ *
+ * @param emojiSupportMatch configuration for emoji support match and replacement
*/
constructor(
emojiSupportMatch: EmojiSupportMatch
@@ -118,17 +128,46 @@
PlatformParagraphStyle()
}
+ /**
+ * Include extra space beyond font ascent and descent.
+ *
+ * Enables turning on and off for Android [includeFontPadding](https://developer.android.com/reference/android/text/StaticLayout.Builder#setIncludePad(boolean)).
+ *
+ * includeFontPadding was added to Android in order to prevent clipping issues on tall scripts.
+ * However that issue has been fixed since Android 28. Jetpack Compose backports the fix for
+ * Android versions prior to Android 28. Therefore the original reason why includeFontPadding
+ * was needed in invalid on Compose.
+ *
+ * This configuration was added for migration of the apps in case some code or design was
+ * relying includeFontPadding=true behavior and will be removed.
+ */
@Deprecated("Sets includeFontPadding parameter for transitioning. Will be removed.")
val includeFontPadding: Boolean
- var emojiSupportMatch: EmojiSupportMatch
+ /**
+ * When to replace emoji with support emoji using androidx.emoji2.
+ *
+ * This is only available on Android.
+ */
+ val emojiSupportMatch: EmojiSupportMatch
+ /**
+ * Represents platform specific text flags
+ *
+ * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+ */
@Deprecated("Provides configuration options for behavior compatibility.")
constructor(includeFontPadding: Boolean = DefaultIncludeFontPadding) {
this.includeFontPadding = includeFontPadding
this.emojiSupportMatch = EmojiSupportMatch.Default
}
+ /**
+ * Represents platform specific text flags
+ *
+ * @param emojiSupportMatch control emoji support matches on Android
+ * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+ */
@Deprecated("Provides configuration options for behavior compatibility.")
constructor(
emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default,
@@ -138,11 +177,19 @@
this.emojiSupportMatch = emojiSupportMatch
}
+ /**
+ * Represents platform specific text flags.
+ *
+ * @param emojiSupportMatch control emoji support matches on Android
+ */
constructor(emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default) {
this.includeFontPadding = DefaultIncludeFontPadding
this.emojiSupportMatch = emojiSupportMatch
}
+ /**
+ * Default platform paragraph style
+ */
constructor() : this(
includeFontPadding = DefaultIncludeFontPadding,
emojiSupportMatch = EmojiSupportMatch.Default
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index db53f5c..fb3978f 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -291,6 +291,9 @@
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/compose/ui/ui"
namespace "androidx.compose.ui"
+ // namespace has to be unique, but default androidx.compose.ui.test package is taken by
+ // the androidx.compose.ui:ui-test library
+ testNamespace "androidx.compose.ui.tests"
}
// Diagnostics for b/188565660
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
index b4aa669..6696cb1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
@@ -22,12 +22,12 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.AtLeastSize
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.paint
import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
class VectorInvalidationTestCase() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 8208a50..eff49cc 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -48,10 +48,10 @@
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -59,20 +59,21 @@
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.ImageVectorCache
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.R
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
+import androidx.compose.ui.unit.Density
+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 java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -80,8 +81,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@MediumTest
@RunWith(AndroidJUnit4::class)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
index 01c9518..4a724a3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.graphics.vector.VectorNode
import androidx.compose.ui.graphics.vector.VectorPath
import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
index 2779885..1b702f2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
@@ -17,9 +17,9 @@
package androidx.compose.ui.platform
import android.view.ViewGroup
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.junit.Assert.assertFalse
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
index e0ec921..cca5c26 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
@@ -19,8 +19,8 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
index 3703ff2..a503656 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
@@ -18,8 +18,8 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
index 4b67de4e..9751449 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
@@ -19,8 +19,8 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 98804c8..49fc678f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -61,11 +61,11 @@
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.findViewTreeCompositionContext
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index b203206..0fe1ac4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -53,12 +53,12 @@
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
index 73c9f5e..ea089f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
@@ -33,7 +33,6 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -41,6 +40,7 @@
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
index 755df89..5392884 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
@@ -44,7 +44,7 @@
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.coordinatorlayout.widget.CoordinatorLayout
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index 2ea1689..67a83e3f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -24,11 +24,11 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.round
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
index 78bd15c..40f2199 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
@@ -25,8 +25,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.Velocity
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
index 878ccca..a9b7904 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
import androidx.customview.poolingcontainer.callPoolingContainerOnRelease
import androidx.customview.poolingcontainer.isPoolingContainer
import androidx.test.ext.junit.rules.ActivityScenarioRule
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
index 40cfb6e..2fe23c8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
@@ -24,6 +24,12 @@
* the motion will end in the next frame callback. Otherwise, the duration [scaleFactor] will be
* used as a multiplier to scale the duration of the motion. The larger the scale, the longer the
* motion will take to finish, and therefore the slower it will be perceived.
+ *
+ * ## Testing
+ *
+ * To control the motion duration scale in tests, create an implementation of this interface and
+ * pass it to the `effectContext` parameter either where you call `runComposeUiTest` or where you
+ * create your test rule.
*/
@Stable
interface MotionDurationScale : CoroutineContext.Element {
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 0182e04..f80b976 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -411,9 +411,6 @@
public final class MotionDragHandlerKt {
}
- @kotlin.DslMarker public @interface MotionDslScope {
- }
-
public interface MotionItemsProvider {
method public int count();
method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 05f48b7..b9c3d1e 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -586,9 +586,6 @@
public final class MotionDragHandlerKt {
}
- @kotlin.DslMarker public @interface MotionDslScope {
- }
-
public interface MotionItemsProvider {
method public int count();
method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
@@ -597,9 +594,6 @@
}
public final class MotionKt {
- method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static void Motion(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.MotionScope,kotlin.Unit> rememberMotionContent(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static java.util.List<kotlin.jvm.functions.Function2<androidx.constraintlayout.compose.MotionScope,java.lang.Integer,kotlin.Unit>> rememberMotionListItems(int count, kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit> content);
}
public enum MotionLayoutDebugFlags {
@@ -734,12 +728,6 @@
method @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
}
- @androidx.constraintlayout.compose.ExperimentalMotionApi @androidx.constraintlayout.compose.MotionDslScope public final class MotionScope implements androidx.compose.ui.layout.LookaheadLayoutScope {
- ctor public MotionScope(androidx.compose.ui.layout.LookaheadLayoutScope lookaheadLayoutScope);
- method @androidx.compose.runtime.Composable public void emit(java.util.List<? extends kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit>>);
- method public androidx.compose.ui.Modifier motion(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional boolean ignoreAxisChanges, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionModifierScope,kotlin.Unit> motionDescription);
- }
-
@androidx.constraintlayout.compose.ExperimentalMotionApi public final class OnSwipe {
ctor public OnSwipe(androidx.constraintlayout.compose.ConstrainedLayoutReference anchor, androidx.constraintlayout.compose.SwipeSide side, androidx.constraintlayout.compose.SwipeDirection direction, optional float dragScale, optional float dragThreshold, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo, optional androidx.constraintlayout.compose.SwipeTouchUp onTouchUp, optional androidx.constraintlayout.compose.SwipeMode mode);
method public androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index d7d773c..c7ccd73 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -528,9 +528,6 @@
method public androidx.constraintlayout.compose.MotionDragState onDragEnd(long velocity);
}
- @kotlin.DslMarker public @interface MotionDslScope {
- }
-
public interface MotionItemsProvider {
method public int count();
method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
index 686ac0b3..662566b 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
@@ -93,7 +93,7 @@
@ExperimentalMotionApi
@OptIn(ExperimentalComposeUiApi::class)
@Composable
-fun Motion(
+private fun Motion(
modifier: Modifier = Modifier,
content: @Composable MotionScope.() -> Unit
) {
@@ -125,7 +125,7 @@
}
@DslMarker
-annotation class MotionDslScope
+private annotation class MotionDslScope
/**
* Scope for the [Motion] Composable.
@@ -136,7 +136,7 @@
@ExperimentalMotionApi
@MotionDslScope
@OptIn(ExperimentalComposeUiApi::class)
-class MotionScope(
+private class MotionScope(
lookaheadLayoutScope: LookaheadLayoutScope
) : LookaheadLayoutScope by lookaheadLayoutScope {
private var nextId: Int = 1000
@@ -360,7 +360,7 @@
*/
@ExperimentalMotionApi
@Composable
-fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
+private fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
@Composable MotionScope.() -> Unit {
return remember {
movableContentOf(content)
@@ -376,7 +376,7 @@
*/
@ExperimentalMotionApi
@Composable
-fun rememberMotionListItems(
+private fun rememberMotionListItems(
count: Int,
content: @Composable MotionScope.(index: Int) -> Unit
): List<@Composable MotionScope.(index: Int) -> Unit> {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
index 2cdabc5..72db469 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
@@ -1094,31 +1094,16 @@
flow.setWrapMode(State.Wrap.getValueByString(wrapValue));
break;
case "vGap":
- String vGapValue = element.get(param).content();
- try {
- int value = Integer.parseInt(vGapValue);
- flow.setVerticalGap(value);
- } catch(NumberFormatException e) {
-
- }
+ int vGapValue = element.get(param).getInt();
+ flow.setVerticalGap(vGapValue);
break;
case "hGap":
- String hGapValue = element.get(param).content();
- try {
- int value = Integer.parseInt(hGapValue);
- flow.setHorizontalGap(value);
- } catch(NumberFormatException e) {
-
- }
+ int hGapValue = element.get(param).getInt();
+ flow.setHorizontalGap(hGapValue);
break;
case "maxElement":
- String maxElementValue = element.get(param).content();
- try {
- int value = Integer.parseInt(maxElementValue);
- flow.setMaxElementsWrap(value);
- } catch(NumberFormatException e) {
-
- }
+ int maxElementValue = element.get(param).getInt();
+ flow.setMaxElementsWrap(maxElementValue);
break;
case "padding":
CLElement paddingObject = element.get(param);
diff --git a/constraintlayout/constraintlayout/api/current.txt b/constraintlayout/constraintlayout/api/current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/current.txt
+++ b/constraintlayout/constraintlayout/api/current.txt
@@ -120,6 +120,20 @@
field protected float mComputedMinY;
}
+ public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public LogJson(android.content.Context);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+ method public void periodicStart();
+ method public void periodicStop();
+ method public void setDelay(int);
+ method public void writeLog();
+ field public static final int LOG_API = 4; // 0x4
+ field public static final int LOG_DELAYED = 2; // 0x2
+ field public static final int LOG_LAYOUT = 3; // 0x3
+ field public static final int LOG_PERIODIC = 1; // 0x1
+ }
+
public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
ctor public MotionEffect(android.content.Context!);
ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
@@ -120,6 +120,20 @@
field protected float mComputedMinY;
}
+ public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public LogJson(android.content.Context);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+ method public void periodicStart();
+ method public void periodicStop();
+ method public void setDelay(int);
+ method public void writeLog();
+ field public static final int LOG_API = 4; // 0x4
+ field public static final int LOG_DELAYED = 2; // 0x2
+ field public static final int LOG_LAYOUT = 3; // 0x3
+ field public static final int LOG_PERIODIC = 1; // 0x1
+ }
+
public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
ctor public MotionEffect(android.content.Context!);
ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/restricted_current.txt b/constraintlayout/constraintlayout/api/restricted_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout/api/restricted_current.txt
@@ -120,6 +120,20 @@
field protected float mComputedMinY;
}
+ public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public LogJson(android.content.Context);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+ ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+ method public void periodicStart();
+ method public void periodicStop();
+ method public void setDelay(int);
+ method public void writeLog();
+ field public static final int LOG_API = 4; // 0x4
+ field public static final int LOG_DELAYED = 2; // 0x2
+ field public static final int LOG_LAYOUT = 3; // 0x3
+ field public static final int LOG_PERIODIC = 1; // 0x1
+ }
+
public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
ctor public MotionEffect(android.content.Context!);
ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
new file mode 100644
index 0000000..6b1c9db
--- /dev/null
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
@@ -0,0 +1,772 @@
+/*
+ * Copyright (C) 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.constraintlayout.helper.widget;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import static androidx.constraintlayout.widget.ConstraintSet.Layout.UNSET_GONE_MARGIN;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Environment;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.constraintlayout.motion.widget.Debug;
+import androidx.constraintlayout.widget.ConstraintAttribute;
+import androidx.constraintlayout.widget.ConstraintHelper;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
+import androidx.constraintlayout.widget.R;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This is a class is a debugging/logging utility to write out the constraints in JSON
+ * This is used for debugging purposes
+ * <ul>
+ * <li>logJsonTo - defines the output log console or "fileName"</li>
+ * <li>logJsonMode - mode one of:
+ * <b>periodic</b>, <b>delayed</b>, <b>layout</b> or <b>api</b></li>
+ * <li>logJsonDelay - the duration of the delay or the delay between repeated logs</li>
+ * </ul>
+ * logJsonTo supports:
+ * <ul>
+ * <li>log - logs using log.v("JSON5", ...)</li>
+ * <li>console - logs using System.out.println(...)</li>
+ * <li>[fileName] - will write to /storage/emulated/0/Download/[fileName].json5</li>
+ * </ul>
+ * logJsonMode modes are:
+ * <ul>
+ * <li>periodic - after window is attached will log every delay ms</li>
+ * <li>delayed - log once after delay ms</li>
+ * <li>layout - log every time there is a layout call</li>
+ * <li>api - do not automatically log developer will call writeLog</li>
+ * </ul>
+ *
+ * The defaults are:
+ * <ul>
+ * <li>logJsonTo="log"</li>
+ * <li>logJsonMode="delayed"</li>
+ * <li>logJsonDelay="1000"</li>
+ * </ul>
+ * Usage:
+ * <p></p>
+ * <pre>
+ * {@code
+ * <androidx.constraintlayout.helper.widget.LogJson
+ * android:layout_width="0dp"
+ * android:layout_height="0dp"
+ * android:visibility="gone"
+ * app:logJsonTo="log"
+ * app:logJsonMode="delayed"
+ * app:logJsonDelay="1000"
+ * />
+ * }
+ * </pre>
+ * </p>
+ */
+public class LogJson extends ConstraintHelper {
+ private static final String TAG = "JSON5";
+ private int mDelay = 1000;
+ private int mMode = LOG_DELAYED;
+ private String mLogToFile = null;
+ private boolean mLogConsole = true;
+
+ public static final int LOG_PERIODIC = 1;
+ public static final int LOG_DELAYED = 2;
+ public static final int LOG_LAYOUT = 3;
+ public static final int LOG_API = 4;
+ private boolean mPeriodic = false;
+
+ public LogJson(@androidx.annotation.NonNull Context context) {
+ super(context);
+ }
+
+ public LogJson(@androidx.annotation.NonNull Context context,
+ @androidx.annotation.Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLogJson(attrs);
+
+ }
+
+ public LogJson(@androidx.annotation.NonNull Context context,
+ @androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLogJson(attrs);
+ }
+
+ private void initLogJson(AttributeSet attrs) {
+
+ if (attrs != null) {
+ TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.LogJson);
+ final int count = a.getIndexCount();
+ for (int i = 0; i < count; i++) {
+ int attr = a.getIndex(i);
+ if (attr == R.styleable.LogJson_logJsonDelay) {
+ mDelay = a.getInt(attr, mDelay);
+ } else if (attr == R.styleable.LogJson_logJsonMode) {
+ mMode = a.getInt(attr, mMode);
+ } else if (attr == R.styleable.LogJson_logJsonTo) {
+ TypedValue v = a.peekValue(attr);
+ if (v.type == TypedValue.TYPE_STRING) {
+ mLogToFile = a.getString(attr);
+ } else {
+ int value = a.getInt(attr, 0);
+ mLogConsole = value == 2;
+ }
+ }
+ }
+ a.recycle();
+ }
+ setVisibility(GONE);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ switch (mMode) {
+ case LOG_PERIODIC:
+ mPeriodic = true;
+ this.postDelayed(this::periodic, mDelay);
+ break;
+ case LOG_DELAYED:
+ this.postDelayed(this::writeLog, mDelay);
+ break;
+ case LOG_LAYOUT:
+ ConstraintLayout cl = (ConstraintLayout) getParent();
+ cl.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) -> logOnLayout());
+ }
+ }
+
+ private void logOnLayout() {
+ if (mMode == LOG_LAYOUT) {
+ writeLog();
+ }
+ }
+
+ /**
+ * Set the duration of periodic logging of constraints
+ *
+ * @param duration the time in ms between writing files
+ */
+ public void setDelay(int duration) {
+ mDelay = duration;
+ }
+
+ /**
+ * Start periodic sampling
+ */
+ public void periodicStart() {
+ if (mPeriodic) {
+ return;
+ }
+ mPeriodic = true;
+ this.postDelayed(this::periodic, mDelay);
+ }
+
+ /**
+ * Stop periodic sampling
+ */
+ public void periodicStop() {
+ mPeriodic = false;
+ }
+
+ private void periodic() {
+ if (mPeriodic) {
+ writeLog();
+ this.postDelayed(this::periodic, mDelay);
+ }
+ }
+
+ /**
+ * This writes a JSON5 representation of the constraintSet
+ */
+ public void writeLog() {
+ String str = asString((ConstraintLayout) this.getParent());
+ if (mLogToFile == null) {
+ if (mLogConsole) {
+ System.out.println(str);
+ } else {
+ logBigString(str);
+ }
+ } else {
+ String name = toFile(str, mLogToFile);
+ Log.v("JSON", "\"" + name + "\" written!");
+ }
+ }
+
+ /**
+ * This writes the JSON5 description of the constraintLayout to a file named fileName.json5
+ * in the download directory which can be pulled with:
+ * "adb pull "/storage/emulated/0/Download/" ."
+ *
+ * @param str String to write as a file
+ * @param fileName file name
+ * @return full path name of file
+ */
+ private static String toFile(String str, String fileName) {
+ FileOutputStream outputStream;
+ if (!fileName.endsWith(".json5")) {
+ fileName += ".json5";
+ }
+ try {
+ File down =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File file = new File(down, fileName);
+ outputStream = new FileOutputStream(file);
+ outputStream.write(str.getBytes());
+ outputStream.close();
+ return file.getCanonicalPath();
+ } catch (IOException e) {
+ return e.toString();
+ }
+ }
+
+ @SuppressLint("LogConditional")
+ private void logBigString(String str) {
+ int len = str.length();
+ for (int i = 0; i < len; i++) {
+ int k = str.indexOf("\n", i);
+ if (k == -1) {
+ Log.v(TAG, str.substring(i));
+ break;
+ }
+ Log.v(TAG, str.substring(i, k));
+ i = k;
+ }
+ }
+
+ /**
+ * Get a JSON5 String that represents the Constraints in a running ConstraintLayout
+ *
+ * @param constraintLayout its constraints are converted to a string
+ * @return JSON5 string
+ */
+ private static String asString(ConstraintLayout constraintLayout) {
+ JsonWriter c = new JsonWriter();
+ return c.constraintLayoutToJson(constraintLayout);
+ }
+
+ // ================================== JSON writer==============================================
+
+ private static class JsonWriter {
+ public static final int UNSET = ConstraintLayout.LayoutParams.UNSET;
+ ConstraintSet mSet;
+ Writer mWriter;
+ Context mContext;
+ int mUnknownCount = 0;
+ final String mLEFT = "left";
+ final String mRIGHT = "right";
+ final String mBASELINE = "baseline";
+ final String mBOTTOM = "bottom";
+ final String mTOP = "top";
+ final String mSTART = "start";
+ final String mEND = "end";
+ private static final String INDENT = " ";
+ private static final String SMALL_INDENT = " ";
+ HashMap<Integer, String> mIdMap = new HashMap<>();
+ private static final String LOG_JSON = LogJson.class.getSimpleName();
+ private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
+ HashMap<Integer, String> mNames = new HashMap<>();
+
+ private static int generateViewId() {
+ final int max_id = 0x00FFFFFF;
+ for (;;) {
+ final int result = sNextGeneratedId.get();
+ int newValue = result + 1;
+ if (newValue > max_id) {
+ newValue = 1;
+ }
+ if (sNextGeneratedId.compareAndSet(result, newValue)) {
+ return result;
+ }
+ }
+ }
+
+ @RequiresApi(17)
+ private static class JellyBean {
+ static int generateViewId() {
+ return View.generateViewId();
+ }
+ }
+
+ String constraintLayoutToJson(ConstraintLayout constraintLayout) {
+ StringWriter writer = new StringWriter();
+
+ int count = constraintLayout.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View v = constraintLayout.getChildAt(i);
+ String name = v.getClass().getSimpleName();
+ int id = v.getId();
+ if (id == -1) {
+ if (android.os.Build.VERSION.SDK_INT
+ >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ id = JellyBean.generateViewId();
+ } else {
+ id = generateViewId();
+ }
+ v.setId(id);
+ if (!LOG_JSON.equals(name)) {
+ name = "noid_" + name;
+ }
+ mNames.put(id, name);
+ } else if (LOG_JSON.equals(name)) {
+ mNames.put(id, name);
+ }
+ }
+ writer.append("{\n");
+
+ writeWidgets(writer, constraintLayout);
+ writer.append(" ConstraintSet:{\n");
+ ConstraintSet set = new ConstraintSet();
+ set.clone(constraintLayout);
+ String name =
+ (constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout);
+ try {
+ writer.append(name + ":");
+ setup(writer, set, constraintLayout);
+ writeLayout();
+ writer.append("\n");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ writer.append(" }\n");
+ writer.append("}\n");
+ return writer.toString();
+ }
+
+ private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) {
+ writer.append("Widgets:{\n");
+ int count = constraintLayout.getChildCount();
+
+ for (int i = -1; i < count; i++) {
+ View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i);
+ int id = v.getId();
+ if (LOG_JSON.equals(v.getClass().getSimpleName())) {
+ continue;
+ }
+ String name = mNames.containsKey(id) ? mNames.get(id)
+ : ((i == -1) ? "parent" : Debug.getName(v));
+ String cname = v.getClass().getSimpleName();
+ String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop()
+ + ", " + v.getRight() + ", " + v.getBottom() + "]},\n";
+ writer.append(" " + name + ": { ");
+ if (i == -1) {
+ writer.append("type: '" + v.getClass().getSimpleName() + "' , ");
+
+ try {
+ ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams();
+ String wrap = "'WRAP_CONTENT'";
+ String match = "'MATCH_PARENT'";
+ String w = p.width == MATCH_PARENT ? match :
+ (p.width == WRAP_CONTENT) ? wrap : p.width + "";
+ writer.append("width: " + w + ", ");
+ String h = p.height == MATCH_PARENT ? match :
+ (p.height == WRAP_CONTENT) ? wrap : p.height + "";
+ writer.append("height: ").append(h);
+ } catch (Exception e) {
+ }
+ } else if (cname.contains("Text")) {
+ if (v instanceof TextView) {
+ writer.append("type: 'Text', label: '"
+ + escape(((TextView) v).getText().toString()) + "'");
+ } else {
+ writer.append("type: 'Text' },\n");
+ }
+ } else if (cname.contains("Button")) {
+ if (v instanceof Button) {
+ writer.append("type: 'Button', label: '" + ((Button) v).getText() + "'");
+ } else
+ writer.append("type: 'Button'");
+ } else if (cname.contains("Image")) {
+ writer.append("type: 'Image'");
+ } else if (cname.contains("View")) {
+ writer.append("type: 'Box'");
+ } else {
+ writer.append("type: '" + v.getClass().getSimpleName() + "'");
+ }
+ writer.append(bounds);
+ }
+ writer.append("},\n");
+ }
+
+ private static String escape(String str) {
+ return str.replaceAll("'", "\\'");
+ }
+
+ JsonWriter() {
+ }
+
+ void setup(Writer writer,
+ ConstraintSet set,
+ ConstraintLayout layout) throws IOException {
+ this.mWriter = writer;
+ this.mContext = layout.getContext();
+ this.mSet = set;
+ set.getConstraint(2);
+ }
+
+ private int[] getIDs() {
+ return mSet.getKnownIds();
+ }
+
+ private ConstraintSet.Constraint getConstraint(int id) {
+ return mSet.getConstraint(id);
+ }
+
+ private void writeLayout() throws IOException {
+ mWriter.write("{\n");
+ for (Integer id : getIDs()) {
+ ConstraintSet.Constraint c = getConstraint(id);
+ String idName = getSimpleName(id);
+ if (LOG_JSON.equals(idName)) { // skip LogJson it is for used to log
+ continue;
+ }
+ mWriter.write(SMALL_INDENT + idName + ":{\n");
+ ConstraintSet.Layout l = c.layout;
+ if (l.mReferenceIds != null) {
+ StringBuilder ref =
+ new StringBuilder("type: '_" + idName + "_' , contains: [");
+ for (int r = 0; r < l.mReferenceIds.length; r++) {
+ int rid = l.mReferenceIds[r];
+ ref.append((r == 0) ? "" : ", ").append(getName(rid));
+ }
+ mWriter.write(ref + "]\n");
+ }
+ if (l.mReferenceIdString != null) {
+ StringBuilder ref =
+ new StringBuilder(SMALL_INDENT + "type: '???' , contains: [");
+ String[] rids = l.mReferenceIdString.split(",");
+ for (int r = 0; r < rids.length; r++) {
+ String rid = rids[r];
+ ref.append((r == 0) ? "" : ", ").append("`").append(rid).append("`");
+ }
+ mWriter.write(ref + "]\n");
+ }
+ writeDimension("height", l.mHeight, l.heightDefault, l.heightPercent,
+ l.heightMin, l.heightMax, l.constrainedHeight);
+ writeDimension("width", l.mWidth, l.widthDefault, l.widthPercent,
+ l.widthMin, l.widthMax, l.constrainedWidth);
+
+ writeConstraint(mLEFT, l.leftToLeft, mLEFT, l.leftMargin, l.goneLeftMargin);
+ writeConstraint(mLEFT, l.leftToRight, mRIGHT, l.leftMargin, l.goneLeftMargin);
+ writeConstraint(mRIGHT, l.rightToLeft, mLEFT, l.rightMargin, l.goneRightMargin);
+ writeConstraint(mRIGHT, l.rightToRight, mRIGHT, l.rightMargin, l.goneRightMargin);
+ writeConstraint(mBASELINE, l.baselineToBaseline, mBASELINE, UNSET,
+ l.goneBaselineMargin);
+ writeConstraint(mBASELINE, l.baselineToTop, mTOP, UNSET, l.goneBaselineMargin);
+ writeConstraint(mBASELINE, l.baselineToBottom,
+ mBOTTOM, UNSET, l.goneBaselineMargin);
+
+ writeConstraint(mTOP, l.topToBottom, mBOTTOM, l.topMargin, l.goneTopMargin);
+ writeConstraint(mTOP, l.topToTop, mTOP, l.topMargin, l.goneTopMargin);
+ writeConstraint(mBOTTOM, l.bottomToBottom, mBOTTOM, l.bottomMargin,
+ l.goneBottomMargin);
+ writeConstraint(mBOTTOM, l.bottomToTop, mTOP, l.bottomMargin, l.goneBottomMargin);
+ writeConstraint(mSTART, l.startToStart, mSTART, l.startMargin, l.goneStartMargin);
+ writeConstraint(mSTART, l.startToEnd, mEND, l.startMargin, l.goneStartMargin);
+ writeConstraint(mEND, l.endToStart, mSTART, l.endMargin, l.goneEndMargin);
+ writeConstraint(mEND, l.endToEnd, mEND, l.endMargin, l.goneEndMargin);
+
+ writeVariable("horizontalBias", l.horizontalBias, 0.5f);
+ writeVariable("verticalBias", l.verticalBias, 0.5f);
+
+ writeCircle(l.circleConstraint, l.circleAngle, l.circleRadius);
+
+ writeGuideline(l.orientation, l.guideBegin, l.guideEnd, l.guidePercent);
+ writeVariable("dimensionRatio", l.dimensionRatio);
+ writeVariable("barrierMargin", l.mBarrierMargin);
+ writeVariable("type", l.mHelperType);
+ writeVariable("ReferenceId", l.mReferenceIdString);
+ writeVariable("mBarrierAllowsGoneWidgets",
+ l.mBarrierAllowsGoneWidgets, true);
+ writeVariable("WrapBehavior", l.mWrapBehavior);
+
+ writeVariable("verticalWeight", l.verticalWeight);
+ writeVariable("horizontalWeight", l.horizontalWeight);
+ writeVariable("horizontalChainStyle", l.horizontalChainStyle);
+ writeVariable("verticalChainStyle", l.verticalChainStyle);
+ writeVariable("barrierDirection", l.mBarrierDirection);
+ if (l.mReferenceIds != null) {
+ writeVariable("ReferenceIds", l.mReferenceIds);
+ }
+ writeTransform(c.transform);
+ writeCustom(c.mCustomConstraints);
+
+ mWriter.write(" },\n");
+ }
+ mWriter.write("},\n");
+ }
+
+ private void writeTransform(ConstraintSet.Transform transform) throws IOException {
+ if (transform.applyElevation) {
+ writeVariable("elevation", transform.elevation);
+ }
+ writeVariable("rotationX", transform.rotationX, 0);
+ writeVariable("rotationY", transform.rotationY, 0);
+ writeVariable("rotationZ", transform.rotation, 0);
+ writeVariable("scaleX", transform.scaleX, 1);
+ writeVariable("scaleY", transform.scaleY, 1);
+ writeVariable("translationX", transform.translationX, 0);
+ writeVariable("translationY", transform.translationY, 0);
+ writeVariable("translationZ", transform.translationZ, 0);
+ }
+
+ private void writeCustom(HashMap<String, ConstraintAttribute> cset) throws IOException {
+ if (cset != null && cset.size() > 0) {
+ mWriter.write(INDENT + "custom: {\n");
+ for (String s : cset.keySet()) {
+ ConstraintAttribute attr = cset.get(s);
+ if (attr == null) {
+ continue;
+ }
+ String custom = INDENT + SMALL_INDENT + attr.getName() + ": ";
+ switch (attr.getType()) {
+ case INT_TYPE:
+ custom += attr.getIntegerValue();
+ break;
+ case COLOR_TYPE:
+ custom += colorString(attr.getColorValue());
+ break;
+ case FLOAT_TYPE:
+ custom += attr.getFloatValue();
+ break;
+ case STRING_TYPE:
+ custom += "'" + attr.getStringValue() + "'";
+ break;
+ case DIMENSION_TYPE:
+ custom = custom + attr.getFloatValue();
+ break;
+ case REFERENCE_TYPE:
+ case COLOR_DRAWABLE_TYPE:
+ case BOOLEAN_TYPE:
+ custom = null;
+ }
+ if (custom != null) {
+ mWriter.write(custom + ",\n");
+ }
+ }
+ mWriter.write(SMALL_INDENT + " } \n");
+ }
+ }
+
+ private static String colorString(int v) {
+ String str = "00000000" + Integer.toHexString(v);
+ return "#" + str.substring(str.length() - 8);
+ }
+
+ private void writeGuideline(int orientation,
+ int guideBegin,
+ int guideEnd,
+ float guidePercent) throws IOException {
+ writeVariable("orientation", orientation);
+ writeVariable("guideBegin", guideBegin);
+ writeVariable("guideEnd", guideEnd);
+ writeVariable("guidePercent", guidePercent);
+ }
+
+ private void writeDimension(String dimString,
+ int dim,
+ int dimDefault,
+ float dimPercent,
+ int dimMin,
+ int dimMax,
+ boolean unusedConstrainedDim) throws IOException {
+ if (dim == 0) {
+ if (dimMax != UNSET || dimMin != UNSET) {
+ String s = "-----";
+ switch (dimDefault) {
+ case 0: // spread
+ s = INDENT + dimString + ": {value:'spread'";
+ break;
+ case 1: // wrap
+ s = INDENT + dimString + ": {value:'wrap'";
+ break;
+ case 2: // percent
+ s = INDENT + dimString + ": {value: '" + dimPercent + "%'";
+ break;
+ }
+ if (dimMax != UNSET) {
+ s += ", max: " + dimMax;
+ }
+ if (dimMax != UNSET) {
+ s += ", min: " + dimMin;
+ }
+ s += "},\n";
+ mWriter.write(s);
+ return;
+ }
+
+ switch (dimDefault) {
+ case 0: // spread is the default
+ break;
+ case 1: // wrap
+ mWriter.write(INDENT + dimString + ": '???????????',\n");
+ return;
+ case 2: // percent
+ mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n");
+ }
+
+ } else if (dim == -2) {
+ mWriter.write(INDENT + dimString + ": 'wrap',\n");
+ } else if (dim == -1) {
+ mWriter.write(INDENT + dimString + ": 'parent',\n");
+ } else {
+ mWriter.write(INDENT + dimString + ": " + dim + ",\n");
+ }
+ }
+
+ private String getSimpleName(int id) {
+ if (mIdMap.containsKey(id)) {
+ return "" + mIdMap.get(id);
+ }
+ if (id == 0) {
+ return "parent";
+ }
+ String name = lookup(id);
+ mIdMap.put(id, name);
+ return "" + name + "";
+ }
+
+ private String getName(int id) {
+ return "'" + getSimpleName(id) + "'";
+ }
+
+ private String lookup(int id) {
+ try {
+ if (mNames.containsKey(id)) {
+ return mNames.get(id);
+ }
+ if (id != -1) {
+ return mContext.getResources().getResourceEntryName(id);
+ } else {
+ return "unknown" + ++mUnknownCount;
+ }
+ } catch (Exception ex) {
+ return "unknown" + ++mUnknownCount;
+ }
+ }
+
+ private void writeConstraint(String my,
+ int constraint,
+ String other,
+ int margin,
+ int goneMargin) throws IOException {
+ if (constraint == UNSET) {
+ return;
+ }
+ mWriter.write(INDENT + my);
+ mWriter.write(":[");
+ mWriter.write(getName(constraint));
+ mWriter.write(", ");
+ mWriter.write("'" + other + "'");
+ if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) {
+ mWriter.write(", " + margin);
+ if (goneMargin != UNSET_GONE_MARGIN) {
+ mWriter.write(", " + goneMargin);
+ }
+ }
+ mWriter.write("],\n");
+ }
+
+ private void writeCircle(int circleConstraint,
+ float circleAngle,
+ int circleRadius) throws IOException {
+ if (circleConstraint == UNSET) {
+ return;
+ }
+ mWriter.write(INDENT + "circle");
+ mWriter.write(":[");
+ mWriter.write(getName(circleConstraint));
+ mWriter.write(", " + circleAngle);
+ mWriter.write(circleRadius + "],\n");
+ }
+
+ private void writeVariable(String name, int value) throws IOException {
+ if (value == 0 || value == -1) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": " + value);
+ mWriter.write(",\n");
+ }
+
+ private void writeVariable(String name, float value) throws IOException {
+ if (value == UNSET) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": " + value);
+ mWriter.write(",\n");
+ }
+
+ private void writeVariable(String name, float value, float def) throws IOException {
+ if (value == def) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": " + value);
+ mWriter.write(",\n");
+ }
+
+ private void writeVariable(String name, boolean value, boolean def) throws IOException {
+ if (value == def) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": " + value);
+ mWriter.write(",\n");
+ }
+
+ private void writeVariable(String name, int[] value) throws IOException {
+ if (value == null) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": ");
+ for (int i = 0; i < value.length; i++) {
+ mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i]));
+ }
+ mWriter.write("],\n");
+ }
+
+ private void writeVariable(String name, String value) throws IOException {
+ if (value == null) {
+ return;
+ }
+ mWriter.write(INDENT + name);
+ mWriter.write(": '" + value);
+ mWriter.write("',\n");
+ }
+ }
+}
diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
index d9029b17..aba4fb6 100644
--- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
+++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
@@ -816,6 +816,21 @@
</declare-styleable>
+ <!-- This is the api for logging constraints in JSON5 format -->
+ <declare-styleable name="LogJson">
+ <attr name="logJsonTo" format="enum|string">
+ <enum name="log" value="1" />
+ <enum name="console" value="2" />
+ </attr>
+ <attr name="logJsonMode" format="enum">
+ <enum name="periodic" value="1" />
+ <enum name="delayed" value="2" />
+ <enum name="layout" value="3" />
+ <enum name="api" value="4" />
+ </attr>
+ <attr name="logJsonDelay" format="integer" />
+ </declare-styleable>
+
<declare-styleable name="Constraint">
<attr name="android:orientation" />
<attr name="android:id" />
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index aa9867b..3f5d92d 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1804,6 +1804,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+ field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3135,8 +3136,9 @@
method public void setSystemBarsBehavior(int);
method public void show(int);
method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
- field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
- field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+ field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
}
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 7960fa9..9df9106 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1807,6 +1807,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
method @ChecksSdkIntAtLeast(api=33, codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
method @ChecksSdkIntAtLeast(codename="UpsideDownCake") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastU();
+ field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3141,8 +3142,9 @@
method public void setSystemBarsBehavior(int);
method public void show(int);
method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
- field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
- field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+ field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
}
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 63e99d4..51353767 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2144,6 +2144,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+ field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3595,8 +3596,9 @@
method public void setSystemBarsBehavior(int);
method public void show(@androidx.core.view.WindowInsetsCompat.Type.InsetsType int);
method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
- field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
- field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+ field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+ field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
index b7bb15f..6a40e06 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
@@ -376,12 +376,31 @@
}
@Test
- // minSdkVersion = 21 due to b/189492236
- @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
- public fun systemBarsBehavior_swipe() {
+ @SdkSuppress(minSdkVersion = 31) // Older APIs does not support getSystemBarsBehavior
+ fun systemBarsBehavior() {
scenario.onActivity {
windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+ assertEquals(
+ WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
+ windowInsetsController.systemBarsBehavior
+ )
+ windowInsetsController.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ assertEquals(
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+ windowInsetsController.systemBarsBehavior
+ )
+ }
+ }
+
+ @Test
+ // minSdkVersion = 21 due to b/189492236
+ @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
+ public fun systemBarsBehavior_default() {
+ scenario.onActivity {
+ windowInsetsController.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
val decorView = scenario.withActivity { window.decorView }
val sysUiVis = decorView.systemUiVisibility
@@ -409,23 +428,6 @@
assertEquals(0, sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE)
}
- @Test
- public fun systemBarsBehavior_touch() {
- scenario.onActivity {
- windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH
- }
- val decorView = scenario.withActivity { window.decorView }
- val sysUiVis = decorView.systemUiVisibility
- assertEquals(
- 0,
- sysUiVis and (
- View.SYSTEM_UI_FLAG_IMMERSIVE
- or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
- )
- )
- }
-
private fun assumeNotCuttlefish() {
// TODO: remove this if b/159103848 is resolved
assumeFalse(
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e50f09e..223fba1 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -21,6 +21,7 @@
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Build.VERSION;
+import android.os.ext.SdkExtensions;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
@@ -281,6 +282,21 @@
@SuppressLint("CompileTimeConstant")
public static final int T_EXTENSION_INT = VERSION.SDK_INT >= 30 ? Extensions30Impl.TIRAMISU : 0;
+ /**
+ * The value of {@code SdkExtensions.getExtensionVersion(AD_SERVICES)}. This is a convenience
+ * constant which provides the extension version in a similar style to
+ * {@code Build.VERSION.SDK_INT}.
+ * <p>
+ * Compared to calling {@code getExtensionVersion} directly, using this constant has the
+ * benefit of not having to verify the {@code getExtensionVersion} method is available.
+ *
+ * @return the version of the AdServices extension, if it exists. 0 otherwise.
+ */
+ @ChecksSdkIntAtLeast(extension = SdkExtensions.AD_SERVICES)
+ @SuppressLint("CompileTimeConstant")
+ public static final int AD_SERVICES_EXTENSION_INT =
+ VERSION.SDK_INT >= 30 ? Extensions30Impl.AD_SERVICES : 0;
+
@SuppressLint("ClassVerificationFailure") // Remove when SDK including b/206996004 is imported
@RequiresApi(30)
private static final class Extensions30Impl {
@@ -290,6 +306,8 @@
static final int S = getExtensionVersion(Build.VERSION_CODES.S);
@SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
static final int TIRAMISU = getExtensionVersion(Build.VERSION_CODES.TIRAMISU);
+ @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
+ static final int AD_SERVICES = getExtensionVersion(SdkExtensions.AD_SERVICES);
}
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
index 9a49066..bff7344 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
@@ -54,22 +54,43 @@
public final class WindowInsetsControllerCompat {
/**
- * The default option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly
- * shown on any user interaction on the corresponding display if navigation bars are hidden
- * by {@link #hide(int)} or
+ * Option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly shown on any
+ * user interaction on the corresponding display if navigation bars are hidden by
+ * {@link #hide(int)} or
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+ *
+ * @deprecated This is not supported on Android {@link android.os.Build.VERSION_CODES#S} and
+ * later. Use {@link #BEHAVIOR_DEFAULT} or {@link #BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}
+ * instead.
*/
+ @Deprecated
public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0;
/**
+ * The default option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
+ * interactive when hiding navigation bars by calling {@link #hide(int)} or
+ * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+ *
+ * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such
+ * as swiping from the edge of the screen where the bar is hidden from.</p>
+ *
+ * <p>When the gesture navigation is enabled, the system gestures can be triggered regardless
+ * the visibility of system bars.</p>
+ */
+ public static final int BEHAVIOR_DEFAULT = 1;
+
+ /**
* Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive
* when hiding navigation bars by calling {@link #hide(int)} or
* {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
* <p>
* When system bars are hidden in this mode, they can be revealed with system
* gestures, such as swiping from the edge of the screen where the bar is hidden from.
+ *
+ * @deprecated Use {@link #BEHAVIOR_DEFAULT} instead.
*/
- public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1;
+ @Deprecated
+ public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = BEHAVIOR_DEFAULT;
/**
* Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
@@ -135,8 +156,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {BEHAVIOR_SHOW_BARS_BY_TOUCH, BEHAVIOR_SHOW_BARS_BY_SWIPE,
- BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
+ @IntDef(value = {BEHAVIOR_DEFAULT, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
@interface Behavior {
}
@@ -515,7 +535,7 @@
@Override
void setSystemBarsBehavior(int behavior) {
switch (behavior) {
- case BEHAVIOR_SHOW_BARS_BY_SWIPE:
+ case BEHAVIOR_DEFAULT:
unsetSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
setSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE);
break;
diff --git a/emoji2/emoji2-bundled/api/1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+ public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public BundledEmojiCompatConfig(android.content.Context);
+ }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+ public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public BundledEmojiCompatConfig(android.content.Context);
+ }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+ public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public BundledEmojiCompatConfig(android.content.Context);
+ }
+
+}
+
diff --git a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
index 929dbc8..1cf822f 100644
--- a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
@@ -22,6 +22,8 @@
import android.content.Context;
import android.graphics.Paint;
import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextPaint;
import androidx.core.graphics.PaintCompat;
import androidx.emoji2.bundled.util.EmojiMatcher;
@@ -135,6 +137,23 @@
EmojiCompat.get().hasEmojiGlyph(mString, Integer.MAX_VALUE));
}
+ @Test
+ public void emoji_hasDesiredWidth() {
+ TextPaint tp = new TextPaint();
+ // spanned, test fails, width == 0
+ CharSequence processedText = EmojiCompat.get()
+ .process(
+ mString,
+ /* start= */ 0,
+ /* end= */ mString.length(),
+ /* maxEmojiCount= */ mString.length(),
+ EmojiCompat.REPLACE_STRATEGY_ALL);
+ float emojiCompatWidth = StaticLayout.getDesiredWidth(processedText, tp);
+ assertTrue("emoji " + mString + " with codepoints " + mCodepoints + "has "
+ + "desired width " + emojiCompatWidth,
+ emojiCompatWidth > 0);
+ }
+
private void assertSpanCanRenderEmoji(final String str) {
final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString());
final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class);
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index be8163d..3ea643f 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -60,6 +60,7 @@
androidx {
name = "androidx.emoji2:emoji2-emojipicker"
type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.EMOJI2_QUARANTINE
inceptionYear = "2022"
description = "This library provides the latest emoji support and emoji picker UI to input " +
"emoji in current and older devices"
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 989a8e3..479e1ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -30,4 +30,9 @@
// The max pool size of the Emoji ItemType in RecyclerViewPool.
const val EMOJI_VIEW_POOL_SIZE = 100
+
+ const val ADD_VIEW_EXCEPTION_MESSAGE = "Adding views to the EmojiPickerView is unsupported"
+
+ const val REMOVE_VIEW_EXCEPTION_MESSAGE =
+ "Removing views from the EmojiPickerView is unsupported"
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index ef30c3b..79ce7be 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -20,6 +20,7 @@
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.View
+import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.util.Consumer
import androidx.core.view.ViewCompat
@@ -46,9 +47,10 @@
/**
* The number of rows of the emoji picker.
*
- * Default value will be used if emojiGridRows is set to non-positive value. Float value
- * indicates that we will display partial of the last row and have content down, so the users
- * get the idea that they can scroll down for more contents.
+ * Default value([EmojiPickerConstants.DEFAULT_BODY_ROWS]: 7.5) will be used if emojiGridRows
+ * is set to non-positive value. Float value indicates that we will display partial of the last
+ * row and have content down, so the users get the idea that they can scroll down for more
+ * contents.
* @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridRows
*/
var emojiGridRows: Float = EmojiPickerConstants.DEFAULT_BODY_ROWS
@@ -63,7 +65,8 @@
/**
* The number of columns of the emoji picker.
*
- * Default value will be used if emojiGridColumns is set to non-positive value.
+ * Default value([EmojiPickerConstants.DEFAULT_BODY_COLUMNS]: 9) will be used if
+ * emojiGridColumns is set to non-positive value.
* @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridColumns
*/
var emojiGridColumns: Int = EmojiPickerConstants.DEFAULT_BODY_COLUMNS
@@ -180,7 +183,7 @@
})
// clear view's children in case of resetting layout
- removeAllViews()
+ super.removeAllViews()
with(inflate(context, R.layout.emoji_picker, this)) {
// set headerView
ViewCompat.requireViewById<RecyclerView>(this, R.id.emoji_picker_header).apply {
@@ -259,14 +262,110 @@
}
/**
- * Disallow clients to add view to the EmojiPickerView
+ * The following functions disallow clients to add view to the EmojiPickerView
*
* @param child the child view to be added
* @throws UnsupportedOperationException
*/
override fun addView(child: View?) {
- throw UnsupportedOperationException(
- "Adding views to the EmojiPickerView is unsupported"
- )
+ if (childCount > 0)
+ throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+ else super.addView(child)
+ }
+
+ /**
+ * @param child
+ * @param params
+ * @throws UnsupportedOperationException
+ */
+ override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
+ if (childCount > 0)
+ throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+ else super.addView(child, params)
+ }
+
+ /**
+ * @param child
+ * @param index
+ * @throws UnsupportedOperationException
+ */
+ override fun addView(child: View?, index: Int) {
+ if (childCount > 0)
+ throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+ else super.addView(child, index)
+ }
+
+ /**
+ * @param child
+ * @param index
+ * @param params
+ * @throws UnsupportedOperationException
+ */
+ override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+ if (childCount > 0)
+ throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+ else super.addView(child, index, params)
+ }
+
+ /**
+ * @param child
+ * @param width
+ * @param height
+ * @throws UnsupportedOperationException
+ */
+ override fun addView(child: View?, width: Int, height: Int) {
+ if (childCount > 0)
+ throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+ else super.addView(child, width, height)
+ }
+
+ /**
+ * The following functions disallow clients to remove view from the EmojiPickerView
+ * @throws UnsupportedOperationException
+ */
+ override fun removeAllViews() {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+ }
+
+ /**
+ * @param child
+ * @throws UnsupportedOperationException
+ */
+ override fun removeView(child: View?) {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+ }
+
+ /**
+ * @param index
+ * @throws UnsupportedOperationException
+ */
+ override fun removeViewAt(index: Int) {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+ }
+
+ /**
+ * @param child
+ * @throws UnsupportedOperationException
+ */
+ override fun removeViewInLayout(child: View?) {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+ }
+
+ /**
+ * @param start
+ * @param count
+ * @throws UnsupportedOperationException
+ */
+ override fun removeViews(start: Int, count: Int) {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+ }
+
+ /**
+ * @param start
+ * @param count
+ * @throws UnsupportedOperationException
+ */
+ override fun removeViewsInLayout(start: Int, count: Int) {
+ throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
}
}
diff --git a/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+ public final class EmojiEditTextHelper {
+ ctor public EmojiEditTextHelper(android.widget.EditText);
+ ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+ method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+ method public int getMaxEmojiCount();
+ method public boolean isEnabled();
+ method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+ method public void setEnabled(boolean);
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public final class EmojiTextViewHelper {
+ ctor public EmojiTextViewHelper(android.widget.TextView);
+ ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+ method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+ method public boolean isEnabled();
+ method public void setAllCaps(boolean);
+ method public void setEnabled(boolean);
+ method public void updateTransformationMethod();
+ method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+ public final class EmojiEditTextHelper {
+ ctor public EmojiEditTextHelper(android.widget.EditText);
+ ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+ method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+ method public int getMaxEmojiCount();
+ method public boolean isEnabled();
+ method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+ method public void setEnabled(boolean);
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public final class EmojiTextViewHelper {
+ ctor public EmojiTextViewHelper(android.widget.TextView);
+ ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+ method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+ method public boolean isEnabled();
+ method public void setAllCaps(boolean);
+ method public void setEnabled(boolean);
+ method public void updateTransformationMethod();
+ method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+ public final class EmojiEditTextHelper {
+ ctor public EmojiEditTextHelper(android.widget.EditText);
+ ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+ method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+ method public int getMaxEmojiCount();
+ method public boolean isEnabled();
+ method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+ method public void setEnabled(boolean);
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public final class EmojiTextViewHelper {
+ ctor public EmojiTextViewHelper(android.widget.TextView);
+ ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+ method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+ method public boolean isEnabled();
+ method public void setAllCaps(boolean);
+ method public void setEnabled(boolean);
+ method public void updateTransformationMethod();
+ method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views/api/1.3.0-beta01.txt b/emoji2/emoji2-views/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+ public class EmojiButton extends android.widget.Button {
+ ctor public EmojiButton(android.content.Context);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+ public class EmojiEditText extends android.widget.EditText {
+ ctor public EmojiEditText(android.content.Context);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+ method public int getMaxEmojiCount();
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+ ctor public EmojiExtractTextLayout(android.content.Context);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public int getEmojiReplaceStrategy();
+ method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+ method public void setEmojiReplaceStrategy(int);
+ }
+
+ public class EmojiTextView extends android.widget.TextView {
+ ctor public EmojiTextView(android.content.Context);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+ public class EmojiButton extends android.widget.Button {
+ ctor public EmojiButton(android.content.Context);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+ public class EmojiEditText extends android.widget.EditText {
+ ctor public EmojiEditText(android.content.Context);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+ method public int getMaxEmojiCount();
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+ ctor public EmojiExtractTextLayout(android.content.Context);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public int getEmojiReplaceStrategy();
+ method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+ method public void setEmojiReplaceStrategy(int);
+ }
+
+ public class EmojiTextView extends android.widget.TextView {
+ ctor public EmojiTextView(android.content.Context);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+}
+
diff --git a/emoji2/emoji2-views/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..8bc8423
--- /dev/null
+++ b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
@@ -0,0 +1,2 @@
+attr emojiReplaceStrategy
+attr maxEmojiCount
diff --git a/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+ public class EmojiButton extends android.widget.Button {
+ ctor public EmojiButton(android.content.Context);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+ public class EmojiEditText extends android.widget.EditText {
+ ctor public EmojiEditText(android.content.Context);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+ method public int getMaxEmojiCount();
+ method public void setMaxEmojiCount(@IntRange(from=0) int);
+ }
+
+ public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+ ctor public EmojiExtractTextLayout(android.content.Context);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public int getEmojiReplaceStrategy();
+ method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+ method public void setEmojiReplaceStrategy(int);
+ }
+
+ public class EmojiTextView extends android.widget.TextView {
+ ctor public EmojiTextView(android.content.Context);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+ ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+ }
+
+}
+
diff --git a/emoji2/emoji2/api/1.3.0-beta01.txt b/emoji2/emoji2/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+ public final class DefaultEmojiCompatConfig {
+ method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+ }
+
+ @AnyThread public class EmojiCompat {
+ method public static androidx.emoji2.text.EmojiCompat get();
+ method public String getAssetSignature();
+ method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+ method public int getLoadState();
+ method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+ method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+ method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+ method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+ method public static boolean isConfigured();
+ method public void load();
+ method @CheckResult public CharSequence? process(CharSequence?);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+ method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+ field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+ field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int EMOJI_FALLBACK = 2; // 0x2
+ field public static final int EMOJI_SUPPORTED = 1; // 0x1
+ field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+ field public static final int LOAD_STATE_FAILED = 2; // 0x2
+ field public static final int LOAD_STATE_LOADING = 0; // 0x0
+ field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+ }
+
+ public abstract static class EmojiCompat.Config {
+ ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+ method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+ method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+ method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+ method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ }
+
+ public static interface EmojiCompat.GlyphChecker {
+ method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ }
+
+ public abstract static class EmojiCompat.InitCallback {
+ ctor public EmojiCompat.InitCallback();
+ method public void onFailed(Throwable?);
+ method public void onInitialized();
+ }
+
+ public static interface EmojiCompat.MetadataRepoLoader {
+ method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+ }
+
+ public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+ ctor public EmojiCompat.MetadataRepoLoaderCallback();
+ method public abstract void onFailed(Throwable?);
+ method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+ }
+
+ public static interface EmojiCompat.SpanFactory {
+ method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+ }
+
+ public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+ ctor public EmojiCompatInitializer();
+ method public Boolean create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
+ @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+ method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+ method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+ }
+
+ public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+ method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
+ }
+
+ @AnyThread @RequiresApi(19) public final class MetadataRepo {
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+ }
+
+ @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+ method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+ method public int getCodepointAt(int);
+ method public int getCodepointsLength();
+ method public int getHeight();
+ method public android.graphics.Typeface getTypeface();
+ method public int getWidth();
+ method public boolean isDefaultEmoji();
+ method public boolean isPreferredSystemRender();
+ }
+
+}
+
diff --git a/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+ public final class DefaultEmojiCompatConfig {
+ method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+ }
+
+ @AnyThread public class EmojiCompat {
+ method public static androidx.emoji2.text.EmojiCompat get();
+ method public String getAssetSignature();
+ method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+ method public int getLoadState();
+ method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+ method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+ method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+ method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+ method public static boolean isConfigured();
+ method public void load();
+ method @CheckResult public CharSequence? process(CharSequence?);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+ method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+ field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+ field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int EMOJI_FALLBACK = 2; // 0x2
+ field public static final int EMOJI_SUPPORTED = 1; // 0x1
+ field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+ field public static final int LOAD_STATE_FAILED = 2; // 0x2
+ field public static final int LOAD_STATE_LOADING = 0; // 0x0
+ field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+ }
+
+ public abstract static class EmojiCompat.Config {
+ ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+ method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+ method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+ method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+ method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ }
+
+ public static interface EmojiCompat.GlyphChecker {
+ method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ }
+
+ public abstract static class EmojiCompat.InitCallback {
+ ctor public EmojiCompat.InitCallback();
+ method public void onFailed(Throwable?);
+ method public void onInitialized();
+ }
+
+ public static interface EmojiCompat.MetadataRepoLoader {
+ method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+ }
+
+ public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+ ctor public EmojiCompat.MetadataRepoLoaderCallback();
+ method public abstract void onFailed(Throwable?);
+ method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+ }
+
+ public static interface EmojiCompat.SpanFactory {
+ method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+ }
+
+ public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+ ctor public EmojiCompatInitializer();
+ method public Boolean create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
+ @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+ method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+ method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+ }
+
+ public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+ method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
+ }
+
+ @AnyThread @RequiresApi(19) public final class MetadataRepo {
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+ }
+
+ @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+ method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+ method public int getCodepointAt(int);
+ method public int getCodepointsLength();
+ method public int getHeight();
+ method public android.graphics.Typeface getTypeface();
+ method public int getWidth();
+ method public boolean isDefaultEmoji();
+ method public boolean isPreferredSystemRender();
+ }
+
+}
+
diff --git a/emoji2/emoji2/api/res-1.3.0-beta01.txt b/emoji2/emoji2/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+ public final class DefaultEmojiCompatConfig {
+ method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+ }
+
+ @AnyThread public class EmojiCompat {
+ method public static androidx.emoji2.text.EmojiCompat get();
+ method public String getAssetSignature();
+ method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+ method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+ method public int getLoadState();
+ method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+ method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+ method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+ method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+ method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+ method public static boolean isConfigured();
+ method public void load();
+ method @CheckResult public CharSequence? process(CharSequence?);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+ method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+ field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+ field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int EMOJI_FALLBACK = 2; // 0x2
+ field public static final int EMOJI_SUPPORTED = 1; // 0x1
+ field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+ field public static final int LOAD_STATE_FAILED = 2; // 0x2
+ field public static final int LOAD_STATE_LOADING = 0; // 0x0
+ field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+ field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+ }
+
+ public abstract static class EmojiCompat.Config {
+ ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+ method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+ method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+ method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+ method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+ method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+ method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+ method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+ }
+
+ public static interface EmojiCompat.GlyphChecker {
+ method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+ }
+
+ public abstract static class EmojiCompat.InitCallback {
+ ctor public EmojiCompat.InitCallback();
+ method public void onFailed(Throwable?);
+ method public void onInitialized();
+ }
+
+ public static interface EmojiCompat.MetadataRepoLoader {
+ method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+ }
+
+ public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+ ctor public EmojiCompat.MetadataRepoLoaderCallback();
+ method public abstract void onFailed(Throwable?);
+ method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+ }
+
+ public static interface EmojiCompat.SpanFactory {
+ method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+ }
+
+ public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+ ctor public EmojiCompatInitializer();
+ method public Boolean create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
+ @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+ method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+ method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+ }
+
+ public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+ ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+ method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+ method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
+ }
+
+ @AnyThread @RequiresApi(19) public final class MetadataRepo {
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+ method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+ }
+
+ @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+ method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+ method public int getCodepointAt(int);
+ method public int getCodepointsLength();
+ method public int getHeight();
+ method public android.graphics.Typeface getTypeface();
+ method public int getWidth();
+ method public boolean isDefaultEmoji();
+ method public boolean isPreferredSystemRender();
+ }
+
+}
+
diff --git a/libraryversions.toml b/libraryversions.toml
index 9160d928..46498e4 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -6,7 +6,7 @@
APPACTIONS_INTERACTION = "1.0.0-alpha01"
APPCOMPAT = "1.7.0-alpha02"
APPSEARCH = "1.1.0-alpha03"
-ARCH_CORE = "2.2.0-alpha02"
+ARCH_CORE = "2.2.0-beta01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
AUTOFILL = "1.2.0-beta02"
BENCHMARK = "1.2.0-alpha10"
@@ -53,7 +53,8 @@
DYNAMICANIMATION = "1.1.0-alpha04"
DYNAMICANIMATION_KTX = "1.0.0-alpha04"
EMOJI = "1.2.0-alpha03"
-EMOJI2 = "1.3.0-alpha02"
+EMOJI2 = "1.3.0-beta01"
+EMOJI2_QUARANTINE = "1.0.0-alpha01"
ENTERPRISE = "1.1.0-rc01"
EXIFINTERFACE = "1.4.0-alpha01"
FRAGMENT = "1.6.0-alpha05"
diff --git a/lifecycle/lifecycle-livedata-core/api/current.txt b/lifecycle/lifecycle-livedata-core/api/current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/current.txt
@@ -25,8 +25,8 @@
method public void setValue(T!);
}
- public interface Observer<T> {
- method public void onChanged(T!);
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
}
}
diff --git a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
@@ -25,8 +25,8 @@
method public void setValue(T!);
}
- public interface Observer<T> {
- method public void onChanged(T!);
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
}
}
diff --git a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
@@ -25,8 +25,8 @@
method public void setValue(T!);
}
- public interface Observer<T> {
- method public void onChanged(T!);
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
}
}
diff --git a/lifecycle/lifecycle-livedata-core/build.gradle b/lifecycle/lifecycle-livedata-core/build.gradle
index be5361f..fba1c86 100644
--- a/lifecycle/lifecycle-livedata-core/build.gradle
+++ b/lifecycle/lifecycle-livedata-core/build.gradle
@@ -19,9 +19,11 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("org.jetbrains.kotlin.android")
}
dependencies {
+ api(libs.kotlinStdlib)
implementation("androidx.arch.core:core-common:2.1.0")
implementation("androidx.arch.core:core-runtime:2.1.0")
api(project(":lifecycle:lifecycle-common"))
diff --git a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
similarity index 72%
rename from lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
rename to lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
index 30afeedf..2df6295 100644
--- a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
+++ b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
@@ -13,20 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.lifecycle;
+package androidx.lifecycle
/**
- * A simple callback that can receive from {@link LiveData}.
- *
- * @param <T> The type of the parameter
+ * A simple callback that can receive from [LiveData].
*
* @see LiveData LiveData - for a usage description.
- */
-public interface Observer<T> {
+*/
+fun interface Observer<T> {
+
/**
- * Called when the data is changed.
- * @param t The new data
+ * Called when the data is changed is changed to [value].
*/
- void onChanged(T t);
-}
+ fun onChanged(value: T)
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
index 85834a3..874db97 100644
--- a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
+++ b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -954,7 +954,7 @@
private class FailReentranceObserver<T> implements Observer<T> {
@Override
- public void onChanged(@Nullable T t) {
+ public void onChanged(@Nullable T value) {
assertThat(mInObserver, is(false));
}
}
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
index 7791be2..d1193af 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
+++ b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
@@ -104,8 +104,8 @@
private val scopes: ScopesRule
) : Observer<T> {
private var items = mutableListOf<T>()
- override fun onChanged(t: T) {
- items.add(t)
+ override fun onChanged(value: T) {
+ items.add(value)
}
fun assertItems(vararg expected: T) {
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
index 7c8af62..26d7c36 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -120,8 +120,8 @@
result.addSource(this, object : Observer<X> {
var liveData: LiveData<Y>? = null
- override fun onChanged(x: X) {
- val newLiveData = transform(x)
+ override fun onChanged(value: X) {
+ val newLiveData = transform(value)
if (liveData === newLiveData) {
return
}
@@ -149,8 +149,8 @@
result.addSource(this, object : Observer<X> {
var liveData: LiveData<Y>? = null
- override fun onChanged(x: X) {
- val newLiveData = switchMapFunction.apply(x)
+ override fun onChanged(value: X) {
+ val newLiveData = switchMapFunction.apply(value)
if (liveData === newLiveData) {
return
}
@@ -180,14 +180,14 @@
outputLiveData.addSource(this, object : Observer<X> {
var firstTime = true
- override fun onChanged(currentValue: X) {
+ override fun onChanged(value: X) {
val previousValue = outputLiveData.value
if (firstTime ||
- previousValue == null && currentValue != null ||
- previousValue != null && previousValue != currentValue
+ previousValue == null && value != null ||
+ previousValue != null && previousValue != value
) {
firstTime = false
- outputLiveData.value = currentValue
+ outputLiveData.value = value
}
}
})
diff --git a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
index e77c136..4b1a493 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
@@ -238,7 +238,7 @@
int mTimesUpdated;
@Override
- public void onChanged(@Nullable T t) {
+ public void onChanged(@Nullable T value) {
++mTimesUpdated;
}
}
diff --git a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
index dbf4021..5f09eb2 100644
--- a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
+++ b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
@@ -75,7 +75,7 @@
val subscriber: Subscriber<in T>,
val lifecycle: LifecycleOwner,
val liveData: LiveData<T>
- ) : Subscription, Observer<T> {
+ ) : Subscription, Observer<T?> {
@Volatile
var canceled = false
@@ -86,18 +86,18 @@
// used on main thread only
var latest: T? = null
- override fun onChanged(t: T?) {
+ override fun onChanged(value: T?) {
if (canceled) {
return
}
if (requested > 0) {
latest = null
- subscriber.onNext(t)
+ subscriber.onNext(value)
if (requested != Long.MAX_VALUE) {
requested--
}
} else {
- latest = t
+ latest = value
}
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
index 6561594..3e09793 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
@@ -93,7 +93,7 @@
private fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
val controller = LegacySavedStateHandleController
- .create(savedStateRegistry, lifecycle, key, defaultArgs)
+ .create(savedStateRegistry!!, lifecycle!!, key, defaultArgs)
val viewModel = create(key, modelClass, controller.handle)
viewModel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller)
return viewModel
@@ -148,7 +148,7 @@
override fun onRequery(viewModel: ViewModel) {
// is need only for legacy path
if (savedStateRegistry != null) {
- attachHandleIfNeeded(viewModel, savedStateRegistry, lifecycle)
+ attachHandleIfNeeded(viewModel, savedStateRegistry!!, lifecycle!!)
}
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
deleted file mode 100644
index f7f9033..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
-import static androidx.lifecycle.Lifecycle.State.STARTED;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.savedstate.SavedStateRegistry;
-import androidx.savedstate.SavedStateRegistryOwner;
-
-class LegacySavedStateHandleController {
-
- private LegacySavedStateHandleController() {}
-
- static final String TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag";
-
- static SavedStateHandleController create(SavedStateRegistry registry, Lifecycle lifecycle,
- String key, Bundle defaultArgs) {
- Bundle restoredState = registry.consumeRestoredStateForKey(key);
- SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, defaultArgs);
- SavedStateHandleController controller = new SavedStateHandleController(key, handle);
- controller.attachToLifecycle(registry, lifecycle);
- tryToAddRecreator(registry, lifecycle);
- return controller;
- }
-
- static final class OnRecreation implements SavedStateRegistry.AutoRecreated {
-
- @Override
- public void onRecreated(@NonNull SavedStateRegistryOwner owner) {
- if (!(owner instanceof ViewModelStoreOwner)) {
- throw new IllegalStateException(
- "Internal error: OnRecreation should be registered only on components"
- + " that implement ViewModelStoreOwner");
- }
- ViewModelStore viewModelStore = ((ViewModelStoreOwner) owner).getViewModelStore();
- SavedStateRegistry savedStateRegistry = owner.getSavedStateRegistry();
- for (String key : viewModelStore.keys()) {
- ViewModel viewModel = viewModelStore.get(key);
- attachHandleIfNeeded(viewModel, savedStateRegistry, owner.getLifecycle());
- }
- if (!viewModelStore.keys().isEmpty()) {
- savedStateRegistry.runOnNextRecreation(OnRecreation.class);
- }
- }
- }
-
- static void attachHandleIfNeeded(ViewModel viewModel, SavedStateRegistry registry,
- Lifecycle lifecycle) {
- SavedStateHandleController controller = viewModel.getTag(
- TAG_SAVED_STATE_HANDLE_CONTROLLER);
- if (controller != null && !controller.isAttached()) {
- controller.attachToLifecycle(registry, lifecycle);
- tryToAddRecreator(registry, lifecycle);
- }
- }
-
- private static void tryToAddRecreator(SavedStateRegistry registry, Lifecycle lifecycle) {
- Lifecycle.State currentState = lifecycle.getCurrentState();
- if (currentState == INITIALIZED || currentState.isAtLeast(STARTED)) {
- registry.runOnNextRecreation(OnRecreation.class);
- } else {
- lifecycle.addObserver(new LifecycleEventObserver() {
- @Override
- public void onStateChanged(@NonNull LifecycleOwner source,
- @NonNull Lifecycle.Event event) {
- if (event == Lifecycle.Event.ON_START) {
- lifecycle.removeObserver(this);
- registry.runOnNextRecreation(OnRecreation.class);
- }
- }
- });
- }
- }
-}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
new file mode 100644
index 0000000..2f3de01
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle.Companion.createHandle
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+
+internal object LegacySavedStateHandleController {
+ const val TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag"
+
+ @JvmStatic
+ fun create(
+ registry: SavedStateRegistry,
+ lifecycle: Lifecycle,
+ key: String?,
+ defaultArgs: Bundle?
+ ): SavedStateHandleController {
+ val restoredState = registry.consumeRestoredStateForKey(key!!)
+ val handle = createHandle(restoredState, defaultArgs)
+ val controller = SavedStateHandleController(key, handle)
+ controller.attachToLifecycle(registry, lifecycle)
+ tryToAddRecreator(registry, lifecycle)
+ return controller
+ }
+
+ @JvmStatic
+ fun attachHandleIfNeeded(
+ viewModel: ViewModel,
+ registry: SavedStateRegistry,
+ lifecycle: Lifecycle
+ ) {
+ val controller = viewModel.getTag<SavedStateHandleController>(
+ TAG_SAVED_STATE_HANDLE_CONTROLLER
+ )
+ if (controller != null && !controller.isAttached) {
+ controller.attachToLifecycle(registry, lifecycle)
+ tryToAddRecreator(registry, lifecycle)
+ }
+ }
+
+ private fun tryToAddRecreator(registry: SavedStateRegistry, lifecycle: Lifecycle) {
+ val currentState = lifecycle.currentState
+ if (currentState === Lifecycle.State.INITIALIZED ||
+ currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ registry.runOnNextRecreation(OnRecreation::class.java)
+ } else {
+ lifecycle.addObserver(object : LifecycleEventObserver {
+ override fun onStateChanged(
+ source: LifecycleOwner,
+ event: Lifecycle.Event
+ ) {
+ if (event === Lifecycle.Event.ON_START) {
+ lifecycle.removeObserver(this)
+ registry.runOnNextRecreation(OnRecreation::class.java)
+ }
+ }
+ })
+ }
+ }
+
+ internal class OnRecreation : SavedStateRegistry.AutoRecreated {
+ override fun onRecreated(owner: SavedStateRegistryOwner) {
+ check(owner is ViewModelStoreOwner) {
+ ("Internal error: OnRecreation should be registered only on components " +
+ "that implement ViewModelStoreOwner")
+ }
+ val viewModelStore = (owner as ViewModelStoreOwner).viewModelStore
+ val savedStateRegistry = owner.savedStateRegistry
+ for (key in viewModelStore.keys()) {
+ val viewModel = viewModelStore[key]
+ attachHandleIfNeeded(viewModel!!, savedStateRegistry, owner.lifecycle)
+ }
+ if (viewModelStore.keys().isNotEmpty()) {
+ savedStateRegistry.runOnNextRecreation(OnRecreation::class.java)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
index 2e22611..aa24f21 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
@@ -156,12 +156,11 @@
*/
fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
// empty constructor was called.
- if (lifecycle == null) {
- throw UnsupportedOperationException(
+ val lifecycle = lifecycle
+ ?: throw UnsupportedOperationException(
"SavedStateViewModelFactory constructed with empty constructor supports only " +
"calls to create(modelClass: Class<T>, extras: CreationExtras)."
)
- }
val isAndroidViewModel = AndroidViewModel::class.java.isAssignableFrom(modelClass)
val constructor: Constructor<T>? = if (isAndroidViewModel && application != null) {
findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE)
@@ -169,14 +168,13 @@
findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)
}
// doesn't need SavedStateHandle
- if (constructor == null) {
- // If you are using a stateful constructor and no application is available, we
+ constructor
+ ?: // If you are using a stateful constructor and no application is available, we
// use an instance factory instead.
return if (application != null) factory.create(modelClass)
- else instance.create(modelClass)
- }
+ else instance.create(modelClass)
val controller = LegacySavedStateHandleController.create(
- savedStateRegistry, lifecycle, key, defaultArgs
+ savedStateRegistry!!, lifecycle, key, defaultArgs
)
val viewModel: T = if (isAndroidViewModel && application != null) {
newInstance(modelClass, constructor, application!!, controller.handle)
@@ -212,8 +210,8 @@
if (lifecycle != null) {
LegacySavedStateHandleController.attachHandleIfNeeded(
viewModel,
- savedStateRegistry,
- lifecycle
+ savedStateRegistry!!,
+ lifecycle!!
)
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index ad6bb9d..3d85b4c 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -63,6 +63,10 @@
/**
* Shows the system output switcher dialog.
*
+ * <p>See
+ * <a href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">
+ * Output Switcher documentation</a> for more details.
+ *
* @param context Android context
* @return {@code true} if the dialog was shown successfully and {@code false} otherwise
*/
diff --git a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
index 57d781b..99c67e7 100644
--- a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
@@ -72,8 +72,8 @@
// it to fail before we check for test failure.
val liveData = viewModel.installMonitor!!.status
val observer = object : Observer<SplitInstallSessionState> {
- override fun onChanged(state: SplitInstallSessionState) {
- if (state.status() == SplitInstallSessionStatus.FAILED) {
+ override fun onChanged(value: SplitInstallSessionState) {
+ if (value.status() == SplitInstallSessionStatus.FAILED) {
liveData.removeObserver(this)
failureCountdownLatch.countDown()
}
diff --git a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
index c550abe..e0caf39 100644
--- a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
@@ -126,59 +126,60 @@
private inner class StateObserver constructor(private val monitor: DynamicInstallMonitor) :
Observer<SplitInstallSessionState> {
- override fun onChanged(sessionState: SplitInstallSessionState?) {
- if (sessionState != null) {
- if (sessionState.hasTerminalStatus()) {
- monitor.status.removeObserver(this)
+ override fun onChanged(
+ @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
+ sessionState: SplitInstallSessionState
+ ) {
+ if (sessionState.hasTerminalStatus()) {
+ monitor.status.removeObserver(this)
+ }
+ when (sessionState.status()) {
+ SplitInstallSessionStatus.INSTALLED -> {
+ onInstalled()
+ navigate()
}
- when (sessionState.status()) {
- SplitInstallSessionStatus.INSTALLED -> {
- onInstalled()
- navigate()
- }
- SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
- try {
- val splitInstallManager = monitor.splitInstallManager
- if (splitInstallManager == null) {
- onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
- return
- }
- splitInstallManager.startConfirmationDialogForResult(
- sessionState,
- IntentSenderForResultStarter { intent,
- _,
- fillInIntent,
- flagsMask,
- flagsValues,
- _,
- _ ->
- intentSenderLauncher.launch(
- IntentSenderRequest.Builder(intent)
- .setFillInIntent(fillInIntent)
- .setFlags(flagsValues, flagsMask)
- .build()
- )
- },
- INSTALL_REQUEST_CODE
- )
- } catch (e: IntentSender.SendIntentException) {
+ SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
+ try {
+ val splitInstallManager = monitor.splitInstallManager
+ if (splitInstallManager == null) {
onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+ return
}
- SplitInstallSessionStatus.CANCELED -> onCancelled()
- SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
- SplitInstallSessionStatus.UNKNOWN ->
- onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
- SplitInstallSessionStatus.CANCELING,
- SplitInstallSessionStatus.DOWNLOADED,
- SplitInstallSessionStatus.DOWNLOADING,
- SplitInstallSessionStatus.INSTALLING,
- SplitInstallSessionStatus.PENDING -> {
- onProgress(
- sessionState.status(),
- sessionState.bytesDownloaded(),
- sessionState.totalBytesToDownload()
+ splitInstallManager.startConfirmationDialogForResult(
+ sessionState,
+ IntentSenderForResultStarter { intent,
+ _,
+ fillInIntent,
+ flagsMask,
+ flagsValues,
+ _,
+ _ ->
+ intentSenderLauncher.launch(
+ IntentSenderRequest.Builder(intent)
+ .setFillInIntent(fillInIntent)
+ .setFlags(flagsValues, flagsMask)
+ .build()
+ )
+ },
+ INSTALL_REQUEST_CODE
)
+ } catch (e: IntentSender.SendIntentException) {
+ onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
}
+ SplitInstallSessionStatus.CANCELED -> onCancelled()
+ SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
+ SplitInstallSessionStatus.UNKNOWN ->
+ onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+ SplitInstallSessionStatus.CANCELING,
+ SplitInstallSessionStatus.DOWNLOADED,
+ SplitInstallSessionStatus.DOWNLOADING,
+ SplitInstallSessionStatus.INSTALLING,
+ SplitInstallSessionStatus.PENDING -> {
+ onProgress(
+ sessionState.status(),
+ sessionState.bytesDownloaded(),
+ sessionState.totalBytesToDownload()
+ )
}
}
}
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 523d14e..168482b 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -591,6 +591,189 @@
@UiThreadTest
@Test
+ fun testPopBackStack() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}") {
+ argument("arg") { type = NavType.StringType }
+ }
+ }
+
+ // first nav with arg filed in
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+ navController.navigate(deepLink)
+
+ // second nav with arg filled in
+ val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+ navController.navigate(deepLink2)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13", "second_test/18"]
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack("second_test/{arg}", true)
+ assertThat(popped).isTrue()
+ // only last entry with "second_test/{arg}" has been popped
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithExactRoute() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}") {
+ argument("arg") { type = NavType.StringType }
+ }
+ }
+
+ // first nav with arg filed in
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+ navController.navigate(deepLink)
+
+ // second nav with arg filled in
+ val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+ navController.navigate(deepLink2)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13", "second_test/18"]
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack("second_test/13", true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithExactRoute_multiArgs() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}/{arg2}") {
+ argument("arg") { type = NavType.StringType }
+ argument("arg2") { type = NavType.StringType }
+ }
+ }
+
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+ navController.navigate(deepLink)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13/18"]
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ val popped = navController.popBackStack("second_test/13/18", true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithPartialExactRoute() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}/{arg2}") {
+ argument("arg") { type = NavType.StringType }
+ argument("arg2") { type = NavType.StringType }
+ }
+ }
+
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+ navController.navigate(deepLink)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13/{arg2}"]
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ val popped = navController.popBackStack("second_test/13/{arg2}", true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithIncorrectExactRoute() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}/{arg2}") {
+ argument("arg") { type = NavType.StringType }
+ argument("arg2") { type = NavType.StringType }
+ }
+ }
+
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+ navController.navigate(deepLink)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13/18"]
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ val popped = navController.popBackStack("second_test/13/19", true)
+ assertThat(popped).isFalse()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithAdditionalPartialArgs() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}/{arg2}") {
+ argument("arg") { type = NavType.StringType }
+ argument("arg2") { type = NavType.StringType }
+ }
+ }
+
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+ navController.navigate(deepLink)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13/{arg2}"]
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ val popped = navController.popBackStack("second_test/13/18", true)
+ assertThat(popped).isFalse()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithMissingPartialArgs() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(route = "nav_root", startDestination = "start_test") {
+ test("start_test")
+ test("second_test/{arg}/{arg2}") {
+ argument("arg") { type = NavType.StringType }
+ argument("arg2") { type = NavType.StringType }
+ }
+ }
+
+ val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+ navController.navigate(deepLink)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ // ["start_test", "second_test/13/18"]
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ val popped = navController.popBackStack("second_test/13/{arg2}", true)
+ assertThat(popped).isFalse()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateViaDeepLinkDefaultArgs() {
val navController = createNavController()
navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 126bf8f..d7d03d6 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -506,7 +506,12 @@
route: String,
inclusive: Boolean,
saveState: Boolean = false
- ): Boolean = popBackStack(createRoute(route).hashCode(), inclusive, saveState)
+ ): Boolean {
+ val popped = popBackStackInternal(route, inclusive, saveState)
+ // Only return true if the pop succeeded and we've dispatched
+ // the change to a new destination
+ return popped && dispatchOnDestinationChanged()
+ }
/**
* Attempts to pop the controller's back stack back to a specific destination. This does
@@ -564,6 +569,55 @@
return executePopOperations(popOperations, foundDestination, inclusive, saveState)
}
+ /**
+ * Attempts to pop the controller's back stack back to a specific destination. This does
+ * **not** handle calling [dispatchOnDestinationChanged]
+ *
+ * @param route The topmost destination with this route to retain
+ * @param inclusive Whether the given destination should also be popped.
+ * @param saveState Whether the back stack and the state of all destinations between the
+ * current destination and the destination with [route] should be saved for later to be
+ * restored via [NavOptions.Builder.setRestoreState] or the `restoreState` attribute using
+ * the [NavDestination.id] of the destination with this route (note: this matching ID
+ * is true whether [inclusive] is true or false).
+ *
+ * @return true if the stack was popped at least once, false otherwise
+ */
+ private fun popBackStackInternal(
+ route: String,
+ inclusive: Boolean,
+ saveState: Boolean,
+ ): Boolean {
+ if (backQueue.isEmpty()) {
+ // Nothing to pop if the back stack is empty
+ return false
+ }
+
+ val popOperations = mutableListOf<Navigator<*>>()
+ val foundDestination = backQueue.lastOrNull { entry ->
+ val hasRoute = entry.destination.hasRoute(route, entry.arguments)
+ if (inclusive || !hasRoute) {
+ val navigator = _navigatorProvider.getNavigator<Navigator<*>>(
+ entry.destination.navigatorName
+ )
+ popOperations.add(navigator)
+ }
+ hasRoute
+ }?.destination
+
+ if (foundDestination == null) {
+ // We were passed a route that doesn't exist on our back stack.
+ // Better to ignore the popBackStack than accidentally popping the entire stack
+ Log.i(
+ TAG,
+ "Ignoring popBackStack to route $route as it was not found " +
+ "on the current back stack"
+ )
+ return false
+ }
+ return executePopOperations(popOperations, foundDestination, inclusive, saveState)
+ }
+
private fun executePopOperations(
popOperations: List<Navigator<*>>,
foundDestination: NavDestination,
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 5782e08..19c669b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -40,7 +40,7 @@
/** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
private val typeParser = TypeParser(logger)
- private val interfaceParser = InterfaceParser(logger, typeParser)
+ private val interfaceParser = InterfaceParser(logger, typeParser, resolver)
private val valueParser = ValueParser(logger, typeParser)
fun parseApi(): ParsedApi {
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 cb2b9d0..7455049 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
@@ -23,13 +23,18 @@
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isPublic
import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
-internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
+internal class InterfaceParser(
+ private val logger: KSPLogger,
+ private val typeParser: TypeParser,
+ private val resolver: Resolver,
+) {
private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
@@ -71,6 +76,17 @@
})."
)
}
+ if (interfaceDeclaration.superTypes.singleOrNull()?.resolve()
+ != resolver.builtIns.anyType
+ ) {
+ logger.error(
+ "Error in $name: annotated interface inherits prohibited types (${
+ interfaceDeclaration.superTypes.map {
+ it.resolve().declaration.simpleName.getShortName()
+ }.sorted().joinToString(limit = 3)
+ })."
+ )
+ }
val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
return AnnotatedInterface(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index c6be61c..e473360 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
*/
fun compileWithPrivacySandboxKspCompiler(
sources: List<Source>,
- addSdkRuntimeLibraryStubs: Boolean = true,
+ addLibraryStubs: Boolean = true,
extraProcessorOptions: Map<String, String> = mapOf(),
): TestCompilationResult {
val provider = PrivacySandboxKspCompiler.Provider()
@@ -41,7 +41,8 @@
}
return CompilationTestHelper.compileAll(
- if (addSdkRuntimeLibraryStubs) sources + syntheticSdkRuntimeLibraryStubs else sources,
+ if (addLibraryStubs) sources + syntheticSdkRuntimeLibraryStubs + syntheticUiLibraryStubs
+ else sources,
symbolProcessorProviders = listOf(provider),
processorOptions = processorOptions,
)
@@ -88,5 +89,57 @@
| ): View
|}
|""".trimMargin()
- )
+ ),
+)
+
+private val syntheticUiLibraryStubs = listOf(
+ Source.kotlin(
+ "androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt", """
+ |package androidx.privacysandbox.ui.core
+ |
+ |import android.content.Context
+ |import android.view.View
+ |import java.util.concurrent.Executor
+ |
+ |interface SandboxedUiAdapter {
+ | fun openSession(
+ | context: Context,
+ | initialWidth: Int,
+ | initialHeight: Int,
+ | isZOrderOnTop: Boolean,
+ | clientExecutor: Executor,
+ | client: SessionClient
+ | )
+ |
+ |
+ | interface Session {
+ | fun close()
+ | val view: View
+ | }
+ |
+ | interface SessionClient {
+ | fun onSessionError(throwable: Throwable);
+ | fun onSessionOpened(session: Session);
+ | }
+ |}
+ |""".trimMargin()
+ ),
+ Source.kotlin(
+ "androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt", """
+ |package androidx.privacysandbox.ui.core
+ |
+ |import androidx.annotation.RestrictTo
+ |
+ |object SdkRuntimeUiLibVersions {
+ | var clientVersion: Int = -1
+ | /**
+ | * @hide
+ | */
+ | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ | set
+ |
+ | const val apiVersion: Int = 1
+ |}
+ |""".trimMargin()
+ ),
)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
index aeeca05..388e130 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
@@ -34,7 +34,7 @@
val result = compileWithPrivacySandboxKspCompiler(
inputSources,
- addSdkRuntimeLibraryStubs = false,
+ addLibraryStubs = false,
extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
)
assertThat(result).succeeds()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index 93b2d53..776d7b3 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -202,6 +202,49 @@
}
@Test
+ fun interfaceInheritance_fails() {
+ val source = Source.kotlin(
+ "com/mysdk/MySdk.kt", """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+
+ interface FooInterface {}
+
+ @PrivacySandboxService
+ interface MySdk : FooInterface {
+ suspend fun foo(): Int
+ }"""
+ )
+ checkSourceFails(source).containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (" +
+ "FooInterface)."
+ )
+ }
+
+ @Test
+ fun interfaceInheritsManyInterfaces_fails() {
+ val source = Source.kotlin(
+ "com/mysdk/MySdk.kt", """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+
+ interface A {}
+ interface B {}
+ interface C {}
+ interface D {}
+
+ @PrivacySandboxService
+ interface MySdk : B, C, D, A {
+ suspend fun foo(): Int
+ }"""
+ )
+ checkSourceFails(source).containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (A, B, C, " +
+ "...)."
+ )
+ }
+
+ @Test
fun methodWithImplementation_fails() {
checkSourceFails(serviceMethod("suspend fun foo(): Int = 1")).containsExactlyErrors(
"Error in com.mysdk.MySdk.foo: method cannot have default implementation."
@@ -223,7 +266,7 @@
}
@Test
- fun parameterWitDefaultValue_fails() {
+ fun parameterWithDefaultValue_fails() {
checkSourceFails(serviceMethod("suspend fun foo(x: Int = 5)")).containsExactlyErrors(
"Error in com.mysdk.MySdk.foo: parameters cannot have default values."
)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
index aabc29a..98b9f79 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
@@ -29,8 +29,8 @@
val latch = CountDownLatch(1)
var data: T? = null
val observer = object : Observer<T> {
- override fun onChanged(o: T?) {
- data = o
+ override fun onChanged(value: T) {
+ data = value
liveData.removeObserver(this)
latch.countDown()
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
index c3005bf..2b89222 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
@@ -27,8 +27,8 @@
mLastData = null
}
- override fun onChanged(o: T?) {
- mLastData = o
+ override fun onChanged(value: T) {
+ mLastData = value
mHasValue = true
}
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
index 63cf794..67fee43 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
@@ -487,9 +487,10 @@
private fun setupBehaviorSpinner() {
val types = mapOf(
- "BY TOUCH" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
- "BY SWIPE" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
+ "DEFAULT" to WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
"TRANSIENT" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+ "BY TOUCH (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
+ "BY SWIPE (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
)
findViewById<Spinner>(R.id.spn_behavior).apply {
adapter = ArrayAdapter(
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index 7c3174f..b14edf9 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,7 +38,6 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -251,7 +250,6 @@
assertThat(state.canScrollBackward).isFalse()
}
- @Ignore("b/259608530")
@Test
fun canScrollBackward() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index 93719d4..1d2de6dc 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -250,10 +250,12 @@
spanLayoutProvider = spanLayoutProvider
)
+ val lastVisibleItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
return TvLazyGridMeasureResult(
firstVisibleLine = firstLine,
firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
- canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
+ canScrollForward =
+ lastVisibleItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index 6e600c6..adb434b 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -16,6 +16,8 @@
package androidx.tv.material3
+import android.os.SystemClock
+import android.view.KeyEvent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
@@ -57,6 +59,7 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -616,6 +619,50 @@
rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
}
+ fun carousel_manualScrolling_onDpadLongPress() {
+ rule.setContent {
+ SampleCarousel(slideCount = 6) { index ->
+ SampleButton("Button ${index + 1}")
+ }
+ }
+
+ // Request focus for Carousel on start
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag("pager")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+
+ // Trigger recomposition after requesting focus
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+
+ // Assert that Button 1 from first slide is focused
+ rule.onNodeWithText("Button 1").assertIsFocused()
+
+ // Trigger dpad right key long press
+ performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+ // Advance time and trigger recomposition to switch to next slide
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+ rule.waitForIdle()
+
+ // Assert that Button 2 from second slide is focused
+ rule.onNodeWithText("Button 2").assertIsFocused()
+
+ // Trigger dpad left key long press
+ performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+ // Advance time and trigger recomposition to switch to previous slide
+ rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+
+ // Assert that Button 1 from first slide is focused
+ rule.onNodeWithText("Button 1").assertIsFocused()
+ }
+
@Test
fun carousel_manualScrolling_ltr() {
rule.setContent {
@@ -804,3 +851,35 @@
afterEachPress()
}
}
+
+private fun performLongKeyPress(
+ rule: ComposeContentTestRule,
+ keyCode: Int,
+ count: Int = 1
+) {
+ repeat(count) {
+ // Trigger the first key down event to simulate key press
+ val firstKeyDownEvent = KeyEvent(
+ SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+ KeyEvent.ACTION_DOWN, keyCode, 0, 0, 0, 0
+ )
+ rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(firstKeyDownEvent))
+ rule.waitForIdle()
+
+ // Trigger multiple key down events with repeat count (>0) to simulate key long press
+ val repeatedKeyDownEvent = KeyEvent(
+ SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+ KeyEvent.ACTION_DOWN, keyCode, 5, 0, 0, 0
+ )
+ rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(repeatedKeyDownEvent))
+ rule.waitForIdle()
+
+ // Trigger the final key up event to simulate key release
+ val keyUpEvent = KeyEvent(
+ SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+ KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0
+ )
+ rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(keyUpEvent))
+ rule.waitForIdle()
+ }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index ffcef53..70c5480 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -241,16 +241,30 @@
KeyEventPropagation.ContinuePropagation
}
- KEYCODE_DPAD_LEFT -> if (isLtr) {
- showPreviousSlideAndGetKeyEventPropagation()
- } else {
- showNextSlideAndGetKeyEventPropagation()
+ KEYCODE_DPAD_LEFT -> {
+ // Ignore long press key event for manual scrolling
+ if (it.nativeKeyEvent.repeatCount > 0) {
+ return@onKeyEvent KeyEventPropagation.StopPropagation
+ }
+
+ if (isLtr) {
+ showPreviousSlideAndGetKeyEventPropagation()
+ } else {
+ showNextSlideAndGetKeyEventPropagation()
+ }
}
- KEYCODE_DPAD_RIGHT -> if (isLtr) {
- showNextSlideAndGetKeyEventPropagation()
- } else {
- showPreviousSlideAndGetKeyEventPropagation()
+ KEYCODE_DPAD_RIGHT -> {
+ // Ignore long press key event for manual scrolling
+ if (it.nativeKeyEvent.repeatCount > 0) {
+ return@onKeyEvent KeyEventPropagation.StopPropagation
+ }
+
+ if (isLtr) {
+ showNextSlideAndGetKeyEventPropagation()
+ } else {
+ showPreviousSlideAndGetKeyEventPropagation()
+ }
}
else -> KeyEventPropagation.ContinuePropagation
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index b88eadc..4fd0c50 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -241,6 +241,56 @@
}
@Test
+ public fun complicationSettingsWithIndices() {
+ val one = UserStyleSetting.Id("one")
+ val two = UserStyleSetting.Id("two")
+ val schema = UserStyleSchema(
+ listOf(
+ ListUserStyleSetting(
+ one,
+ context.resources,
+ R.string.ith_style,
+ R.string.ith_style_screen_reader_name,
+ icon = null,
+ options = listOf(
+ ListOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null
+ )
+ ),
+ listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+ ),
+ ComplicationSlotsUserStyleSetting(
+ two,
+ context.resources,
+ R.string.ith_style,
+ R.string.ith_style_screen_reader_name,
+ icon = null,
+ complicationConfig = listOf(
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null,
+ emptyList()
+ )
+ ),
+ listOf(WatchFaceLayer.COMPLICATIONS)
+ )
+ )
+ )
+
+ Truth.assertThat(schema[one]!!.displayName).isEqualTo("1st style")
+ Truth.assertThat(schema[one]!!.description).isEqualTo("1st style setting")
+ Truth.assertThat(schema[two]!!.displayName).isEqualTo("2nd style")
+ Truth.assertThat(schema[two]!!.description).isEqualTo("2nd style setting")
+ }
+
+ @Test
@Suppress("deprecation")
public fun
complicationsUserStyleSettingWireFormatRoundTrip_noScreenReaderName_filledByDisplayName() {
diff --git a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
index 3133a45..aa37205 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -36,6 +36,9 @@
<string name="ith_option" translatable="false">%1$s option</string>
<string name="ith_option_screen_reader_name" translatable="false">%1$s list option</string>
+ <string name="ith_style" translatable="false">%1$s style</string>
+ <string name="ith_style_screen_reader_name" translatable="false">%1$s style setting</string>
+
<!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
<string name="colors_style_red">Red</string>
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index d5c8fff..fe8e80f 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -432,7 +432,6 @@
public class UserStyleSchema constructor(
userStyleSettings: List<UserStyleSetting>
) {
-
public val userStyleSettings = userStyleSettings
@Deprecated("use rootUserStyleSettings instead")
get
@@ -503,7 +502,11 @@
init {
var complicationSlotsUserStyleSettingCount = 0
var customValueUserStyleSettingCount = 0
+ var displayNameIndex = 1
for (setting in userStyleSettings) {
+ // Provide the ordinal used by fallback descriptions for each setting.
+ setting.setDisplayNameIndex(displayNameIndex++)
+
when (setting) {
is UserStyleSetting.ComplicationSlotsUserStyleSetting ->
complicationSlotsUserStyleSettingCount++
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 483ff43..097a0b2e 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -177,6 +177,16 @@
}
}
+ internal fun setDisplayNameIndex(index: Int) {
+ if (displayNameInternal is DisplayText.ResourceDisplayTextWithIndex) {
+ displayNameInternal.setIndex(index)
+ }
+
+ if (descriptionInternal is DisplayText.ResourceDisplayTextWithIndex) {
+ descriptionInternal.setIndex(index)
+ }
+ }
+
/**
* Optional data for an on watch face editor (not the companion editor).
*
@@ -788,8 +798,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(
id,
- DisplayText.ResourceDisplayText(resources, displayNameResourceId),
- DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
icon,
watchFaceEditorData,
listOf(BooleanOption.TRUE, BooleanOption.FALSE),
@@ -1351,8 +1361,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : this(
id,
- DisplayText.ResourceDisplayText(resources, displayNameResourceId),
- DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
icon,
watchFaceEditorData,
complicationConfig,
@@ -1952,8 +1962,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(
id,
- DisplayText.ResourceDisplayText(resources, displayNameResourceId),
- DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
icon,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
@@ -2155,8 +2165,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(
id,
- DisplayText.ResourceDisplayText(resources, displayNameResourceId),
- DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
icon,
watchFaceEditorData,
options,
@@ -2739,8 +2749,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(
id,
- DisplayText.ResourceDisplayText(resources, displayNameResourceId),
- DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
icon,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 541c0da..6ed0000 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -44,6 +44,7 @@
import androidx.wear.watchface.complications.SystemDataSources
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.toApiComplicationData
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
import androidx.wear.watchface.control.data.ComplicationRenderParams
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WatchFaceRenderParams
@@ -207,8 +208,8 @@
watchFaceService.setContext(context)
val engine = watchFaceService.createHeadlessEngine() as
WatchFaceService.EngineWrapper
- engine.createHeadlessInstance(params)
- return engine.deferredWatchFaceImpl.await().WFEditorDelegate()
+ val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
+ return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
}
}
}
@@ -727,7 +728,10 @@
}
if (!watchState.isHeadless) {
- WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+ WatchFace.registerEditorDelegate(
+ componentName,
+ WFEditorDelegate(headlessWatchFaceImpl = null)
+ )
registerReceivers()
}
@@ -778,7 +782,9 @@
}
}
- internal inner class WFEditorDelegate : WatchFace.EditorDelegate {
+ internal inner class WFEditorDelegate(
+ private val headlessWatchFaceImpl: HeadlessWatchFaceImpl?
+ ) : WatchFace.EditorDelegate {
override val userStyleSchema
get() = currentUserStyleRepository.schema
@@ -849,8 +855,10 @@
complicationSlotsManager.configExtrasChangeCallback = callback
}
+ @SuppressLint("NewApi") // release
override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
if (watchState.isHeadless) {
+ headlessWatchFaceImpl!!.release()
[email protected]()
}
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index a5f3897..2c2540b 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -55,7 +55,7 @@
indentingPrintWriter.decreaseIndent()
}
- private val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
+ internal val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
}
init {
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index fd9bfe2..48c3f56 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -18,6 +18,7 @@
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
@@ -69,6 +70,7 @@
import androidx.wear.watchface.complications.data.TimeDifferenceStyle
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
import androidx.wear.watchface.control.IInteractiveWatchFace
import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.IWatchfaceListener
@@ -6578,6 +6580,42 @@
.isInstanceOf(ShortTextComplicationData::class.java)
}
+ @SuppressLint("NewApi")
+ @Test
+ public fun createHeadlessSessionDelegate_onDestroy() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val componentName = ComponentName(context, TestNopCanvasWatchFaceService::class.java)
+ lateinit var delegate: WatchFace.EditorDelegate
+
+ // Allows us to programmatically control tasks.
+ TestNopCanvasWatchFaceService.handler = this.handler
+
+ CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
+ delegate = WatchFace.createHeadlessSessionDelegate(
+ componentName,
+ HeadlessWatchFaceInstanceParams(
+ componentName,
+ DeviceConfig(false, false, 100, 200),
+ 100,
+ 100,
+ null
+ ),
+ context
+ )
+ }
+
+ // Run all pending tasks.
+ while (pendingTasks.isNotEmpty()) {
+ pendingTasks.remove().runnable.run()
+ }
+
+ assertThat(HeadlessWatchFaceImpl.headlessInstances).isNotEmpty()
+ delegate.onDestroy()
+
+ // The headlessInstances should become empty, otherwise there's a leak.
+ assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
+ }
+
private fun getLeftShortTextComplicationDataText(): CharSequence {
val complication = complicationSlotsManager[
LEFT_COMPLICATION_ID
@@ -6612,3 +6650,50 @@
private suspend fun Flow<ComplicationData>.firstNonEmpty(): ComplicationData =
withTimeout(1000) { dropWhile { it is NoDataComplicationData }.first() }
}
+
+class TestNopCanvasWatchFaceService : WatchFaceService() {
+ companion object {
+ lateinit var handler: Handler
+ }
+
+ override fun getUiThreadHandlerImpl() = handler
+
+ // To make unit tests simpler and non-flaky we run background tasks and ui tasks on the same
+ // handler.
+ override fun getBackgroundThreadHandlerImpl() = handler
+
+ override suspend fun createWatchFace(
+ surfaceHolder: SurfaceHolder,
+ watchState: WatchState,
+ complicationSlotsManager: ComplicationSlotsManager,
+ currentUserStyleRepository: CurrentUserStyleRepository
+ ) = WatchFace(
+ WatchFaceType.DIGITAL,
+ @Suppress("deprecation")
+ object : Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.HARDWARE,
+ 16
+ ) {
+ override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+ // Intentionally empty.
+ }
+
+ override fun renderHighlightLayer(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime
+ ) {
+ // Intentionally empty.
+ }
+ }
+ )
+
+ override fun getSystemTimeProvider() = object : SystemTimeProvider {
+ override fun getSystemTimeMillis() = 123456789L
+
+ override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+ }
+}
\ No newline at end of file
diff --git a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
index 66a0dd2..f51e4cf 100644
--- a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
+++ b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
@@ -133,13 +133,13 @@
owner,
object : Observer<T> {
private var lastValue: T? = null
- override fun onChanged(t: T) {
- if (t == null) {
+ override fun onChanged(value: T) {
+ if (value == null) {
removeObserver(this)
} else {
executor.execute {
- listener(lastValue, t)
- lastValue = t
+ listener(lastValue, value)
+ lastValue = value
}
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
index bd21bae..c186329 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
@@ -124,7 +124,7 @@
int mTimesUpdated;
@Override
- public void onChanged(@Nullable T t) {
+ public void onChanged(@Nullable T value) {
++mTimesUpdated;
}
}