Merge "Annotate flaky tests" into androidx-main
diff --git a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
index 1822def..76be17e 100644
--- a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
+++ b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
@@ -1126,4 +1126,13 @@
file="src/main/java/sample/optin/UseKtExperimentalFromJava.java"/>
</issue>
+ <issue
+ id="WrongRequiresOptIn"
+ message="Experimental annotation should use kotlin.RequiresOptIn"
+ errorLine1="annotation class ExperimentalKotlinAnnotationWrongAnnotation"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt"/>
+ </issue>
+
</issues>
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
similarity index 71%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
index 4f92192..722bfcb 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
@@ -14,4 +14,9 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package sample.kotlin
+
+@Retention(AnnotationRetention.BINARY)
+@Suppress("unused")
+@androidx.annotation.RequiresOptIn(level = androidx.annotation.RequiresOptIn.Level.ERROR)
+annotation class ExperimentalKotlinAnnotationWrongAnnotation
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
index 94ec534..50643b2 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
@@ -48,12 +48,18 @@
private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
override fun visitAnnotation(node: UAnnotation) {
- when (node.qualifiedName) {
+ val annotated = node.uastParent as? UAnnotated ?: return
+ val isKotlin = isKotlin(annotated.sourcePsi)
+ val qualifiedName = node.qualifiedName
+
+ if (isKotlin && qualifiedName == JAVA_REQUIRES_OPT_IN_ANNOTATION) {
+ reportKotlinUsage(annotated)
+ }
+
+ when (qualifiedName) {
KOTLIN_EXPERIMENTAL_ANNOTATION, KOTLIN_REQUIRES_OPT_IN_ANNOTATION,
JAVA_EXPERIMENTAL_ANNOTATION, JAVA_REQUIRES_OPT_IN_ANNOTATION -> {
- (node.uastParent as? UAnnotated)?.let { annotated ->
- validateAnnotationRetention(annotated)
- }
+ validateAnnotationRetention(annotated)
}
}
}
@@ -88,7 +94,7 @@
}?.extractAttribute(context, "value") ?: defaultRetention
if (expectedRetention != actualRetention) {
- report(
+ reportRetention(
annotated,
formatRetention(expectedRetention, defaultRetention),
formatRetention(actualRetention, defaultRetention),
@@ -108,16 +114,39 @@
* Reports an issue with the [annotated] element where the [expected] retention does not
* match the [actual] retention.
*/
- private fun report(annotated: UAnnotated, expected: String, actual: String) {
+ private fun reportRetention(annotated: UAnnotated, expected: String, actual: String) {
context.report(
- ISSUE, annotated, context.getNameLocation(annotated),
+ ISSUE_RETENTION, annotated, context.getNameLocation(annotated),
"Experimental annotation has $actual retention, should use $expected"
)
}
+
+ /**
+ * Reports an issue with the [annotated] element where the experimental meta-annotation
+ * should be changed to `kotlin.RequiresOptIn`.
+ */
+ private fun reportKotlinUsage(annotated: UAnnotated) {
+ context.report(
+ ISSUE_KOTLIN_USAGE, annotated, context.getNameLocation(annotated),
+ "Experimental annotation should use kotlin.RequiresOptIn"
+ )
+ }
}
companion object {
- val ISSUE = Issue.create(
+ val ISSUE_KOTLIN_USAGE = Issue.create(
+ "WrongRequiresOptIn",
+ "Experimental annotations defined in Kotlin must use kotlin.RequiresOptIn",
+ """
+ Experimental features defined in Kotlin source code must be annotated with the Kotlin
+ `@RequiresOptIn` annotation. Using `androidx.annotation.RequiresOptIn` will prevent the
+ Kotlin compiler from enforcing its opt-in policies.
+ """,
+ Category.CORRECTNESS, 4, Severity.ERROR,
+ Implementation(AnnotationRetentionDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+
+ val ISSUE_RETENTION = Issue.create(
"ExperimentalAnnotationRetention",
"Experimental annotation with incorrect retention",
"Experimental annotations defined in Java source should use default " +
@@ -126,6 +155,11 @@
Category.CORRECTNESS, 5, Severity.ERROR,
Implementation(AnnotationRetentionDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
+
+ val ISSUES = listOf(
+ ISSUE_RETENTION,
+ ISSUE_KOTLIN_USAGE
+ )
}
}
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
index 83bc43f..0f6ff59 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
@@ -24,7 +24,7 @@
class ExperimentalIssueRegistry : IssueRegistry() {
override val minApi = CURRENT_API
override val api = 13
- override val issues get() = ExperimentalDetector.ISSUES + AnnotationRetentionDetector.ISSUE
+ override val issues get() = ExperimentalDetector.ISSUES + AnnotationRetentionDetector.ISSUES
override val vendor = Vendor(
feedbackUrl = "https://issuetracker.google.com/issues/new?component=459778",
identifier = "androidx.annotation.experimental",
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
index d08a07d..f6ee76c 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
@@ -35,7 +35,7 @@
RequiresOptInDetectorTest.ANDROIDX_OPT_IN_KT,
*testFiles
)
- .issues(AnnotationRetentionDetector.ISSUE)
+ .issues(*AnnotationRetentionDetector.ISSUES.toTypedArray())
.run()
}
@@ -78,4 +78,26 @@
check(*input).expect(expected)
}
+
+ /**
+ * Test for lint check that discourages the use of Java-style opt-in on Kotlin-sourced
+ * annotations.
+ */
+ @Test
+ fun wrongRequiresOptInAnnotation() {
+ val input = arrayOf(
+ ktSample("sample.kotlin.ExperimentalKotlinAnnotationWrongAnnotation"),
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt:22: Error: Experimental annotation should use kotlin.RequiresOptIn [WrongRequiresOptIn]
+annotation class ExperimentalKotlinAnnotationWrongAnnotation
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected)
+ }
}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java
new file mode 100644
index 0000000..e061f95
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.processing;
+
+import android.media.ImageWriter;
+import android.view.Surface;
+
+/**
+ * A handler to a publisher/subscriber pair.
+ *
+ * <p>Upstream units publish the frames to the edge, which will be delivered to the downstream
+ * subscribers. Edge usually contains both the image buffer and the specifications about the
+ * image, such as the size and format of the image buffer.
+ *
+ * <p>One example is Android’s {@link Surface} API. Surface is a handler to a {@code BufferQueue}.
+ * The publisher, e.g. camera, sends images to the {@link Surface} by using APIs such as
+ * {@link ImageWriter}, OpenGL and/or NDK. Subscribers can get the frames by using APIs such as
+ * {@code ImageReader.OnImageAvailableListener} or {@code SurfaceTexture.OnFrameAvailableListener}.
+ */
+public interface Edge {
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
index 34a1f12..ae4f055 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
@@ -24,9 +24,7 @@
import androidx.camera.core.impl.CameraCaptureResult;
/**
- * Base unit for CameraX post-processing.
- *
- * <p>All CameraX post-processing should be wrapped by this interface to explicitly define the I/O.
+ * A pub/sub based unit for CameraX post-processing.
*
* <p>Both {@link I} and {@link O} should include handlers to buffers that
* contain camera frames, as well as the callbacks to notify when the frames are updated. One
@@ -64,11 +62,6 @@
*
* <p>This method will be invoked in {@code UseCase#createPipeline}. For now, {@code
* #createPipeline}s are called on the main thread.
- *
- * <p> Returns {@code null} if the input does not change the current state of the
- * {@link Node}. This usually happens when the input specification can be handled by the
- * previously allocated buffer, thus no new buffer needs to be allocated. The node will
- * provide the existing buffer for the upstream node to write to.
*/
@NonNull
@MainThread
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java
new file mode 100644
index 0000000..759708c
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.processing;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+
+/**
+ * Class for processing a single image frame.
+ *
+ * <p>Both {@link I} and {@link O} contain one or many camera frames and their metadata such as
+ * dimension, format and Exif.
+ *
+ * <p>This is a syntax sugar for the {@link Node} class. The purpose is for building a pipeline
+ * intuitively without using {@link Node}'s publisher/subscriber model.
+ *
+ * @param <I> input image frame
+ * @param <O> output image frame.
+ */
+public interface Processor<I, O> {
+
+ /**
+ * Processes an input frame and produces an output frame.
+ *
+ * <p>The implementation of method performs the image processing operations that usually
+ * blocks the current thread. It must be invoked on a non-blocking thread. e.g.
+ * {@link CameraXExecutors#ioExecutor()}.
+ */
+ @NonNull
+ @WorkerThread
+ O process(@NonNull I i) throws ImageCaptureException;
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index 20bab64..b2af040 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -28,7 +28,7 @@
* A data class represents a {@link Node} output that is based on {@link Surface}s.
*/
@AutoValue
-public abstract class SurfaceEdge {
+public abstract class SurfaceEdge implements Edge {
/**
* Gets output surfaces.
diff --git a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
index 52b5f08..55b0cfd 100644
--- a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
+++ b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
@@ -51,18 +51,18 @@
import java.util.concurrent.Executor;
/**
- * An implementation of {@link ImageAnalysis.Analyzer} with MLKit libraries.
+ * An implementation of {@link ImageAnalysis.Analyzer} with ML Kit libraries.
*
- * <p> This class is a wrapper of one or many MLKit {@code Detector}s. It forwards
+ * <p> This class is a wrapper of one or many ML Kit {@code Detector}s. It forwards
* {@link ImageAnalysis} frames to all the {@code Detector}s sequentially. Once all the
* {@code Detector}s finish analyzing the frame, {@link Consumer#accept} will be
* invoked with the aggregated analysis results.
*
- * <p> This class handles the coordinates transformation between MLKit output and the target
- * coordinate system. Based the {@code targetCoordinateSystem} set in the constructor, it
+ * <p> This class handles the coordinate transformation between ML Kit output and the target
+ * coordinate system. Using the {@code targetCoordinateSystem} set in the constructor, it
* calculates the {@link Matrix} with the value provided by CameraX via
- * {@link ImageAnalysis.Analyzer#updateTransform} and forward it to the MLKit {@code Detector}. The
- * coordinates returned by MLKit will be in the desired coordinate system.
+ * {@link ImageAnalysis.Analyzer#updateTransform} and forwards it to the ML Kit {@code Detector}. The
+ * coordinates returned by MLKit will be in the specified coordinate system.
*
* <p> This class is designed to work seamlessly with the {@code CameraController} class in
* camera-view. When used with {@link ImageAnalysis} in camera-core, the following scenarios may
@@ -79,7 +79,7 @@
* cameraController.setImageAnalysisAnalyzer(executor,
* new MlKitAnalyzer(List.of(barcodeScanner), COORDINATE_SYSTEM_VIEW_REFERENCED,
* executor, result -> {
- * // The value of result.getResult(barcodeScanner) can be used directly for drawying UI layover.
+ * // The value of result.getResult(barcodeScanner) can be used directly for drawing UI overlay.
* });
* </pre></code>
*
@@ -109,23 +109,23 @@
/**
* Constructor of {@link MlKitAnalyzer}.
*
- * <p>The list detectors will be invoked sequentially in order.
+ * <p>The list of detectors will be invoked sequentially in order.
*
* <p>When the targetCoordinateSystem is {@link ImageAnalysis#COORDINATE_SYSTEM_ORIGINAL}, the
- * output coordinate system is defined by MLKit, which is the buffer with rotation applied. For
+ * output coordinate system is defined by ML Kit, which is the buffer with rotation applied. For
* example, if {@link ImageProxy#getHeight()} is {@code h} and the rotation is 90°, (0, 0) in
* the result maps to the pixel (0, h) in the original buffer.
*
* <p>The constructor throws {@link IllegalArgumentException} if
* {@code Detector#getDetectorType()} is TYPE_SEGMENTATION and {@code targetCoordinateSystem}
- * is COORDINATE_SYSTEM_ORIGINAL. Currently MLKit does not support transformation with
+ * is COORDINATE_SYSTEM_ORIGINAL. Currently ML Kit does not support transformation with
* segmentation.
*
- * @param detectors list of MLKit {@link Detector}.
+ * @param detectors list of ML Kit {@link Detector}.
* @param targetCoordinateSystem e.g. {@link ImageAnalysis#COORDINATE_SYSTEM_ORIGINAL}
- * the coordinates in MLKit output will be based on this value.
+ * the coordinates in ML Kit output will be based on this value.
* @param executor on which the consumer is invoked.
- * @param consumer invoked when there is new MLKit result.
+ * @param consumer invoked when there is a new ML Kit result.
*/
@OptIn(markerClass = TransformExperimental.class)
public MlKitAnalyzer(
@@ -262,7 +262,7 @@
/**
* Gets the recommended resolution for the given {@code Detector} type.
*
- * <p> The resolution can be found on MLKit's DAC page.
+ * <p> The resolution can be found on ML Kit's DAC page.
*/
@NonNull
private Size getTargetResolution(int detectorType) {
@@ -314,7 +314,7 @@
}
/**
- * Get the analysis result for the given MLKit {@code Detector}.
+ * Get the analysis result for the given ML Kit {@code Detector}.
*
* <p>Returns {@code null} if the detection is unsuccessful.
*
diff --git a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
index dea5c58..d7bbf31 100644
--- a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
+++ b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
@@ -1,8 +1,8 @@
# Module root
-CameraX MLKit Vision
+CameraX ML Kit Vision
# Package androidx.camera.mlkit.vision
-A library providing a seamless integration between CameraX and Google's MLKit library.
+A library providing a seamless integration between CameraX and Google's ML Kit library.
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
index 4ce892c..dd8bdef 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.car.app.SessionInfo;
+import androidx.core.util.Pair;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
@@ -31,13 +32,15 @@
import java.util.Map;
/**
- * A factory to provide a unique {@link CarAppViewModel} for each given {@link ComponentName}.
+ * A factory to provide a unique {@link CarAppViewModel} for each pair of {@link ComponentName} and
+ * {@link SessionInfo}.
*
* @hide
*/
@RestrictTo(LIBRARY)
class CarAppViewModelFactory implements ViewModelProvider.Factory {
- private static final Map<ComponentName, CarAppViewModelFactory> sInstances = new HashMap<>();
+ private static final Map<Pair<ComponentName, SessionInfo>, CarAppViewModelFactory> sInstances =
+ new HashMap<>();
Application mApplication;
ComponentName mComponentName;
@@ -51,17 +54,19 @@
}
/**
- * Retrieve a singleton instance of CarAppViewModelFactory for the given key.
+ * Retrieve a singleton instance of CarAppViewModelFactory for the given
+ * {@link ComponentName} and {@link SessionInfo}.
*
* @return A valid {@link CarAppViewModelFactory}
*/
@NonNull
static CarAppViewModelFactory getInstance(Application application,
ComponentName componentName, SessionInfo sessionInfo) {
- CarAppViewModelFactory instance = sInstances.get(componentName);
+ Pair<ComponentName, SessionInfo> instanceCacheKey = new Pair<>(componentName, sessionInfo);
+ CarAppViewModelFactory instance = sInstances.get(instanceCacheKey);
if (instance == null) {
instance = new CarAppViewModelFactory(componentName, application, sessionInfo);
- sInstances.put(componentName, instance);
+ sInstances.put(instanceCacheKey, instance);
}
return instance;
}
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
index 8625a22..fc228fe 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
@@ -17,12 +17,14 @@
package androidx.car.app.activity;
import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+import static androidx.car.app.SessionInfo.DISPLAY_TYPE_MAIN;
import static com.google.common.truth.Truth.assertThat;
import android.app.Application;
import android.content.ComponentName;
+import androidx.car.app.SessionInfo;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
@@ -49,18 +51,29 @@
CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
- assertThat(factory1).isEqualTo(factory2);
+ assertThat(factory1).isSameInstanceAs(factory2);
}
@Test
- public void getInstance_differentKeys_returnsDifferent() {
+ public void getInstance_differentComponentNames_returnsDifferent() {
CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
TEST_COMPONENT_NAME_2, DEFAULT_SESSION_INFO);
- assertThat(factory1).isNotEqualTo(factory2);
+ assertThat(factory1).isNotSameInstanceAs(factory2);
+ }
+
+ @Test
+ public void getInstance_differentSessionInfos_returnsDifferent() {
+ CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
+ TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
+
+ CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
+ TEST_COMPONENT_NAME_1, new SessionInfo(DISPLAY_TYPE_MAIN, "a session id"));
+
+ assertThat(factory1).isNotSameInstanceAs(factory2);
}
@Test
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index cc6101d..919b746 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -980,6 +980,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.CarIcon? getImage();
method public androidx.car.app.model.Metadata? getMetadata();
+ method public int getNumericDecoration();
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public int getRowImageType();
method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -992,6 +993,7 @@
field public static final int IMAGE_TYPE_ICON = 4; // 0x4
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int NO_DECORATION = -1; // 0xffffffff
}
public static final class Row.Builder {
@@ -1005,6 +1007,7 @@
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index a040961..41b5994 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1330,6 +1330,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.CarIcon? getImage();
method public androidx.car.app.model.Metadata? getMetadata();
+ method public int getNumericDecoration();
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public int getRowImageType();
method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -1342,6 +1343,7 @@
field public static final int IMAGE_TYPE_ICON = 4; // 0x4
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int NO_DECORATION = -1; // 0xffffffff
}
public static final class Row.Builder {
@@ -1355,6 +1357,7 @@
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index cc6101d..919b746 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -980,6 +980,7 @@
method public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.CarIcon? getImage();
method public androidx.car.app.model.Metadata? getMetadata();
+ method public int getNumericDecoration();
method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
method public int getRowImageType();
method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -992,6 +993,7 @@
field public static final int IMAGE_TYPE_ICON = 4; // 0x4
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+ field public static final int NO_DECORATION = -1; // 0xffffffff
}
public static final class Row.Builder {
@@ -1005,6 +1007,7 @@
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+ method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 8987cfa..09805cc 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -52,6 +52,9 @@
/** A boat that belongs to you. */
private static final String YOUR_BOAT = "\uD83D\uDEA3"; // 🚣
+ /** An integer value indicating no decoration should be shown. */
+ public static final int NO_DECORATION = -1;
+
/**
* The type of images supported within rows.
*
@@ -106,6 +109,8 @@
@Keep
private final List<Action> mActions;
@Keep
+ private final int mDecoration;
+ @Keep
@Nullable
private final Toggle mToggle;
@Keep
@@ -168,6 +173,19 @@
}
/**
+ * Returns the numeric decoration.
+ *
+ * <p> Numeric decorations are displayed at the end of the row, but before any actions.
+ *
+ * <p> {@link Row#NO_DECORATION} will be returned if the row does not contain a decoration.
+ *
+ * @see Builder#setNumericDecoration(int)
+ */
+ public int getNumericDecoration() {
+ return mDecoration;
+ }
+
+ /**
* Returns the {@link Toggle} in the row or {@code null} if the row does not contain a
* toggle.
*
@@ -292,6 +310,7 @@
mTexts = CollectionUtils.unmodifiableCopy(builder.mTexts);
mImage = builder.mImage;
mActions = CollectionUtils.unmodifiableCopy(builder.mActions);
+ mDecoration = builder.mDecoration;
mToggle = builder.mToggle;
mOnClickDelegate = builder.mOnClickDelegate;
mMetadata = builder.mMetadata;
@@ -306,6 +325,7 @@
mTexts = Collections.emptyList();
mImage = null;
mActions = Collections.emptyList();
+ mDecoration = NO_DECORATION;
mToggle = null;
mOnClickDelegate = null;
mMetadata = EMPTY_METADATA;
@@ -323,6 +343,7 @@
@Nullable
CarIcon mImage;
final List<Action> mActions = new ArrayList<>();
+ int mDecoration;
@Nullable
Toggle mToggle;
@Nullable
@@ -522,6 +543,35 @@
}
/**
+ * Sets a numeric decoration to display in the row.
+ *
+ * <p> Numeric decorations are displayed at the end of the row, but before any actions.
+ *
+ * <p> Numeric decorations typically represent a quantity of unseen content. For example, a
+ * decoration might represent a number of missed notifications, or a number of unread
+ * messages in a conversation.
+ *
+ * @param decoration the {@code int} to display. Must be positive, zero, or equal to
+ * {@link Row#NO_DECORATION}.
+ * @throws IllegalArgumentException if {@code decoration} is invalid
+ */
+ @NonNull
+ public Builder setNumericDecoration(int decoration) {
+ if (decoration < 0 && decoration != NO_DECORATION) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Decoration should be positive, zero, or equal to NO_DECORATION. "
+ + "Instead, was %d",
+ decoration
+ )
+ );
+ }
+
+ mDecoration = decoration;
+ return this;
+ }
+
+ /**
* Sets a {@link Toggle} to show in the row.
*
* @throws NullPointerException if {@code toggle} is {@code null}
diff --git a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
index d64eac7..c745f1a 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
@@ -132,6 +132,38 @@
}
@Test
+ public void setDecoration_positiveValue() {
+ int decoration = 5;
+ Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+ assertThat(decoration).isEqualTo(row.getNumericDecoration());
+ }
+
+ @Test
+ public void setDecoration_zero() {
+ int decoration = 0;
+ Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+ assertThat(decoration).isEqualTo(row.getNumericDecoration());
+ }
+
+ @Test
+ public void setDecoration_noDecoration() {
+ int decoration = Row.NO_DECORATION;
+ Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+ assertThat(decoration).isEqualTo(row.getNumericDecoration());
+ }
+
+ @Test
+ public void setDecoration_negative_throws() {
+ int decoration = -123;
+ Row.Builder rowBuilder =
+ new Row.Builder().setTitle("Title");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> rowBuilder.setNumericDecoration(decoration)
+ );
+ }
+
+ @Test
public void setToggle() {
Toggle toggle1 = new Toggle.Builder(isChecked -> {
}).build();
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 7a4a17c..9a50c15 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -26,10 +26,11 @@
companion object {
/**
- * A table of runtime version ints to version strings. This should be
- * updated every time ComposeVersion.kt is updated.
+ * A table of runtime version ints to version strings for compose-runtime.
+ * This should be updated every time a new version of the Compose Runtime is released.
+ * Typically updated via update_versions_for_release.py
*/
- private val versionTable = mapOf(
+ private val runtimeVersionToMavenVersionTable = mapOf(
1600 to "0.1.0-dev16",
1700 to "1.0.0-alpha06",
1800 to "1.0.0-alpha07",
@@ -103,7 +104,7 @@
*/
const val compilerVersion: String = "1.3.1"
private val minimumRuntimeVersion: String
- get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+ get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
}
fun check() {
@@ -137,7 +138,7 @@
}
val versionInt = versionExpr.value as Int
if (versionInt < minimumRuntimeVersionInt) {
- outdatedRuntime(versionTable[versionInt] ?: "<unknown>")
+ outdatedRuntime(runtimeVersionToMavenVersionTable[versionInt] ?: "<unknown>")
}
// success. We are compatible with this runtime version!
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 3feb40c..963ea75 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -708,6 +708,22 @@
}
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+ public final class LazyStaggeredGridDslKt {
+ }
+
+ public final class LazyStaggeredGridItemProviderKt {
+ }
+
+ public final class LazyStaggeredGridKt {
+ }
+
+ public final class LazyStaggeredGridMeasureKt {
+ }
+
+}
+
package androidx.compose.foundation.relocation {
public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 6f14566..93e6607 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -853,6 +853,22 @@
}
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+ public final class LazyStaggeredGridDslKt {
+ }
+
+ public final class LazyStaggeredGridItemProviderKt {
+ }
+
+ public final class LazyStaggeredGridKt {
+ }
+
+ public final class LazyStaggeredGridMeasureKt {
+ }
+
+}
+
package androidx.compose.foundation.relocation {
public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 3feb40c..963ea75 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -708,6 +708,22 @@
}
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+ public final class LazyStaggeredGridDslKt {
+ }
+
+ public final class LazyStaggeredGridItemProviderKt {
+ }
+
+ public final class LazyStaggeredGridKt {
+ }
+
+ public final class LazyStaggeredGridMeasureKt {
+ }
+
+}
+
package androidx.compose.foundation.relocation {
public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
new file mode 100644
index 0000000..179dd3a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
@@ -0,0 +1,151 @@
+/*
+ * 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
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.scrollBy
+import androidx.compose.runtime.Stable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+
+open class BaseLazyLayoutTestWithOrientation(private val orientation: Orientation) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val vertical: Boolean
+ get() = orientation == Orientation.Vertical
+
+ @Stable
+ fun Modifier.crossAxisSize(size: Dp) =
+ if (vertical) {
+ this.width(size)
+ } else {
+ this.height(size)
+ }
+
+ @Stable
+ fun Modifier.mainAxisSize(size: Dp) =
+ if (vertical) {
+ this.height(size)
+ } else {
+ this.width(size)
+ }
+
+ @Stable
+ fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
+ if (vertical) {
+ this.size(crossAxis, mainAxis)
+ } else {
+ this.size(mainAxis, crossAxis)
+ }
+
+ fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
+ if (vertical) {
+ this.scrollBy(y = distance, density = rule.density)
+ } else {
+ this.scrollBy(x = distance, density = rule.density)
+ }
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertHeightIsEqualTo(expectedSize)
+ } else {
+ assertWidthIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertWidthIsEqualTo(expectedSize)
+ } else {
+ assertHeightIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+ val position = if (vertical) {
+ getUnclippedBoundsInRoot().top
+ } else {
+ getUnclippedBoundsInRoot().left
+ }
+ position.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun PaddingValues(
+ mainAxis: Dp = 0.dp,
+ crossAxis: Dp = 0.dp
+ ) = PaddingValues(
+ beforeContent = mainAxis,
+ afterContent = mainAxis,
+ beforeContentCrossAxis = crossAxis,
+ afterContentCrossAxis = crossAxis
+ )
+
+ fun PaddingValues(
+ beforeContent: Dp = 0.dp,
+ afterContent: Dp = 0.dp,
+ beforeContentCrossAxis: Dp = 0.dp,
+ afterContentCrossAxis: Dp = 0.dp,
+ ) = if (vertical) {
+ androidx.compose.foundation.layout.PaddingValues(
+ start = beforeContentCrossAxis,
+ top = beforeContent,
+ end = afterContentCrossAxis,
+ bottom = afterContent
+ )
+ } else {
+ androidx.compose.foundation.layout.PaddingValues(
+ start = beforeContent,
+ top = beforeContentCrossAxis,
+ end = afterContent,
+ bottom = afterContentCrossAxis
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
index 16b0e12..dcb34e1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
@@ -18,139 +18,24 @@
import androidx.compose.animation.core.snap
import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.testutils.assertIsEqualTo
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-open class BaseLazyGridTestWithOrientation(private val orientation: Orientation) {
-
- @get:Rule
- val rule = createComposeRule()
-
- val vertical: Boolean
- get() = orientation == Orientation.Vertical
-
- @Stable
- fun Modifier.crossAxisSize(size: Dp) =
- if (vertical) {
- this.width(size)
- } else {
- this.height(size)
- }
-
- @Stable
- fun Modifier.mainAxisSize(size: Dp) =
- if (vertical) {
- this.height(size)
- } else {
- this.width(size)
- }
-
- @Stable
- fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
- if (vertical) {
- this.size(crossAxis, mainAxis)
- } else {
- this.size(mainAxis, crossAxis)
- }
-
- fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
- if (vertical) {
- this.scrollBy(y = distance, density = rule.density)
- } else {
- this.scrollBy(x = distance, density = rule.density)
- }
- }
-
- fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertHeightIsEqualTo(expectedSize)
- } else {
- assertWidthIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertWidthIsEqualTo(expectedSize)
- } else {
- assertHeightIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
- val position = if (vertical) {
- getUnclippedBoundsInRoot().top
- } else {
- getUnclippedBoundsInRoot().left
- }
- position.assertIsEqualTo(expected, tolerance = 1.dp)
- }
-
- fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertTopPositionInRootIsEqualTo(expectedStart)
- } else {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- } else {
- assertTopPositionInRootIsEqualTo(expectedStart)
- }
-
- fun PaddingValues(
- mainAxis: Dp = 0.dp,
- crossAxis: Dp = 0.dp
- ) = PaddingValues(
- beforeContent = mainAxis,
- afterContent = mainAxis,
- beforeContentCrossAxis = crossAxis,
- afterContentCrossAxis = crossAxis
- )
-
- fun PaddingValues(
- beforeContent: Dp = 0.dp,
- afterContent: Dp = 0.dp,
- beforeContentCrossAxis: Dp = 0.dp,
- afterContentCrossAxis: Dp = 0.dp,
- ) = if (vertical) {
- PaddingValues(
- start = beforeContentCrossAxis,
- top = beforeContent,
- end = afterContentCrossAxis,
- bottom = afterContent
- )
- } else {
- PaddingValues(
- start = beforeContent,
- top = beforeContentCrossAxis,
- end = afterContent,
- bottom = afterContentCrossAxis
- )
- }
+open class BaseLazyGridTestWithOrientation(
+ orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
fun LazyGridState.scrollBy(offset: Dp) {
runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
@@ -164,11 +49,7 @@
}
}
- fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollBy(
- x = if (vertical) 0.dp else offset,
- y = if (!vertical) 0.dp else offset,
- density = rule.density
- )
+ fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollMainAxisBy(offset)
@Composable
fun LazyGrid(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index 1919263..44c5abf 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.snap
import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
import androidx.compose.foundation.composeViewSwipeDown
import androidx.compose.foundation.composeViewSwipeLeft
import androidx.compose.foundation.composeViewSwipeRight
@@ -30,8 +31,6 @@
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
@@ -39,42 +38,16 @@
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertIsEqualTo
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-open class BaseLazyListTestWithOrientation(private val orientation: Orientation) {
-
- @get:Rule
- val rule = createComposeRule()
-
- val vertical: Boolean
- get() = orientation == Orientation.Vertical
-
- fun Modifier.mainAxisSize(size: Dp) =
- if (vertical) {
- this.height(size)
- } else {
- this.width(size)
- }
-
- fun Modifier.crossAxisSize(size: Dp) =
- if (vertical) {
- this.width(size)
- } else {
- this.height(size)
- }
+open class BaseLazyListTestWithOrientation(
+ private val orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
fun Modifier.fillMaxCrossAxis() =
if (vertical) {
@@ -90,82 +63,6 @@
Modifier.fillParentMaxHeight()
}
- fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
- if (vertical) {
- this.scrollBy(y = distance, density = rule.density)
- } else {
- this.scrollBy(x = distance, density = rule.density)
- }
- }
-
- fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertHeightIsEqualTo(expectedSize)
- } else {
- assertWidthIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertWidthIsEqualTo(expectedSize)
- } else {
- assertHeightIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
- val position = if (vertical) {
- getUnclippedBoundsInRoot().top
- } else {
- getUnclippedBoundsInRoot().left
- }
- position.assertIsEqualTo(expected, tolerance = 1.dp)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertTopPositionInRootIsEqualTo(expectedStart)
- } else {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- } else {
- assertTopPositionInRootIsEqualTo(expectedStart)
- }
-
- fun PaddingValues(
- mainAxis: Dp = 0.dp,
- crossAxis: Dp = 0.dp
- ) = PaddingValues(
- beforeContent = mainAxis,
- afterContent = mainAxis,
- beforeContentCrossAxis = crossAxis,
- afterContentCrossAxis = crossAxis
- )
-
- fun PaddingValues(
- beforeContent: Dp = 0.dp,
- afterContent: Dp = 0.dp,
- beforeContentCrossAxis: Dp = 0.dp,
- afterContentCrossAxis: Dp = 0.dp,
- ) = if (vertical) {
- PaddingValues(
- start = beforeContentCrossAxis,
- top = beforeContent,
- end = afterContentCrossAxis,
- bottom = afterContent
- )
- } else {
- PaddingValues(
- start = beforeContent,
- top = beforeContentCrossAxis,
- end = afterContent,
- bottom = afterContentCrossAxis
- )
- }
-
fun LazyListState.scrollBy(offset: Dp) {
runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
new file mode 100644
index 0000000..b865bdb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.staggeredgrid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import kotlin.math.roundToInt
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+@OptIn(ExperimentalFoundationApi::class)
+open class BaseLazyStaggeredGridWithOrientation(
+ private val orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
+
+ internal fun LazyStaggeredGridState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ @Composable
+ internal fun LazyStaggeredGrid(
+ lanes: Int,
+ modifier: Modifier = Modifier,
+ state: LazyStaggeredGridState = remember { LazyStaggeredGridState() },
+ content: LazyStaggeredGridScope.() -> Unit,
+ ) {
+ LazyStaggeredGrid(
+ state = state,
+ modifier = modifier,
+ orientation = orientation,
+ userScrollEnabled = true,
+ verticalArrangement = Arrangement.Top,
+ horizontalArrangement = Arrangement.Start,
+ slotSizesSums = { constraints ->
+ val crossAxisSize = if (orientation == Orientation.Vertical) {
+ constraints.maxWidth
+ } else {
+ constraints.maxHeight
+ }
+ IntArray(lanes) {
+ (crossAxisSize / lanes.toDouble() * (it + 1)).roundToInt()
+ }
+ },
+ content = content
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
new file mode 100644
index 0000000..df3d0f3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -0,0 +1,342 @@
+/*
+ * 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.gestures.Orientation
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyStaggeredGridTest(
+ private val orientation: Orientation
+) : BaseLazyStaggeredGridWithOrientation(orientation) {
+ private val LazyStaggeredGridTag = "LazyStaggeredGridTag"
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ Orientation.Vertical,
+ Orientation.Horizontal,
+ )
+ }
+
+ private var itemSizeDp: Dp = Dp.Unspecified
+ private val itemSizePx: Int = 50
+
+ @Before
+ fun setUp() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ }
+ }
+
+ @Test
+ fun showsOneItem() {
+ val itemTestTag = "itemTestTag"
+
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ ) {
+ item {
+ Spacer(
+ Modifier.size(itemSizeDp).testTag(itemTestTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(itemTestTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun distributesSingleLine() {
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ modifier = Modifier.crossAxisSize(itemSizeDp * 3),
+ ) {
+ items(3) {
+ Spacer(
+ Modifier.size(itemSizeDp).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+ }
+
+ @Test
+ fun distributesTwoLines() {
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ modifier = Modifier.crossAxisSize(itemSizeDp * 3),
+ ) {
+ items(6) {
+ Spacer(
+ Modifier.axisSize(
+ crossAxis = itemSizeDp,
+ mainAxis = itemSizeDp * (it + 1)
+ ).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ // [item, 0, 0]
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+ // [item, item x 2, 0]
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+ // [item, item x 2, item x 3]
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ // [item x 4, item x 2, item x 3]
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+ // [item x 4, item x 7, item x 3]
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 3)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+ // [item x 4, item x 7, item x 9]
+ }
+
+ @Test
+ fun moreItemsDisplayedOnScroll() {
+ val state = LazyStaggeredGridState()
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+ ) {
+ items(6) {
+ Spacer(
+ Modifier.axisSize(
+ crossAxis = itemSizeDp,
+ mainAxis = itemSizeDp * (it + 1)
+ ).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ state.scrollBy(itemSizeDp * 3)
+
+ // [item, item x 2, item x 3]
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp * 2)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ // [item x 4, item x 2, item x 3]
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+ // [item x 4, item x 7, item x 3]
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+ // [item x 4, item x 7, item x 9]
+ }
+
+ @Test
+ fun itemsAreHiddenOnScroll() {
+ val state = LazyStaggeredGridState()
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+ ) {
+ items(6) {
+ Spacer(
+ Modifier.axisSize(
+ crossAxis = itemSizeDp,
+ mainAxis = itemSizeDp * (it + 1)
+ ).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+
+ state.scrollBy(itemSizeDp * 3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun itemsArePresentedWhenScrollingBack() {
+ val state = LazyStaggeredGridState()
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+ ) {
+ items(6) {
+ Spacer(
+ Modifier.axisSize(
+ crossAxis = itemSizeDp,
+ mainAxis = itemSizeDp * (it + 1)
+ ).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+
+ state.scrollBy(itemSizeDp * 3)
+ state.scrollBy(-itemSizeDp * 3)
+
+ for (i in 0..2) {
+ rule.onNodeWithTag("$i")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ }
+ }
+
+ @Test
+ fun scrollingALot_layoutIsNotRecomposed() {
+ val state = LazyStaggeredGridState()
+ var recomposed = 0
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier
+ .mainAxisSize(itemSizeDp * 10)
+ .composed {
+ recomposed++
+ Modifier
+ }
+ ) {
+ items(1000) {
+ Spacer(
+ Modifier.mainAxisSize(itemSizeDp).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(recomposed).isEqualTo(1)
+
+ state.scrollBy(1000.dp)
+
+ rule.waitForIdle()
+ assertThat(recomposed).isEqualTo(1)
+ }
+
+ @Test
+ fun onlyOneInitialMeasurePass() {
+ val state = LazyStaggeredGridState()
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier
+ .mainAxisSize(itemSizeDp * 10)
+ .composed {
+ Modifier
+ }
+ ) {
+ items(1000) {
+ Spacer(
+ Modifier.mainAxisSize(itemSizeDp).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(state.measurePassCount).isEqualTo(1)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
similarity index 64%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt
rename to compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
index 5a13ce4..0adee6c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
@@ -16,11 +16,9 @@
package androidx.compose.foundation.relocation
-import android.os.Build.VERSION_CODES.O
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical
@@ -33,50 +31,50 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.foundation.GOLDEN_UI
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.foundation.background
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color.Companion.Blue
import androidx.compose.ui.graphics.Color.Companion.Green
import androidx.compose.ui.graphics.Color.Companion.LightGray
import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.TestMonotonicFrameClock
-import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
@MediumTest
@RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = O)
-class BringIntoViewRequesterModifierTest(private val orientation: Orientation) {
+class BringIntoViewScrollableInteractionTest(private val orientation: Orientation) {
+
@get:Rule
val rule = createComposeRule()
- @get:Rule
- val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_UI)
-
private val parentBox = "parent box"
+ private val childBox = "child box"
+
+ /**
+ * Captures a scope from inside the composition for [runBlockingAndAwaitIdle].
+ * Make sure to call [setContentAndInitialize] instead of calling `rule.setContent` to initialize this.
+ */
+ private lateinit var testScope: CoroutineScope
companion object {
@JvmStatic
@@ -88,13 +86,13 @@
fun noScrollableParent_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.size(100.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 100.dp)
+ Horizontal -> Modifier.size(100.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 100.toDp())
}
)
.testTag(parentBox)
@@ -102,31 +100,34 @@
) {
Box(
Modifier
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
fun noScrollableParent_itemNotVisible_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.size(100.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 100.dp)
+ Horizontal -> Modifier.size(100.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 100.toDp())
}
)
.testTag(parentBox)
@@ -136,29 +137,32 @@
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 150.dp)
- Vertical -> Modifier.offset(y = 150.dp)
+ Horizontal -> Modifier.offset(x = 150.toDp())
+ Vertical -> Modifier.offset(y = 150.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "grayRectangleHorizontal" else "grayRectangleVertical")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
fun itemAtLeadingEdge_alreadyVisible_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
.testTag(parentBox)
@@ -167,36 +171,39 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(rememberScrollState())
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(rememberScrollState())
}
)
) {
Box(
Modifier
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
fun itemAtTrailingEdge_alreadyVisible_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
.testTag(parentBox)
@@ -205,11 +212,11 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(rememberScrollState())
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(rememberScrollState())
}
)
@@ -218,29 +225,32 @@
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 50.dp)
- Vertical -> Modifier.offset(y = 50.dp)
+ Horizontal -> Modifier.offset(x = 50.toDp())
+ Vertical -> Modifier.offset(y = 50.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
fun itemAtCenter_alreadyVisible_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
.testTag(parentBox)
@@ -249,11 +259,11 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(rememberScrollState())
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(rememberScrollState())
}
)
@@ -262,32 +272,35 @@
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 25.dp)
- Vertical -> Modifier.offset(y = 25.dp)
+ Horizontal -> Modifier.offset(x = 25.toDp())
+ Vertical -> Modifier.offset(y = 25.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxCenterHorizontal" else "blueBoxCenterVertical")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
fun itemBiggerThanParentAtLeadingEdge_alreadyVisible_noChange() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- rule.setContent {
+ setContentAndInitialize {
Box(
Modifier
- .size(50.dp)
+ .size(50.toDp())
.testTag(parentBox)
.background(LightGray)
.then(
@@ -299,19 +312,37 @@
) {
// Using a multi-colored item to make sure we can assert that the right part of
// the item is visible.
- RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
- Box(Modifier.size(50.dp).background(Blue))
- Box(Modifier.size(50.dp).background(Green))
- Box(Modifier.size(50.dp).background(Red))
+ RowOrColumn(
+ Modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
+ ) {
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Blue)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Green)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Red)
+ )
}
}
}
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot("blueBox")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
@@ -319,11 +350,11 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
- .size(50.dp)
+ .size(50.toDp())
.testTag(parentBox)
.background(LightGray)
.then(
@@ -335,20 +366,38 @@
) {
// Using a multi-colored item to make sure we can assert that the right part of
// the item is visible.
- RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
- Box(Modifier.size(50.dp).background(Red))
- Box(Modifier.size(50.dp).background(Green))
- Box(Modifier.size(50.dp).background(Blue))
+ RowOrColumn(
+ Modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
+ ) {
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Red)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Green)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Blue)
+ )
}
}
}
runBlockingAndAwaitIdle { scrollState.scrollTo(scrollState.maxValue) }
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot("blueBox")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
@@ -356,11 +405,11 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
- .size(50.dp)
+ .size(50.toDp())
.testTag(parentBox)
.background(LightGray)
.then(
@@ -372,20 +421,38 @@
) {
// Using a multi-colored item to make sure we can assert that the right part of
// the item is visible.
- RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
- Box(Modifier.size(50.dp).background(Green))
- Box(Modifier.size(50.dp).background(Blue))
- Box(Modifier.size(50.dp).background(Red))
+ RowOrColumn(
+ Modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
+ ) {
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Green)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Blue)
+ )
+ Box(
+ Modifier
+ .size(50.toDp())
+ .background(Red)
+ )
}
}
}
runBlockingAndAwaitIdle { scrollState.scrollTo(scrollState.maxValue / 2) }
+ val startingBounds = getUnclippedBoundsInRoot(childBox)
// Act.
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot("blueBox")
+ assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+ assertChildMaxInView()
}
@Test
@@ -393,7 +460,7 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
@@ -403,32 +470,33 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(scrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(scrollState)
}
)
) {
Box(
when (orientation) {
- Horizontal -> Modifier.size(200.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 200.dp)
+ Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 200.toDp())
}
) {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 50.dp)
- Vertical -> Modifier.offset(y = 50.dp)
+ Horizontal -> Modifier.offset(x = 50.toDp())
+ Vertical -> Modifier.offset(y = 50.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -439,7 +507,8 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+ assertChildMaxInView()
}
@Test
@@ -447,7 +516,7 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
@@ -457,32 +526,33 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(scrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(scrollState)
}
)
) {
Box(
when (orientation) {
- Horizontal -> Modifier.size(200.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 200.dp)
+ Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 200.toDp())
}
) {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 150.dp)
- Vertical -> Modifier.offset(y = 150.dp)
+ Horizontal -> Modifier.offset(x = 150.toDp())
+ Vertical -> Modifier.offset(y = 150.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -493,7 +563,11 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(
+ expectedLeft = if (orientation == Horizontal) 50.toDp() else 0.toDp(),
+ expectedTop = if (orientation == Horizontal) 0.toDp() else 50.toDp()
+ )
+ assertChildMaxInView()
}
@Test
@@ -501,7 +575,7 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
@@ -511,27 +585,28 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(scrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(scrollState)
}
)
) {
- Box(Modifier.size(200.dp)) {
+ Box(Modifier.size(200.toDp())) {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 25.dp)
- Vertical -> Modifier.offset(y = 25.dp)
+ Horizontal -> Modifier.offset(x = 25.toDp())
+ Vertical -> Modifier.offset(y = 25.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -542,7 +617,8 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+ assertChildMaxInView()
}
@Test
@@ -550,7 +626,7 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var scrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
scrollState = rememberScrollState()
Box(
Modifier
@@ -560,32 +636,33 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(scrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(scrollState)
}
)
) {
Box(
when (orientation) {
- Horizontal -> Modifier.size(200.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 200.dp)
+ Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 200.toDp())
}
) {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 150.dp)
- Vertical -> Modifier.offset(y = 150.dp)
+ Horizontal -> Modifier.offset(x = 150.toDp())
+ Vertical -> Modifier.offset(y = 150.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -596,7 +673,11 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(
+ expectedLeft = if (orientation == Horizontal) 50.toDp() else 0.toDp(),
+ expectedTop = if (orientation == Horizontal) 0.toDp() else 50.toDp()
+ )
+ assertChildMaxInView()
}
@Test
@@ -605,7 +686,7 @@
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var parentScrollState: ScrollState
lateinit var grandParentScrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
parentScrollState = rememberScrollState()
grandParentScrollState = rememberScrollState()
Box(
@@ -616,11 +697,11 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.horizontalScroll(grandParentScrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.verticalScroll(grandParentScrollState)
}
)
@@ -632,32 +713,33 @@
when (orientation) {
Horizontal ->
Modifier
- .size(200.dp, 50.dp)
+ .size(200.toDp(), 50.toDp())
.horizontalScroll(parentScrollState)
Vertical ->
Modifier
- .size(50.dp, 200.dp)
+ .size(50.toDp(), 200.toDp())
.verticalScroll(parentScrollState)
}
)
) {
Box(
when (orientation) {
- Horizontal -> Modifier.size(400.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 400.dp)
+ Horizontal -> Modifier.size(400.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 400.toDp())
}
) {
Box(
Modifier
.then(
when (orientation) {
- Horizontal -> Modifier.offset(x = 25.dp)
- Vertical -> Modifier.offset(y = 25.dp)
+ Horizontal -> Modifier.offset(x = 25.toDp())
+ Vertical -> Modifier.offset(y = 25.toDp())
}
)
- .size(50.dp)
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -670,7 +752,8 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+ assertChildMaxInView()
}
@Test
@@ -679,7 +762,7 @@
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var parentScrollState: ScrollState
lateinit var grandParentScrollState: ScrollState
- rule.setContent {
+ setContentAndInitialize {
parentScrollState = rememberScrollState()
grandParentScrollState = rememberScrollState()
Box(
@@ -690,18 +773,18 @@
when (orientation) {
Horizontal ->
Modifier
- .size(100.dp, 50.dp)
+ .size(100.toDp(), 50.toDp())
.verticalScroll(grandParentScrollState)
Vertical ->
Modifier
- .size(50.dp, 100.dp)
+ .size(50.toDp(), 100.toDp())
.horizontalScroll(grandParentScrollState)
}
)
) {
Box(
Modifier
- .size(100.dp)
+ .size(100.toDp())
.background(LightGray)
.then(
when (orientation) {
@@ -710,13 +793,14 @@
}
)
) {
- Box(Modifier.size(200.dp)) {
+ Box(Modifier.size(200.toDp())) {
Box(
Modifier
- .offset(x = 25.dp, y = 25.dp)
- .size(50.dp)
+ .offset(x = 25.toDp(), y = 25.toDp())
+ .size(50.toDp())
.background(Blue)
.bringIntoViewRequester(bringIntoViewRequester)
+ .testTag(childBox)
)
}
}
@@ -729,7 +813,8 @@
runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+ assertChildMaxInView()
}
@Test
@@ -737,12 +822,12 @@
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
lateinit var density: Density
- rule.setContent {
+ setContentAndInitialize {
density = LocalDensity.current
Box(
Modifier
.testTag(parentBox)
- .size(50.dp)
+ .size(50.toDp())
.background(LightGray)
.then(
when (orientation) {
@@ -751,22 +836,28 @@
}
)
) {
- Canvas(
- when (orientation) {
- Horizontal -> Modifier.size(150.dp, 50.dp)
- Vertical -> Modifier.size(50.dp, 150.dp)
- }.bringIntoViewRequester(bringIntoViewRequester)
- ) {
- with(density) {
- drawRect(
- color = Blue,
- topLeft = when (orientation) {
- Horizontal -> Offset(50.dp.toPx(), 0.dp.toPx())
- Vertical -> Offset(0.dp.toPx(), 50.dp.toPx())
- },
- size = Size(50.dp.toPx(), 50.dp.toPx())
+ Box(
+ Modifier
+ .then(
+ when (orientation) {
+ Horizontal -> Modifier.size(150.toDp(), 50.toDp())
+ Vertical -> Modifier.size(50.toDp(), 150.toDp())
+ }
)
- }
+ .bringIntoViewRequester(bringIntoViewRequester)
+ ) {
+ Box(
+ Modifier
+ .size(50.toDp())
+ .then(
+ when (orientation) {
+ Horizontal -> Modifier.offset(50.toDp(), 0.toDp())
+ Vertical -> Modifier.offset(0.toDp(), 50.toDp())
+ }
+ )
+ .background(Blue)
+ .testTag(childBox)
+ )
}
}
}
@@ -775,18 +866,40 @@
runBlockingAndAwaitIdle {
val rect = with(density) {
when (orientation) {
- Horizontal -> Rect(50.dp.toPx(), 0.dp.toPx(), 100.dp.toPx(), 50.dp.toPx())
- Vertical -> Rect(0.dp.toPx(), 50.dp.toPx(), 50.dp.toPx(), 100.dp.toPx())
+ Horizontal -> DpRect(50.toDp(), 0.toDp(), 100.toDp(), 50.toDp()).toRect()
+ Vertical -> DpRect(0.toDp(), 50.toDp(), 50.toDp(), 100.toDp()).toRect()
}
}
bringIntoViewRequester.bringIntoView(rect)
}
// Assert.
- assertScreenshot("blueBox")
+ rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+ assertChildMaxInView()
}
- private val horizontal: Boolean get() = (orientation == Horizontal)
+ private fun setContentAndInitialize(content: @Composable () -> Unit) {
+ rule.setContent {
+ testScope = rememberCoroutineScope()
+ content()
+ }
+ }
+
+ /**
+ * Sizes and offsets of the composables in these tests must be specified using this function.
+ * If they're specified using `xx.dp` syntax, a rounding error somewhere in the layout system
+ * will cause the pixel values to be off-by-one.
+ */
+ private fun Int.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
+
+ /**
+ * Returns the bounds of the node with [tag], without performing any clipping by any parents.
+ */
+ @Suppress("SameParameterValue")
+ private fun getUnclippedBoundsInRoot(tag: String): Rect {
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ return Rect(node.positionInRoot, node.size.toSize())
+ }
@Composable
private fun RowOrColumn(
@@ -799,20 +912,32 @@
}
}
- @RequiresApi(O)
- private fun assertScreenshot(screenshot: String) {
- rule.onNodeWithTag(parentBox)
- .captureToImage()
- .assertAgainstGolden(screenshotRule, "bringIntoParentBounds_$screenshot")
+ private fun runBlockingAndAwaitIdle(block: suspend CoroutineScope.() -> Unit) {
+ val job = testScope.launch(block = block)
+ rule.waitForIdle()
+ runBlocking {
+ job.join()
+ }
}
- private fun runBlockingAndAwaitIdle(block: suspend CoroutineScope.() -> Unit) {
- runTest {
- withContext(TestMonotonicFrameClock(this)) {
- block()
- advanceUntilIdle()
- }
+ /**
+ * Asserts that as much of the child (identified by [childBox]) as can fit in the viewport
+ * (identified by [parentBox]) is visible. This is the min of the child size and the viewport
+ * size.
+ */
+ private fun assertChildMaxInView() {
+ val parentNode = rule.onNodeWithTag(parentBox).fetchSemanticsNode()
+ val childNode = rule.onNodeWithTag(childBox).fetchSemanticsNode()
+
+ // BoundsInRoot returns the clipped bounds.
+ val visibleBounds: IntSize = childNode.boundsInRoot.size.run {
+ IntSize(width.roundToInt(), height.roundToInt())
}
- rule.waitForIdle()
+ val expectedVisibleBounds = IntSize(
+ width = minOf(parentNode.size.width, childNode.size.width),
+ height = minOf(parentNode.size.height, childNode.size.height)
+ )
+
+ assertThat(visibleBounds).isEqualTo(expectedVisibleBounds)
}
-}
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
new file mode 100644
index 0000000..eb8ebd4
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.clipScrollableContainer
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.overscroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+
+@ExperimentalFoundationApi
+@Composable
+internal fun LazyStaggeredGrid(
+ /** State controlling the scroll position */
+ state: LazyStaggeredGridState,
+ /** Modifier to be applied for the inner layout */
+ modifier: Modifier = Modifier,
+ /** The inner padding to be added for the whole content (not for each individual item) */
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean = false,
+ /** The layout orientation of the grid */
+ orientation: Orientation,
+ /** fling behavior to be used for flinging */
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ /** Whether scrolling via the user gestures is allowed. */
+ userScrollEnabled: Boolean,
+ /** The vertical arrangement for items/lines. */
+ verticalArrangement: Arrangement.Vertical,
+ /** The horizontal arrangement for items/lines. */
+ horizontalArrangement: Arrangement.Horizontal,
+ /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */
+ slotSizesSums: Density.(Constraints) -> IntArray,
+ /** The content of the grid */
+ content: LazyStaggeredGridScope.() -> Unit
+) {
+ val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+ val itemProvider = rememberStaggeredGridItemProvider(state, content)
+ val measurePolicy = rememberStaggeredGridMeasurePolicy(
+ state,
+ itemProvider,
+ contentPadding,
+ reverseLayout,
+ orientation,
+ verticalArrangement,
+ horizontalArrangement,
+ slotSizesSums,
+ overscrollEffect
+ )
+
+ LazyLayout(
+ modifier = modifier
+ .then(state.remeasurementModifier)
+ .clipScrollableContainer(orientation)
+ .overscroll(overscrollEffect)
+ .scrollable(
+ orientation = orientation,
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
+ flingBehavior = flingBehavior,
+ state = state,
+ overscrollEffect = overscrollEffect,
+ enabled = userScrollEnabled
+ ),
+ prefetchState = state.prefetchState,
+ itemProvider = itemProvider,
+ measurePolicy = measurePolicy
+ )
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
new file mode 100644
index 0000000..d836948
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.runtime.Composable
+
+@DslMarker
+internal annotation class LazyStaggeredGridScopeMarker
+
+@ExperimentalFoundationApi
+@LazyStaggeredGridScopeMarker
+internal sealed interface LazyStaggeredGridScope {
+ fun items(
+ count: Int,
+ key: ((index: Int) -> Any)? = null,
+ contentType: (index: Int) -> Any? = { null },
+ itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+ )
+}
+
+@ExperimentalFoundationApi
+internal sealed interface LazyStaggeredGridItemScope
+
+@ExperimentalFoundationApi
+internal fun LazyStaggeredGridScope.item(
+ key: Any? = null,
+ contentType: Any? = null,
+ itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+) {
+ items(
+ count = 1,
+ key = key?.let { { it } },
+ contentType = { contentType },
+ itemContent = itemContent
+ )
+}
+
+// todo(b/182882362): item DSL for lists/arrays
\ No newline at end of file
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
new file mode 100644
index 0000000..c2d02b2
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+
+@Composable
+@ExperimentalFoundationApi
+internal fun rememberStaggeredGridItemProvider(
+ state: LazyStaggeredGridState,
+ content: LazyStaggeredGridScope.() -> Unit,
+): LazyLayoutItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = { state.firstVisibleItems.getOrNull(0) ?: 0 },
+ slidingWindowSize = { 90 },
+ extraItemCount = { 200 }
+ )
+ return remember(state) {
+ val itemProviderState = derivedStateOf {
+ val scope = LazyStaggeredGridScopeImpl().apply(latestContent.value)
+ LazyLayoutItemProvider(
+ scope.intervals,
+ nearestItemsRangeState.value,
+ ) { interval, index ->
+ interval.item.invoke(LazyStaggeredGridItemScopeImpl, index)
+ }
+ }
+ object : LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) { }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..adc7424
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -0,0 +1,549 @@
+/*
+ * 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.OverscrollEffect
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+@Composable
+@ExperimentalFoundationApi
+internal fun rememberStaggeredGridMeasurePolicy(
+ state: LazyStaggeredGridState,
+ itemProvider: LazyLayoutItemProvider,
+ contentPadding: PaddingValues,
+ reverseLayout: Boolean,
+ orientation: Orientation,
+ verticalArrangement: Arrangement.Vertical,
+ horizontalArrangement: Arrangement.Horizontal,
+ slotSizesSums: Density.(Constraints) -> IntArray,
+ overscrollEffect: OverscrollEffect
+): LazyLayoutMeasureScope.(Constraints) -> LazyStaggeredGridMeasureResult = remember(
+ state,
+ itemProvider,
+ contentPadding,
+ reverseLayout,
+ orientation,
+ verticalArrangement,
+ horizontalArrangement,
+ slotSizesSums,
+ overscrollEffect,
+) {
+ { constraints ->
+ checkScrollableContainerConstraints(
+ constraints,
+ orientation
+ )
+ val isVertical = orientation == Orientation.Vertical
+
+ val resolvedSlotSums = slotSizesSums(this, constraints)
+ val itemCount = itemProvider.itemCount
+
+ val mainAxisAvailableSize =
+ if (isVertical) constraints.maxHeight else constraints.maxWidth
+
+ val measuredItemProvider = LazyStaggeredGridMeasureProvider(
+ isVertical,
+ itemProvider,
+ this,
+ resolvedSlotSums
+ ) { index, key, placeables ->
+ LazyStaggeredGridMeasuredItem(
+ index,
+ key,
+ placeables,
+ isVertical
+ )
+ }
+
+ val beforeContentPadding = 0
+ val afterContentPadding = 0
+
+ val initialItemIndices: IntArray
+ val initialItemOffsets: IntArray
+
+ Snapshot.withoutReadObservation {
+ initialItemIndices =
+ if (state.firstVisibleItems.size == resolvedSlotSums.size) {
+ state.firstVisibleItems
+ } else {
+ IntArray(resolvedSlotSums.size) { -1 }
+ }
+ initialItemOffsets =
+ if (state.firstVisibleItemScrollOffsets.size == resolvedSlotSums.size) {
+ state.firstVisibleItemScrollOffsets
+ } else {
+ IntArray(resolvedSlotSums.size) { 0 }
+ }
+ }
+
+ val spans = state.spans
+ val firstItemIndices = initialItemIndices.copyOf()
+ val firstItemOffsets = initialItemOffsets.copyOf()
+
+ // Measure items
+
+ if (itemCount <= 0) {
+ LazyStaggeredGridMeasureResult(
+ firstVisibleItemIndices = IntArray(0),
+ firstVisibleItemScrollOffsets = IntArray(0),
+ consumedScroll = 0f,
+ measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+ canScrollForward = false,
+ canScrollBackward = false,
+ visibleItemsInfo = emptyArray()
+ )
+ } else {
+ // todo(b/182882362): content padding
+
+ // represents the real amount of scroll we applied as a result of this measure pass.
+ var scrollDelta = state.scrollToBeConsumed.roundToInt()
+
+ // applying the whole requested scroll offset. we will figure out if we can't consume
+ // all of it later
+ firstItemOffsets.offsetBy(-scrollDelta)
+
+ // if the current scroll offset is less than minimally possible
+ if (firstItemIndices[0] == 0 && firstItemOffsets[0] < 0) {
+ scrollDelta += firstItemOffsets[0]
+ firstItemOffsets.fill(0)
+ }
+
+ // this will contain all the MeasuredItems representing the visible items
+ val measuredItems = Array(resolvedSlotSums.size) {
+ mutableListOf<LazyStaggeredGridMeasuredItem>()
+ }
+
+ // include the start padding so we compose items in the padding area. before starting
+ // scrolling forward we would remove it back
+ firstItemOffsets.offsetBy(-beforeContentPadding)
+
+ // define min and max offsets (min offset currently includes beforeContentPadding)
+ val minOffset = -beforeContentPadding
+ val maxOffset = mainAxisAvailableSize
+
+ fun hasSpaceOnTop(): Boolean {
+ for (column in firstItemIndices.indices) {
+ val itemIndex = firstItemIndices[column]
+ val itemOffset = firstItemOffsets[column]
+
+ if (itemOffset <= 0 && itemIndex > 0) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ // we had scrolled backward or we compose items in the start padding area, which means
+ // items before current firstItemScrollOffset should be visible. compose them and update
+ // firstItemScrollOffset
+ while (hasSpaceOnTop()) {
+ val columnIndex = firstItemOffsets.indexOfMinValue()
+ val previousItemIndex = spans.findPreviousItemIndex(
+ item = firstItemIndices[columnIndex],
+ column = columnIndex
+ )
+
+ if (previousItemIndex < 0) {
+ break
+ }
+
+ if (spans.getSpan(previousItemIndex) == SpanLookup.SpanUnset) {
+ spans.setSpan(previousItemIndex, columnIndex)
+ }
+
+ val measuredItem = measuredItemProvider.getAndMeasure(
+ previousItemIndex,
+ columnIndex
+ )
+ measuredItems[columnIndex].add(0, measuredItem)
+
+ firstItemIndices[columnIndex] = previousItemIndex
+ firstItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+ }
+
+ // if we were scrolled backward, but there were not enough items before. this means
+ // not the whole scroll was consumed
+ if (firstItemOffsets[0] < minOffset) {
+ scrollDelta += firstItemOffsets[0]
+ firstItemOffsets.offsetBy(minOffset - firstItemOffsets[0])
+ }
+
+ val currentItemIndices = initialItemIndices.copyOf()
+ val currentItemOffsets = IntArray(initialItemOffsets.size) {
+ -(initialItemOffsets[it] - scrollDelta)
+ }
+
+ // neutralize previously added start padding as we stopped filling the before content padding
+ firstItemOffsets.offsetBy(beforeContentPadding)
+
+ val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+
+ // compose first visible items we received from state
+ currentItemIndices.forEachIndexed { columnIndex, itemIndex ->
+ if (itemIndex == -1) return@forEachIndexed
+
+ val measuredItem = measuredItemProvider.getAndMeasure(itemIndex, columnIndex)
+ currentItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+
+ if (
+ currentItemOffsets[columnIndex] <= minOffset &&
+ measuredItem.index != itemCount - 1
+ ) {
+ // this item is offscreen and will not be placed. advance item index
+ firstItemIndices[columnIndex] = -1
+ firstItemOffsets[columnIndex] -= measuredItem.sizeWithSpacings
+ } else {
+ measuredItems[columnIndex].add(measuredItem)
+ }
+ }
+
+ // then composing visible items forward until we fill the whole viewport.
+ // we want to have at least one item in visibleItems even if in fact all the items are
+ // offscreen, this can happen if the content padding is larger than the available size.
+ while (
+ currentItemOffsets.any { it <= maxMainAxis } ||
+ measuredItems.all { it.isEmpty() }
+ ) {
+ val columnIndex = currentItemOffsets.indexOfMinValue()
+ val nextItemIndex = spans.findNextItemIndex(
+ currentItemIndices[columnIndex],
+ columnIndex
+ )
+
+ if (nextItemIndex == itemCount) {
+ break
+ }
+
+ if (firstItemIndices[columnIndex] == -1) {
+ firstItemIndices[columnIndex] = nextItemIndex
+ }
+ spans.setSpan(nextItemIndex, columnIndex)
+
+ val measuredItem = measuredItemProvider.getAndMeasure(nextItemIndex, columnIndex)
+ currentItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+
+ if (
+ currentItemOffsets[columnIndex] <= minOffset &&
+ measuredItem.index != itemCount - 1
+ ) {
+ // this item is offscreen and will not be placed. advance item index
+ firstItemIndices[columnIndex] = -1
+ firstItemOffsets[columnIndex] -= measuredItem.sizeWithSpacings
+ } else {
+ measuredItems[columnIndex].add(measuredItem)
+ }
+
+ currentItemIndices[columnIndex] = nextItemIndex
+ }
+
+ // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
+ // lets try to scroll back if we have enough items before firstVisibleItemIndex.
+ if (currentItemOffsets.all { it < maxOffset }) {
+ val maxOffsetColumn = currentItemOffsets.indexOfMaxValue()
+ val toScrollBack = maxOffset - currentItemOffsets[maxOffsetColumn]
+ firstItemOffsets.offsetBy(-toScrollBack)
+ currentItemOffsets.offsetBy(toScrollBack)
+ while (
+ firstItemOffsets.any { it < beforeContentPadding } &&
+ firstItemIndices.all { it != 0 }
+ ) {
+ val columnIndex = firstItemOffsets.indexOfMinValue()
+ val currentIndex =
+ if (firstItemIndices[columnIndex] == -1) {
+ itemCount
+ } else {
+ firstItemIndices[columnIndex]
+ }
+
+ val previousIndex =
+ spans.findPreviousItemIndex(currentIndex, columnIndex)
+
+ if (previousIndex < 0) {
+ break
+ }
+
+ val measuredItem = measuredItemProvider.getAndMeasure(
+ previousIndex,
+ columnIndex
+ )
+ measuredItems[columnIndex].add(0, measuredItem)
+ firstItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+ firstItemIndices[columnIndex] = previousIndex
+ }
+ scrollDelta += toScrollBack
+
+ val minOffsetColumn = firstItemOffsets.indexOfMinValue()
+ if (firstItemOffsets[minOffsetColumn] < 0) {
+ val offsetValue = firstItemOffsets[minOffsetColumn]
+ scrollDelta += offsetValue
+ currentItemOffsets.offsetBy(offsetValue)
+ firstItemOffsets.offsetBy(-offsetValue)
+ }
+ }
+
+ // report the amount of pixels we consumed. scrollDelta can be smaller than
+ // scrollToBeConsumed if there were not enough items to fill the offered space or it
+ // can be larger if items were resized, or if, for example, we were previously
+ // displaying the item 15, but now we have only 10 items in total in the data set.
+ val consumedScroll = if (
+ state.scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+ abs(state.scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+ ) {
+ scrollDelta.toFloat()
+ } else {
+ state.scrollToBeConsumed
+ }
+
+ // todo(b/182882362):
+ // even if we compose items to fill before content padding we should ignore items fully
+ // located there for the state's scroll position calculation (first item + first offset)
+
+ // end measure
+
+ val layoutWidth = if (isVertical) {
+ constraints.maxWidth
+ } else {
+ constraints.constrainWidth(currentItemOffsets.max())
+ }
+ val layoutHeight = if (isVertical) {
+ constraints.constrainHeight(currentItemOffsets.max())
+ } else {
+ constraints.maxHeight
+ }
+
+ // Placement
+
+ val itemScrollOffsets = firstItemOffsets.map { -it }
+ val positionedItems = Array(measuredItems.size) {
+ mutableListOf<LazyStaggeredGridPositionedItem>()
+ }
+
+ var currentCrossAxis = 0
+ measuredItems.forEachIndexed { i, columnItems ->
+ var currentMainAxis = itemScrollOffsets[i]
+
+ // todo(b/182882362): arrangement/spacing support
+
+ columnItems.fastForEach { item ->
+ positionedItems[i] += item.position(
+ currentMainAxis,
+ currentCrossAxis,
+ )
+ currentMainAxis += item.sizeWithSpacings
+ }
+ if (columnItems.isNotEmpty()) {
+ currentCrossAxis += columnItems[0].crossAxisSize
+ }
+ }
+
+ // End placement
+
+ // todo: reverse layout support
+ // 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
+ val canScrollForward = currentItemIndices.indexOf(itemCount - 1).let { columnIndex ->
+ if (columnIndex == -1) {
+ true
+ } else {
+ (currentItemOffsets[columnIndex] -
+ measuredItems[columnIndex].last().sizeWithSpacings) < mainAxisAvailableSize
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ LazyStaggeredGridMeasureResult(
+ firstVisibleItemIndices = firstItemIndices,
+ firstVisibleItemScrollOffsets = firstItemOffsets,
+ consumedScroll = consumedScroll,
+ measureResult = layout(layoutWidth, layoutHeight) {
+ positionedItems.forEach {
+ it.fastForEach { item ->
+ item.place(this)
+ }
+ }
+ },
+ canScrollForward = canScrollForward,
+ canScrollBackward = canScrollBackward,
+ visibleItemsInfo = positionedItems as Array<List<LazyStaggeredGridItemInfo>>
+ ).also {
+ state.applyMeasureResult(it)
+ refreshOverscrollInfo(overscrollEffect, it)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun refreshOverscrollInfo(
+ overscrollEffect: OverscrollEffect,
+ result: LazyStaggeredGridMeasureResult
+) {
+ overscrollEffect.isEnabled = result.canScrollForward || result.canScrollBackward
+}
+
+private fun IntArray.offsetBy(delta: Int) {
+ for (i in indices) {
+ this[i] = this[i] + delta
+ }
+}
+
+private fun IntArray.indexOfMinValue(): Int {
+ var result = -1
+ var min = Int.MAX_VALUE
+ for (i in indices) {
+ if (min > this[i]) {
+ min = this[i]
+ result = i
+ }
+ }
+
+ return result
+}
+
+private fun IntArray.indexOfMaxValue(): Int {
+ var result = -1
+ var max = Int.MIN_VALUE
+ for (i in indices) {
+ if (max < this[i]) {
+ max = this[i]
+ result = i
+ }
+ }
+
+ return result
+}
+
+private fun SpanLookup.findPreviousItemIndex(item: Int, column: Int): Int {
+ for (i in (item - 1) downTo 0) {
+ val span = getSpan(i)
+ if (span == column || span == SpanLookup.SpanUnset) {
+ return i
+ }
+ }
+ return -1
+}
+
+private fun SpanLookup.findNextItemIndex(item: Int, column: Int): Int {
+ for (i in (item + 1) until capacity()) {
+ val span = getSpan(i)
+ if (span == column || span == SpanLookup.SpanUnset) {
+ return i
+ }
+ }
+ return capacity()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class LazyStaggeredGridMeasureProvider(
+ private val isVertical: Boolean,
+ private val itemProvider: LazyLayoutItemProvider,
+ private val measureScope: LazyLayoutMeasureScope,
+ private val resolvedSlotSums: IntArray,
+ private val measuredItemFactory: MeasuredItemFactory
+) {
+ fun childConstraints(slot: Int): Constraints {
+ val previousSum = if (slot == 0) 0 else resolvedSlotSums[slot - 1]
+ val crossAxisSize = resolvedSlotSums[slot] - previousSum
+ return if (isVertical) {
+ Constraints.fixedWidth(crossAxisSize)
+ } else {
+ Constraints.fixedHeight(crossAxisSize)
+ }
+ }
+
+ fun getAndMeasure(index: Int, slot: Int): LazyStaggeredGridMeasuredItem {
+ val key = itemProvider.getKey(index)
+ val placeables = measureScope.measure(index, childConstraints(slot))
+ return measuredItemFactory.createItem(index, key, placeables)
+ }
+}
+
+private class LazyStaggeredGridMeasuredItem(
+ val index: Int,
+ val key: Any,
+ val placeables: Array<Placeable>,
+ val isVertical: Boolean
+) {
+ val sizeWithSpacings: Int = placeables.fold(0) { size, placeable ->
+ size + if (isVertical) placeable.height else placeable.width
+ }
+
+ val crossAxisSize: Int = placeables.maxOf {
+ if (isVertical) it.width else it.height
+ }
+
+ fun position(
+ mainAxis: Int,
+ crossAxis: Int,
+ ): LazyStaggeredGridPositionedItem =
+ LazyStaggeredGridPositionedItem(
+ offset = if (isVertical) {
+ IntOffset(crossAxis, mainAxis)
+ } else {
+ IntOffset(mainAxis, crossAxis)
+ },
+ index = index,
+ key = key,
+ size = IntSize(sizeWithSpacings, crossAxisSize),
+ placeables = placeables
+ )
+}
+
+private class LazyStaggeredGridPositionedItem(
+ override val offset: IntOffset,
+ override val index: Int,
+ override val key: Any,
+ override val size: IntSize,
+ val placeables: Array<Placeable>
+) : LazyStaggeredGridItemInfo {
+ fun place(scope: Placeable.PlacementScope) = with(scope) {
+ placeables.forEach { placeable ->
+ placeable.placeWithLayer(offset)
+ }
+ }
+}
+
+// This interface allows to avoid autoboxing on index param
+private fun interface MeasuredItemFactory {
+ fun createItem(
+ index: Int,
+ key: Any,
+ placeables: Array<Placeable>
+ ): LazyStaggeredGridMeasuredItem
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
new file mode 100644
index 0000000..4abccf9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+internal class LazyStaggeredGridMeasureResult(
+ val firstVisibleItemIndices: IntArray,
+ val firstVisibleItemScrollOffsets: IntArray,
+ val consumedScroll: Float,
+ val measureResult: MeasureResult,
+ val canScrollForward: Boolean,
+ val canScrollBackward: Boolean,
+ val visibleItemsInfo: Array<List<LazyStaggeredGridItemInfo>>
+) : MeasureResult by measureResult
+
+internal interface LazyStaggeredGridItemInfo {
+ val offset: IntOffset
+ val index: Int
+ val key: Any
+ val size: IntSize
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
new file mode 100644
index 0000000..3881306
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.lazy.layout.LazyLayoutIntervalContent
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@ExperimentalFoundationApi
+internal class LazyStaggeredGridScopeImpl : LazyStaggeredGridScope {
+ val intervals = MutableIntervalList<LazyStaggeredGridIntervalContent>()
+
+ override fun items(
+ count: Int,
+ key: ((index: Int) -> Any)?,
+ contentType: (index: Int) -> Any?,
+ itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+ ) {
+ intervals.addInterval(
+ count,
+ LazyStaggeredGridIntervalContent(
+ key,
+ contentType,
+ itemContent
+ )
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal object LazyStaggeredGridItemScopeImpl : LazyStaggeredGridItemScope
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyStaggeredGridIntervalContent(
+ override val key: ((index: Int) -> Any)?,
+ override val type: ((index: Int) -> Any?),
+ val item: @Composable LazyStaggeredGridItemScope.(Int) -> Unit
+) : LazyLayoutIntervalContent
\ No newline at end of file
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
new file mode 100644
index 0000000..cef7880
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.MutatePriority
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import kotlin.math.abs
+
+@ExperimentalFoundationApi
+internal class LazyStaggeredGridState : ScrollableState {
+ var firstVisibleItems: IntArray by mutableStateOf(IntArray(0))
+ private set
+
+ var firstVisibleItemScrollOffsets: IntArray by mutableStateOf(IntArray(0))
+ private set
+
+ internal val spans: SpanLookup = SpanLookup()
+
+ private var canScrollForward = true
+ private var canScrollBackward = true
+
+ private var remeasurement: Remeasurement? = null
+
+ internal val remeasurementModifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ this@LazyStaggeredGridState.remeasurement = remeasurement
+ }
+ }
+
+ internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState()
+
+ internal var prefetchingEnabled = true
+
+ private val scrollableState = ScrollableState { -onScroll(-it) }
+
+ internal var scrollToBeConsumed = 0f
+ private set
+
+ /* @VisibleForTesting */
+ internal var measurePassCount = 0
+
+ override suspend fun scroll(
+ scrollPriority: MutatePriority,
+ block: suspend ScrollScope.() -> Unit
+ ) {
+ scrollableState.scroll(scrollPriority, block)
+ }
+
+ private fun onScroll(distance: Float): Float {
+ if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+ return 0f
+ }
+ check(abs(scrollToBeConsumed) <= 0.5f) {
+ "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+ }
+ scrollToBeConsumed += distance
+
+ // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+ // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+ // we have less than 0.5 pixels
+ if (abs(scrollToBeConsumed) > 0.5f) {
+ remeasurement?.forceRemeasure()
+ // todo(b/182882362): notify prefetch
+// if (prefetchingEnabled) {
+// val leftoverScroll = preScrollToBeConsumed - scrollToBeConsumed
+// notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+// }
+ }
+
+ // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+ if (abs(scrollToBeConsumed) <= 0.5f) {
+ // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+ // that we consumed the whole thing
+ return distance
+ } else {
+ val scrollConsumed = distance - scrollToBeConsumed
+ // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+ // nested scrolling)
+ scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+ return scrollConsumed
+ }
+ }
+
+ override fun dispatchRawDelta(delta: Float): Float =
+ scrollableState.dispatchRawDelta(delta)
+
+ internal fun applyMeasureResult(result: LazyStaggeredGridMeasureResult) {
+ scrollToBeConsumed -= result.consumedScroll
+ firstVisibleItems = result.firstVisibleItemIndices
+ firstVisibleItemScrollOffsets = result.firstVisibleItemScrollOffsets
+ canScrollBackward = result.canScrollBackward
+ canScrollForward = result.canScrollForward
+
+ measurePassCount++
+ }
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
+}
+
+internal class SpanLookup {
+ private var spans = IntArray(16)
+
+ fun setSpan(item: Int, span: Int) {
+ ensureCapacity(item + 1)
+ spans[item] = span + 1
+ }
+
+ fun getSpan(item: Int): Int =
+ spans[item] - 1
+
+ fun capacity(): Int =
+ spans.size
+
+ private fun ensureCapacity(capacity: Int) {
+ if (spans.size < capacity) {
+ spans = spans.copyInto(IntArray(spans.size * 2))
+ }
+ }
+
+ fun reset() {
+ spans.fill(0)
+ }
+
+ companion object {
+ internal const val SpanUnset = -1
+ }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
index e6adabb..a8f5d7e4 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
@@ -234,7 +234,9 @@
// Track parent bounds and content size; only show popup once we have both
val canCalculatePosition by derivedStateOf { parentBounds != null && popupContentSize != null }
- private val maxSupportedElevation = 30.dp
+ // On systems older than Android S, there is a bug in the surface insets matrix math used by
+ // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+ private val maxSupportedElevation = 8.dp
// The window visible frame used for the last popup position calculation.
private val previousWindowVisibleFrame = Rect()
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
index 2c2d28c..f05f2bf 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
@@ -235,7 +235,9 @@
// Track parent bounds and content size; only show popup once we have both
val canCalculatePosition by derivedStateOf { parentBounds != null && popupContentSize != null }
- private val maxSupportedElevation = 30.dp
+ // On systems older than Android S, there is a bug in the surface insets matrix math used by
+ // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+ private val maxSupportedElevation = 8.dp
// The window visible frame used for the last popup position calculation.
private val previousWindowVisibleFrame = Rect()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
index 6b31b4e..6bfd57c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
@@ -91,9 +91,7 @@
* Remove all values from the set.
*/
fun clear() {
- for (i in 0 until size) {
- values[i] = null
- }
+ values.fill(null)
size = 0
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 2e0396c..b7f31f9 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -21,6 +21,7 @@
import androidx.compose.runtime.TestOnly
import androidx.compose.runtime.collection.IdentityArrayIntMap
import androidx.compose.runtime.collection.IdentityArrayMap
+import androidx.compose.runtime.collection.IdentityArraySet
import androidx.compose.runtime.collection.IdentityScopeMap
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.observeDerivedStateRecalculations
@@ -261,7 +262,7 @@
/**
* Scopes that were invalidated during previous apply step.
*/
- private val invalidated = hashSetOf<Any>()
+ private val invalidated = IdentityArraySet<Any>()
// derived state handling
@@ -417,7 +418,7 @@
// Invalidate only if currentValue is different than observed on read
if (!policy.equivalent(derivedState.currentValue, previousValue)) {
valueToScopes.forEachScopeOf(derivedState) { scope ->
- invalidated += scope
+ invalidated.add(scope)
hasValues = true
}
}
@@ -425,7 +426,7 @@
}
valueToScopes.forEachScopeOf(value) { scope ->
- invalidated += scope
+ invalidated.add(scope)
hasValues = true
}
}
@@ -436,7 +437,7 @@
* Call [onChanged] for previously invalidated scopes.
*/
fun notifyInvalidatedScopes() {
- invalidated.forEach(onChanged)
+ invalidated.fastForEach(onChanged)
invalidated.clear()
}
}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index dbc0531..057beaf 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -640,9 +640,6 @@
public final class AndroidFontUtils_androidKt {
}
- public final class AndroidLoadableFontsKt {
- }
-
public final class AndroidTypeface_androidKt {
method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
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 62e484e..cde0c73 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -677,6 +677,10 @@
}
public final class AndroidFontKt {
+ method @Deprecated @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.content.res.AssetManager assetManager, String path, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
+ method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(String path, android.content.res.AssetManager assetManager, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
+ method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(java.io.File file, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
+ method @RequiresApi(26) @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.os.ParcelFileDescriptor fileDescriptor, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
}
public final class AndroidFontLoader_androidKt {
@@ -688,13 +692,6 @@
public final class AndroidFontUtils_androidKt {
}
- public final class AndroidLoadableFontsKt {
- method @Deprecated @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.content.res.AssetManager assetManager, String path, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
- method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(String path, android.content.res.AssetManager assetManager, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
- method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(java.io.File file, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
- method @RequiresApi(26) @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.os.ParcelFileDescriptor fileDescriptor, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
- }
-
public final class AndroidTypeface_androidKt {
method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index dbc0531..057beaf 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -640,9 +640,6 @@
public final class AndroidFontUtils_androidKt {
}
- public final class AndroidLoadableFontsKt {
- }
-
public final class AndroidTypeface_androidKt {
method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
index 4582e7b..524441d 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
@@ -17,8 +17,110 @@
package androidx.compose.ui.text.font
import android.content.Context
+import android.content.res.AssetManager
import android.graphics.Typeface
+import android.os.ParcelFileDescriptor
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Stable
import androidx.compose.ui.text.ExperimentalTextApi
+import java.io.File
+
+/**
+ * Create a Font declaration from a file in the assets directory. The content of the [File] is
+ * read during construction.
+ *
+ * @param assetManager Android AssetManager
+ * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
+ * assets/dir/myfont.ttf).
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ */
+@ExperimentalTextApi
+@Stable
+@Deprecated("This experimental Font is replaced by Font(path, assetManager, ...)",
+ replaceWith = ReplaceWith("Font(path, assetManager, weight, style)"),
+ level = DeprecationLevel.WARNING
+)
+fun Font(
+ assetManager: AssetManager,
+ path: String,
+ weight: FontWeight = FontWeight.Normal,
+ style: FontStyle = FontStyle.Normal
+): Font = AndroidAssetFont(
+ assetManager,
+ path,
+ weight,
+ style,
+ FontVariation.Settings(weight, style)
+)
+
+/**
+ * Create a Font declaration from a file in the assets directory. The content of the [File] is
+ * read during construction.
+ *
+ * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
+ * assets/dir/myfont.ttf).
+ * @param assetManager Android AssetManager
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings on API 26 and above these settings are applied to a variable font when
+ * the font is loaded
+ */
+@ExperimentalTextApi
+@Stable
+fun Font(
+ path: String,
+ assetManager: AssetManager,
+ weight: FontWeight = FontWeight.Normal,
+ style: FontStyle = FontStyle.Normal,
+ variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidAssetFont(assetManager, path, weight, style, variationSettings)
+
+/**
+ * Create a Font declaration from a file. The content of the [File] is read during construction.
+ *
+ * @param file the font file.
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings on API 26 and above these settings are applied to a variable font when
+ * the font is loaded
+ */
+@ExperimentalTextApi
+@Stable
+@Suppress("StreamFiles")
+fun Font(
+ file: File,
+ weight: FontWeight = FontWeight.Normal,
+ style: FontStyle = FontStyle.Normal,
+ variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidFileFont(file, weight, style, variationSettings)
+
+/**
+ * Create a Font declaration from a [ParcelFileDescriptor]. The content of the
+ * [ParcelFileDescriptor] is read during construction.
+ *
+ * @param fileDescriptor the file descriptor for the font file.
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings these settings are applied to a variable font when the font is loaded
+ */
+@RequiresApi(26)
+@ExperimentalTextApi
+@Stable
+fun Font(
+ fileDescriptor: ParcelFileDescriptor,
+ weight: FontWeight = FontWeight.Normal,
+ style: FontStyle = FontStyle.Normal,
+ variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidFileDescriptorFont(fileDescriptor, weight, style, variationSettings)
/**
* Font for use on Android.
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
similarity index 63%
rename from compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt
rename to compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
index 07406a8..5f0cad7 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
@@ -24,109 +24,11 @@
import android.os.ParcelFileDescriptor
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
-import androidx.compose.runtime.Stable
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastMap
import java.io.File
-/**
- * Create a Font declaration from a file in the assets directory. The content of the [File] is
- * read during construction.
- *
- * @param assetManager Android AssetManager
- * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
- * assets/dir/myfont.ttf).
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- */
-@ExperimentalTextApi
-@Stable
-@Deprecated("This experimental Font is replaced by Font(path, assetManager, ...)",
- replaceWith = ReplaceWith("Font(path, assetManager, weight, style)"),
- level = DeprecationLevel.WARNING
-)
-fun Font(
- assetManager: AssetManager,
- path: String,
- weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal
-): Font = AndroidAssetFont(
- assetManager,
- path,
- weight,
- style,
- FontVariation.Settings(weight, style)
-)
-
-/**
- * Create a Font declaration from a file in the assets directory. The content of the [File] is
- * read during construction.
- *
- * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
- * assets/dir/myfont.ttf).
- * @param assetManager Android AssetManager
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings on API 26 and above these settings are applied to a variable font when
- * the font is loaded
- */
-@ExperimentalTextApi
-@Stable
-fun Font(
- path: String,
- assetManager: AssetManager,
- weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal,
- variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidAssetFont(assetManager, path, weight, style, variationSettings)
-
-/**
- * Create a Font declaration from a file. The content of the [File] is read during construction.
- *
- * @param file the font file.
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings on API 26 and above these settings are applied to a variable font when
- * the font is loaded
- */
-@ExperimentalTextApi
-@Stable
-@Suppress("StreamFiles")
-fun Font(
- file: File,
- weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal,
- variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidFileFont(file, weight, style, variationSettings)
-
-/**
- * Create a Font declaration from a [ParcelFileDescriptor]. The content of the
- * [ParcelFileDescriptor] is read during construction.
- *
- * @param fileDescriptor the file descriptor for the font file.
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings these settings are applied to a variable font when the font is loaded
- */
-@RequiresApi(26)
-@ExperimentalTextApi
-@Stable
-fun Font(
- fileDescriptor: ParcelFileDescriptor,
- weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal,
- variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidFileDescriptorFont(fileDescriptor, weight, style, variationSettings)
-
@OptIn(ExperimentalTextApi::class)
internal sealed class AndroidPreloadedFont @OptIn(ExperimentalTextApi::class) constructor(
final override val weight: FontWeight,
@@ -163,7 +65,7 @@
}
@OptIn(ExperimentalTextApi::class) /* FontVariation.Settings */
-private class AndroidAssetFont constructor(
+internal class AndroidAssetFont constructor(
val assetManager: AssetManager,
val path: String,
weight: FontWeight = FontWeight.Normal,
@@ -207,7 +109,7 @@
}
@OptIn(ExperimentalTextApi::class)
-private class AndroidFileFont constructor(
+internal class AndroidFileFont constructor(
val file: File,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
@@ -234,7 +136,7 @@
@RequiresApi(26)
@OptIn(ExperimentalTextApi::class)
-private class AndroidFileDescriptorFont constructor(
+internal class AndroidFileDescriptorFont constructor(
val fileDescriptor: ParcelFileDescriptor,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index b41257c..cfb4a53 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -88,7 +88,7 @@
* system service may silently ignore this request.
*/
@Deprecated(
- message = "Use SoftwareKeyboardController.showSoftwareKeyboard or " +
+ message = "Use SoftwareKeyboardController.show or " +
"TextInputSession.showSoftwareKeyboard instead.",
replaceWith = ReplaceWith("textInputSession.showSoftwareKeyboard()")
)
@@ -103,7 +103,7 @@
* Hide onscreen keyboard.
*/
@Deprecated(
- message = "Use SoftwareKeyboardController.hideSoftwareKeyboard or " +
+ message = "Use SoftwareKeyboardController.hide or " +
"TextInputSession.hideSoftwareKeyboard instead.",
replaceWith = ReplaceWith("textInputSession.hideSoftwareKeyboard()")
)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
index bb15949..8fbb979 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
@@ -34,6 +34,7 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.matchers.MSSIMMatcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -71,7 +72,7 @@
fun dialogWithElevation() {
rule.setContent {
Dialog(onDismissRequest = {}) {
- val elevation = with(LocalDensity.current) { 16.dp.toPx() }
+ val elevation = with(LocalDensity.current) { 8.dp.toPx() }
Box(
Modifier
.graphicsLayer(
@@ -87,6 +88,10 @@
rule.onNode(isDialog())
.captureToImage()
- .assertAgainstGolden(screenshotRule, "dialogWithElevation")
+ .assertAgainstGolden(
+ screenshotRule,
+ "dialogWithElevation",
+ matcher = MSSIMMatcher(threshold = 0.999)
+ )
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index c615ddc..f7a7972 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -98,7 +98,7 @@
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
- ) : this (
+ ) : this(
dismissOnBackPress = dismissOnBackPress,
dismissOnClickOutside = dismissOnClickOutside,
securePolicy = securePolicy,
@@ -297,7 +297,9 @@
private val dialogLayout: DialogLayout
- private val maxSupportedElevation = 30.dp
+ // On systems older than Android S, there is a bug in the surface insets matrix math used by
+ // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+ private val maxSupportedElevation = 8.dp
override val subCompositionView: AbstractComposeView get() = dialogLayout
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 1250af4..9702046 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -406,7 +406,9 @@
parentLayoutCoordinates != null && popupContentSize != null
}
- private val maxSupportedElevation = 30.dp
+ // On systems older than Android S, there is a bug in the surface insets matrix math used by
+ // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+ private val maxSupportedElevation = 8.dp
// The window visible frame used for the last popup position calculation.
private val previousWindowVisibleFrame = Rect()
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 198de14..accad37 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -107,15 +107,15 @@
docs("androidx.core.uwb:uwb:1.0.0-alpha03")
docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha03")
docs("androidx.core:core-google-shortcuts:1.1.0-alpha02")
- docs("androidx.core:core-performance:1.0.0-alpha02")
- samples("androidx.core:core-performance-samples:1.0.0-alpha02")
+ docs("androidx.core:core-performance:1.0.0-alpha03")
+ samples("androidx.core:core-performance-samples:1.0.0-alpha03")
docs("androidx.core:core-remoteviews:1.0.0-beta02")
- docs("androidx.core:core-role:1.1.0-rc01")
- docs("androidx.core:core-animation:1.0.0-beta01")
- docs("androidx.core:core-animation-testing:1.0.0-alpha02")
- docs("androidx.core:core:1.9.0-beta01")
- docs("androidx.core:core-ktx:1.9.0-alpha05")
- docs("androidx.core:core-splashscreen:1.0.0")
+ docs("androidx.core:core-role:1.2.0-alpha01")
+ docs("androidx.core:core-animation:1.0.0-beta02")
+ docs("androidx.core:core-animation-testing:1.0.0-beta01")
+ docs("androidx.core:core:1.9.0-rc01")
+ docs("androidx.core:core-ktx:1.9.0-rc01")
+ docs("androidx.core:core-splashscreen:1.1.0-alpha01")
docs("androidx.cursoradapter:cursoradapter:1.0.0")
docs("androidx.customview:customview:1.2.0-alpha01")
docs("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 4f61b07..714f730 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -35,10 +35,38 @@
</receiver>
<receiver
- android:name="androidx.glance.appwidget.template.demos.GalleryDemoWidgetReceiver"
+ android:name="androidx.glance.appwidget.template.demos.SmallImageGalleryReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
- android:label="@string/gallery_template_name">
+ android:label="@string/small_image_gallery">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ <action android:name="android.intent.action.LOCALE_CHANGED" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/default_app_widget_info" />
+ </receiver>
+
+ <receiver
+ android:name="androidx.glance.appwidget.template.demos.MediumImageGalleryReceiver"
+ android:enabled="@bool/glance_appwidget_available"
+ android:exported="false"
+ android:label="@string/medium_image_gallery">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ <action android:name="android.intent.action.LOCALE_CHANGED" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/default_app_widget_info" />
+ </receiver>
+
+ <receiver
+ android:name="androidx.glance.appwidget.template.demos.LargeImageGalleryReceiver"
+ android:enabled="@bool/glance_appwidget_available"
+ android:exported="false"
+ android:label="@string/large_image_gallery">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
index 58496d2..2c00db6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
@@ -25,9 +25,11 @@
import androidx.glance.appwidget.template.GalleryTemplate
import androidx.glance.appwidget.template.GlanceTemplateAppWidget
import androidx.glance.template.ActionBlock
+import androidx.glance.template.AspectRatio
import androidx.glance.template.GalleryTemplateData
import androidx.glance.template.HeaderBlock
import androidx.glance.template.ImageBlock
+import androidx.glance.template.ImageSize
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
import androidx.glance.template.TemplateTextButton
@@ -35,13 +37,58 @@
import androidx.glance.template.TextType
/**
- * A widget that uses [GalleryTemplate].
+ * Gallery demo for the default Small sized images with 1:1 aspect ratio and left-to-right main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
*/
-class GalleryTemplateWidget : GlanceTemplateAppWidget() {
+class SmallGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+ @Composable
+ override fun TemplateContent() = GalleryTemplateContent()
+}
+
+/**
+ * Gallery demo for the Medium sized images with 16:9 aspect ratio and right-to-left main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
+ */
+class MediumGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+ @Composable
+ override fun TemplateContent() =
+ GalleryTemplateContent(ImageSize.Medium, AspectRatio.Ratio16x9, false)
+}
+
+/**
+ * Gallery demo for the Large sized images with 2:3 aspect ratio and left-to-right main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
+ */
+class LargeGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+ @Composable
+ override fun TemplateContent() = GalleryTemplateContent(ImageSize.Large, AspectRatio.Ratio2x3)
+}
+
+class SmallImageGalleryReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = SmallGalleryTemplateDemoWidget()
+}
+
+class MediumImageGalleryReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = MediumGalleryTemplateDemoWidget()
+}
+
+class LargeImageGalleryReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = LargeGalleryTemplateDemoWidget()
+}
+
+/**
+ * Base Gallery Demo widget binding [GalleryTemplateData] to [GalleryTemplate] layout.
+ * It is overridable by gallery image aspect ratio, image size, and main blocks ordering.
+ */
+abstract class BaseGalleryTemplateWidget : GlanceTemplateAppWidget() {
override val sizeMode = SizeMode.Exact
@Composable
- override fun TemplateContent() {
+ internal fun GalleryTemplateContent(
+ imageSize: ImageSize = ImageSize.Small,
+ aspectRatio: AspectRatio = AspectRatio.Ratio1x1,
+ isMainTextBlockFirst: Boolean = true,
+ ) {
val galleryContent = mutableListOf<TemplateImageWithDescription>()
for (i in 1..30) {
galleryContent.add(
@@ -64,7 +111,7 @@
text1 = TemplateText("Title1", TextType.Title),
text2 = TemplateText("Headline1", TextType.Headline),
text3 = TemplateText("Label1", TextType.Label),
- priority = 0,
+ priority = if (isMainTextBlockFirst) 0 else 1,
),
mainImageBlock = ImageBlock(
images = listOf(
@@ -73,7 +120,7 @@
"test image"
)
),
- priority = 1,
+ priority = if (isMainTextBlockFirst) 1 else 0,
),
mainActionBlock = ActionBlock(
actionButtons = listOf(
@@ -89,13 +136,10 @@
),
galleryImageBlock = ImageBlock(
images = galleryContent,
- priority = 2,
+ aspectRatio = aspectRatio,
+ size = imageSize,
),
)
)
}
}
-
-class GalleryDemoWidgetReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = GalleryTemplateWidget()
-}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
index 8277be9..4ee5288 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
@@ -19,8 +19,6 @@
<!-- Name of the app widgets -->
<string name="single_entity_template_widget_name"><u>Single Entity Template demo</u></string>
- <string name="gallery_template_name"><u>Gallery demo</u></string>
- <string name="list_template_widget_name"><u>List Template demo</u></string>
<string name="override_widget_name"><u>Template layout override demo</u></string>
<string name="template_data_saved_message">Saved</string>
@@ -32,4 +30,9 @@
<string name="list_style_with_header">List Template with header demo</string>
<string name="list_style_no_header">List Template with no header demo</string>
<string name="list_style_brief">List Template with no header demo in brief info</string>
+
+ <!-- Sample of the Gallery Template widgets -->
+ <string name="small_image_gallery">Gallery Template with small 1:1 images, text first</string>
+ <string name="medium_image_gallery">Gallery Template with medium 16:9 images, text last</string>
+ <string name="large_image_gallery">Gallery Template with large 2:3 images, text first</string>
</resources>
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 4dc90d3..943136b 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -5,7 +5,7 @@
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
- method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1303,15 +1303,15 @@
}
public final class Metadata {
- ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
- method public String? getClientId();
- method public long getClientVersion();
+ ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+ method public String? getClientRecordId();
+ method public long getClientRecordVersion();
method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
method public androidx.health.connect.client.records.metadata.Device? getDevice();
method public java.time.Instant getLastModifiedTime();
method public String getUid();
- property public final String? clientId;
- property public final long clientVersion;
+ property public final String? clientRecordId;
+ property public final long clientRecordVersion;
property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
property public final androidx.health.connect.client.records.metadata.Device? device;
property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 4dc90d3..943136b 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -5,7 +5,7 @@
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
- method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1303,15 +1303,15 @@
}
public final class Metadata {
- ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
- method public String? getClientId();
- method public long getClientVersion();
+ ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+ method public String? getClientRecordId();
+ method public long getClientRecordVersion();
method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
method public androidx.health.connect.client.records.metadata.Device? getDevice();
method public java.time.Instant getLastModifiedTime();
method public String getUid();
- property public final String? clientId;
- property public final long clientVersion;
+ property public final String? clientRecordId;
+ property public final long clientRecordVersion;
property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
property public final androidx.health.connect.client.records.metadata.Device? device;
property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index b0afe55..9373082 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -5,7 +5,7 @@
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
- method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1326,15 +1326,15 @@
}
public final class Metadata {
- ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
- method public String? getClientId();
- method public long getClientVersion();
+ ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+ method public String? getClientRecordId();
+ method public long getClientRecordVersion();
method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
method public androidx.health.connect.client.records.metadata.Device? getDevice();
method public java.time.Instant getLastModifiedTime();
method public String getUid();
- property public final String? clientId;
- property public final long clientVersion;
+ property public final String? clientRecordId;
+ property public final long clientRecordVersion;
property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
property public final androidx.health.connect.client.records.metadata.Device? device;
property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
index da51ac0..892e6f2 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
@@ -35,7 +35,7 @@
healthConnectClient.deleteRecords(
StepsRecord::class,
uidsList = listOf(uid1, uid2),
- clientIdsList = emptyList()
+ clientRecordIdsList = emptyList()
)
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 1a426c1..61e9f8d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -64,10 +64,10 @@
* To insert some heart rate data:
* @sample androidx.health.connect.client.samples.InsertHeartRateSeries
*
- * [androidx.health.connect.client.records.metadata.Metadata.clientId] can be used to
+ * [androidx.health.connect.client.records.metadata.Metadata.clientRecordId] can be used to
* deduplicate data with a client provided unique identifier. When a subsequent [insertRecords]
- * is called with the same [androidx.health.connect.client.records.metadata.Metadata.clientId],
- * whichever [Record] with the higher [androidx.health.connect.client.records.metadata.Metadata.clientVersion]
+ * is called with the same [androidx.health.connect.client.records.metadata.Metadata.clientRecordId],
+ * whichever [Record] with the higher [androidx.health.connect.client.records.metadata.Metadata.clientRecordVersion]
* takes precedence.
*
* @param records List of records to insert
@@ -101,7 +101,7 @@
*
* @param recordType Which type of [Record] to delete, such as `Steps::class`
* @param uidsList List of uids of [Record] to delete
- * @param clientIdsList List of client IDs of [Record] to delete
+ * @param clientRecordIdsList List of client record IDs of [Record] to delete
* @throws RemoteException For any IPC transportation failures. Deleting by invalid identifiers
* such as a non-existing identifier or deleting the same record multiple times will result in
* IPC failure.
@@ -112,7 +112,7 @@
suspend fun deleteRecords(
recordType: KClass<out Record>,
uidsList: List<String>,
- clientIdsList: List<String>,
+ clientRecordIdsList: List<String>,
)
/**
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index c8b51e8..4140dce 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -110,17 +110,17 @@
override suspend fun deleteRecords(
recordType: KClass<out Record>,
uidsList: List<String>,
- clientIdsList: List<String>,
+ clientRecordIdsList: List<String>,
) {
delegate
.deleteData(
toDataTypeIdPairProtoList(recordType, uidsList),
- toDataTypeIdPairProtoList(recordType, clientIdsList)
+ toDataTypeIdPairProtoList(recordType, clientRecordIdsList)
)
.await()
Logger.debug(
HEALTH_CONNECT_CLIENT_TAG,
- "${uidsList.size + clientIdsList.size} records deleted."
+ "${uidsList.size + clientRecordIdsList.size} records deleted."
)
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
index 53ed952..afcfac2 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -82,8 +82,8 @@
uid = if (hasUid()) uid else Metadata.EMPTY_UID,
dataOrigin = DataOrigin(dataOrigin.applicationId),
lastModifiedTime = Instant.ofEpochMilli(updateTimeMillis),
- clientId = if (hasClientId()) clientId else null,
- clientVersion = clientVersion,
+ clientRecordId = if (hasClientId()) clientId else null,
+ clientRecordVersion = clientVersion,
device = toDevice(device)
)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
index e8bd828..7ced9bd 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
@@ -67,9 +67,9 @@
setUpdateTimeMillis(metadata.lastModifiedTime.toEpochMilli())
}
- metadata.clientId?.let { setClientId(it) }
- if (metadata.clientVersion > 0) {
- metadata.clientVersion.let { setClientVersion(it) }
+ metadata.clientRecordId?.let { setClientId(it) }
+ if (metadata.clientRecordVersion > 0) {
+ metadata.clientRecordVersion.let { setClientVersion(it) }
}
metadata.device?.let { setDevice(it.toProto()) }
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
index 8f97dff..2861b86 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
@@ -43,26 +43,26 @@
public val lastModifiedTime: Instant = Instant.EPOCH,
/**
- * Optional client supplied unique data identifier associated with the data.
+ * Optional client supplied record unique data identifier associated with the data.
*
* There is guaranteed a single entry for any type of data with same client provided identifier
* for a given client. Any new insertions with the same client provided identifier will either
- * replace or be ignored depending on associated [clientVersion].
+ * replace or be ignored depending on associated [clientRecordVersion].
*
- * @see clientVersion
+ * @see clientRecordVersion
*/
- public val clientId: String? = null,
+ public val clientRecordId: String? = null,
/**
* Optional client supplied version associated with the data.
*
* This determines conflict resolution outcome when there are multiple insertions of the same
- * [clientId]. Data with the highest [clientVersion] takes precedence. [clientVersion] starts
- * with 0.
+ * [clientRecordId]. Data with the highest [clientRecordVersion] takes precedence.
+ * [clientRecordVersion] starts with 0.
*
- * @see clientId
+ * @see clientRecordId
*/
- public val clientVersion: Long = 0,
+ public val clientRecordVersion: Long = 0,
/** Optional client supplied device information associated with the data. */
public val device: Device? = null,
@@ -74,8 +74,8 @@
if (uid != other.uid) return false
if (dataOrigin != other.dataOrigin) return false
if (lastModifiedTime != other.lastModifiedTime) return false
- if (clientId != other.clientId) return false
- if (clientVersion != other.clientVersion) return false
+ if (clientRecordId != other.clientRecordId) return false
+ if (clientRecordVersion != other.clientRecordVersion) return false
if (device != other.device) return false
return true
@@ -85,8 +85,8 @@
var result = uid.hashCode()
result = 31 * result + dataOrigin.hashCode()
result = 31 * result + lastModifiedTime.hashCode()
- result = 31 * result + (clientId?.hashCode() ?: 0)
- result = 31 * result + clientVersion.hashCode()
+ result = 31 * result + (clientRecordId?.hashCode() ?: 0)
+ result = 31 * result + clientRecordVersion.hashCode()
result = 31 * result + (device?.hashCode() ?: 0)
return result
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 1510f7b..bf29d60 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -109,8 +109,8 @@
private val TEST_METADATA =
Metadata(
uid = "uid",
- clientId = "clientId",
- clientVersion = 10,
+ clientRecordId = "clientId",
+ clientRecordVersion = 10,
device = Device(manufacturer = "manufacturer"),
lastModifiedTime = END_TIME,
dataOrigin = DataOrigin(packageName = "appId")
diff --git a/libraryversions.toml b/libraryversions.toml
index dfba9d5..cabe69c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,9 +1,9 @@
[versions]
-ACTIVITY = "1.6.0-beta02"
+ACTIVITY = "1.6.0-rc01"
ADS_IDENTIFIER = "1.0.0-alpha05"
ANNOTATION = "1.5.0-beta01"
ANNOTATION_EXPERIMENTAL = "1.3.0-rc01"
-APPCOMPAT = "1.6.0-beta02"
+APPCOMPAT = "1.6.0-rc01"
APPSEARCH = "1.1.0-alpha02"
ARCH_CORE = "2.2.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha01"
diff --git a/privacysandbox/tools/tools-apicompiler/build.gradle b/privacysandbox/tools/tools-apicompiler/build.gradle
index f4cab03..39983af 100644
--- a/privacysandbox/tools/tools-apicompiler/build.gradle
+++ b/privacysandbox/tools/tools-apicompiler/build.gradle
@@ -24,6 +24,11 @@
dependencies {
api(libs.kotlinStdlib)
implementation(libs.kspApi)
+ implementation project(path: ':privacysandbox:tools:tools')
+
+ testImplementation(project(":room:room-compiler-processing-testing"))
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
}
androidx {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index 6f326cd..8fc30bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -16,20 +16,21 @@
package androidx.privacysandbox.tools.apicompiler
+import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
-class PrivacySandboxKspCompiler() : SymbolProcessor {
+class PrivacySandboxKspCompiler(val logger: KSPLogger) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
return emptyList()
}
class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
- return PrivacySandboxKspCompiler()
+ return PrivacySandboxKspCompiler(environment.logger)
}
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
similarity index 72%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
index 4f92192..bfc8047 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
@@ -14,4 +14,11 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+/** Result of parsing a Kotlin interface. */
+data class AnnotatedInterface(
+ val name: String,
+ val packageName: String,
+ val methods: List<Method> = emptyList(),
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
similarity index 78%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
index 4f92192..ea0f619 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
@@ -14,4 +14,10 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Method(
+ val name: String,
+ val parameters: List<Parameter>,
+ val returnType: Type,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
similarity index 83%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
index 4f92192..6e2c29b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
@@ -14,4 +14,9 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Parameter(
+ val name: String,
+ val type: Type,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
similarity index 76%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
index 4f92192..210f065 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
@@ -14,4 +14,9 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+/** Result of parsing a full developer-defined API for an SDK. */
+data class ParsedApi(
+ val services: Set<AnnotatedInterface>,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
similarity index 86%
rename from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
rename to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
index 4f92192..ae444a8 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
@@ -14,4 +14,8 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Type(
+ val name: String,
+)
\ No newline at end of file
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
new file mode 100644
index 0000000..95d1130
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.model.AnnotatedInterface
+import androidx.privacysandbox.tools.apicompiler.model.Method
+import androidx.privacysandbox.tools.apicompiler.model.Parameter
+import androidx.privacysandbox.tools.apicompiler.model.Type
+import com.google.devtools.ksp.getDeclaredFunctions
+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.KSName
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSValueParameter
+
+/** Convenience extension to get the full qualifier + name from a [KSName]. */
+internal fun KSName.getFullName(): String {
+ if (getQualifier() == "") return getShortName()
+ return "${getQualifier()}.${getShortName()}"
+}
+
+/** 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) {
+
+ fun parseApi(): ParsedApi {
+ return ParsedApi(services = parseAllServices())
+ }
+
+ private fun parseAllServices(): Set<AnnotatedInterface> {
+ val symbolsWithServiceAnnotation = resolver
+ .getSymbolsWithAnnotation(PrivacySandboxService::class.qualifiedName!!)
+ val interfacesWithServiceAnnotation = symbolsWithServiceAnnotation
+ .filterIsInstance<KSClassDeclaration>().filter { it.classKind == ClassKind.INTERFACE }
+ if (symbolsWithServiceAnnotation.count() != interfacesWithServiceAnnotation.count()) {
+ logger.error("Only interfaces can be annotated with @PrivacySandboxService.")
+ return setOf()
+ }
+ return interfacesWithServiceAnnotation
+ .map(this::parseInterface)
+ .toSet()
+ }
+
+ private fun parseInterface(classDeclaration: KSClassDeclaration): AnnotatedInterface {
+ return AnnotatedInterface(
+ name = classDeclaration.simpleName.getShortName(),
+ packageName = classDeclaration.packageName.getFullName(),
+ methods = getAllMethods(classDeclaration),
+ )
+ }
+
+ private fun getAllMethods(classDeclaration: KSClassDeclaration) =
+ classDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
+
+ private fun parseMethod(method: KSFunctionDeclaration): Method {
+ return Method(
+ name = method.simpleName.getFullName(),
+ parameters = getAllParameters(method),
+ // TODO: returnType "Can be null if an error occurred during resolution".
+ returnType = parseType(method.returnType!!.resolve()),
+ )
+ }
+
+ private fun getAllParameters(method: KSFunctionDeclaration) =
+ method.parameters.map(::parseParameter).toList()
+
+ private fun parseParameter(parameter: KSValueParameter): Parameter {
+ return Parameter(
+ name = parameter.name!!.getFullName(),
+ type = parseType(parameter.type.resolve()),
+ )
+ }
+
+ private fun parseType(type: KSType): Type {
+ return Type(
+ name = type.declaration.simpleName.getFullName(),
+ )
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt
new file mode 100644
index 0000000..a4ad823
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.apicompiler.model.AnnotatedInterface
+import androidx.privacysandbox.tools.apicompiler.model.Method
+import androidx.privacysandbox.tools.apicompiler.model.Parameter
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.model.Type
+import androidx.privacysandbox.tools.apicompiler.util.checkSourceFails
+import androidx.privacysandbox.tools.apicompiler.util.parseSource
+import androidx.room.compiler.processing.util.Source
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ApiParserTest {
+
+ @Test
+ fun parseServiceInterface_ok() {
+ val source =
+ Source.kotlin(
+ "com/mysdk/MySdk.kt",
+ """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+ @PrivacySandboxService
+ interface MySdk {
+ fun doStuff(x: Int, y: Int): String
+ }
+ """
+ )
+ assertThat(parseSource(source)).isEqualTo(
+ ParsedApi(
+ services = mutableSetOf(
+ AnnotatedInterface(
+ name = "MySdk",
+ packageName = "com.mysdk",
+ methods = listOf(
+ Method(
+ name = "doStuff",
+ parameters = listOf(
+ Parameter(
+ name = "x",
+ type = Type(
+ name = "Int",
+ )
+ ),
+ Parameter(
+ name = "y",
+ type = Type(
+ name = "Int",
+ )
+ )
+ ),
+ returnType = Type(
+ name = "String",
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ }
+
+ @Test
+ fun serviceAnnotatedClass_fails() {
+ val source =
+ Source.kotlin(
+ "com/mysdk/MySdk.kt",
+ """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+ @PrivacySandboxService
+ abstract class MySdk { // Fails because it's a class, not an interface.
+ abstract fun doStuff(x: Int, y: Int): String
+ }
+ """
+ )
+
+ checkSourceFails(source)
+ .containsError("Only interfaces can be annotated with @PrivacySandboxService.")
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
new file mode 100644
index 0000000..e2a71a8
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.util
+
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
+import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+import androidx.room.compiler.processing.util.compiler.compile
+import com.google.common.truth.Truth.assertThat
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.symbol.KSAnnotated
+import java.nio.file.Files
+import javax.tools.Diagnostic
+
+/**
+ * Helper to run KSP processing functionality in tests.
+ */
+fun parseSource(source: Source): ParsedApi {
+ val provider = CapturingSymbolProcessor.Provider()
+ compile(
+ Files.createTempDirectory("test").toFile(),
+ TestCompilationArguments(
+ sources = listOf(source),
+ symbolProcessorProviders = listOf(provider),
+ )
+ )
+ assert(provider.processor.capture != null) { "KSP run didn't produce any output." }
+ return provider.processor.capture!!
+}
+
+fun checkSourceFails(source: Source): CompilationResultSubject {
+ val provider = CapturingSymbolProcessor.Provider()
+ val result = compile(
+ Files.createTempDirectory("test").toFile(),
+ TestCompilationArguments(
+ sources = listOf(source),
+ symbolProcessorProviders = listOf(provider)
+ )
+ )
+ assertThat(result.success).isFalse()
+ return CompilationResultSubject(result)
+}
+
+class CompilationResultSubject(private val result: TestCompilationResult) {
+ fun containsError(error: String) {
+ assertThat(result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg))
+ .contains(error)
+ }
+}
+
+private class CapturingSymbolProcessor(private val logger: KSPLogger) : SymbolProcessor {
+ var capture: ParsedApi? = null
+
+ override fun process(resolver: Resolver): List<KSAnnotated> {
+ capture = ApiParser(resolver, logger).parseApi()
+ return emptyList()
+ }
+
+ class Provider : SymbolProcessorProvider {
+ lateinit var processor: CapturingSymbolProcessor
+
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ assert(!::processor.isInitialized)
+ processor = CapturingSymbolProcessor(environment.logger)
+ return processor
+ }
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
index c2d6323..145d0d1 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -158,7 +158,7 @@
SimpleSQLiteQuery(
"INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
"VALUES (?,?)",
- arrayOf<Any>("3", "Description")
+ arrayOf("3", "Description")
)
)
assertQueryLogged(
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 2d17a93..c70bacd 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -403,5 +403,10 @@
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
+ }
+
}
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index 2d17a93..c70bacd 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -403,5 +403,10 @@
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
+ }
+
}
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index ab2db40..ddf226b 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -412,5 +412,10 @@
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
+ }
+
}
diff --git a/room/room-common/src/main/java/androidx/room/Upsert.kt b/room/room-common/src/main/java/androidx/room/Upsert.kt
index e7d5560..117159e 100644
--- a/room/room-common/src/main/java/androidx/room/Upsert.kt
+++ b/room/room-common/src/main/java/androidx/room/Upsert.kt
@@ -16,8 +16,6 @@
package androidx.room
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import kotlin.reflect.KClass
/**
@@ -84,7 +82,6 @@
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
-@RestrictTo(LIBRARY_GROUP)
public annotation class Upsert(
/**
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 13d97c6..ad40553 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -125,7 +125,7 @@
dir: provider {
// Wrapping in a provider as we access buildDir before this project is configured
// Replace with AGP API once it is added b/228109260
- "${new File(project(":sqlite:sqlite").buildDir, "libJar")}"
+ "${new File(project(":sqlite:sqlite").buildDir, "intermediates/compile_library_classes_jar/release/")}"
},
include : "*.jar"
))
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
index d53b9c9..ac20e68 100644
--- a/room/room-runtime/api/restricted_current.ignore
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -1,6 +1,30 @@
// Baseline format: 1.0
AddedFinal: androidx.room.Room:
Class androidx.room.Room added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindBlob(int, byte[]):
+ Method androidx.room.RoomSQLiteQuery.bindBlob has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindDouble(int, double):
+ Method androidx.room.RoomSQLiteQuery.bindDouble has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindLong(int, long):
+ Method androidx.room.RoomSQLiteQuery.bindLong has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindNull(int):
+ Method androidx.room.RoomSQLiteQuery.bindNull has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindString(int, String):
+ Method androidx.room.RoomSQLiteQuery.bindString has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindTo(androidx.sqlite.db.SupportSQLiteProgram):
+ Method androidx.room.RoomSQLiteQuery.bindTo has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#clearBindings():
+ Method androidx.room.RoomSQLiteQuery.clearBindings has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#close():
+ Method androidx.room.RoomSQLiteQuery.close has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#copyArgumentsFrom(androidx.room.RoomSQLiteQuery):
+ Method androidx.room.RoomSQLiteQuery.copyArgumentsFrom has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#getArgCount():
+ Method androidx.room.RoomSQLiteQuery.getArgCount has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#getSql():
+ Method androidx.room.RoomSQLiteQuery.getSql has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#release():
+ Method androidx.room.RoomSQLiteQuery.release has added 'final' qualifier
ChangedType: androidx.room.DatabaseConfiguration#autoMigrationSpecs:
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index aa860b1..ee903c6 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -225,28 +225,29 @@
field public final boolean isValid;
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RoomSQLiteQuery implements androidx.sqlite.db.SupportSQLiteProgram androidx.sqlite.db.SupportSQLiteQuery {
- method public static final androidx.room.RoomSQLiteQuery acquire(String query, int argumentCount);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class RoomSQLiteQuery implements androidx.sqlite.db.SupportSQLiteProgram androidx.sqlite.db.SupportSQLiteQuery {
+ method public static androidx.room.RoomSQLiteQuery acquire(String query, int argumentCount);
method public void bindBlob(int index, byte[] value);
method public void bindDouble(int index, double value);
method public void bindLong(int index, long value);
method public void bindNull(int index);
method public void bindString(int index, String value);
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram program);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public void clearBindings();
method public void close();
method public void copyArgumentsFrom(androidx.room.RoomSQLiteQuery other);
- method public static final androidx.room.RoomSQLiteQuery copyFrom(androidx.sqlite.db.SupportSQLiteQuery supportSQLiteQuery);
+ method public static androidx.room.RoomSQLiteQuery copyFrom(androidx.sqlite.db.SupportSQLiteQuery supportSQLiteQuery);
method public int getArgCount();
- method public final int getCapacity();
+ method public int getCapacity();
method public String getSql();
method public void init(String query, int initArgCount);
method public void release();
+ property public int argCount;
property public final int capacity;
+ property public String sql;
field public static final androidx.room.RoomSQLiteQuery.Companion Companion;
field @VisibleForTesting public static final int DESIRED_POOL_SIZE = 10; // 0xa
field @VisibleForTesting public static final int POOL_LIMIT = 15; // 0xf
- field @VisibleForTesting public int argCount;
field @VisibleForTesting public final byte[]![] blobBindings;
field @VisibleForTesting public final double[] doubleBindings;
field @VisibleForTesting public final long[] longBindings;
diff --git a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
index 1ae68ea3..ab9a009 100644
--- a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
@@ -103,7 +103,7 @@
val queryInterceptorProgram = QueryInterceptorProgram()
query.bindTo(queryInterceptorProgram)
queryCallbackExecutor.execute {
- queryCallback.onQuery(query.getSql(), queryInterceptorProgram.bindArgsCache)
+ queryCallback.onQuery(query.sql, queryInterceptorProgram.bindArgsCache)
}
return delegate.query(query)
}
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt b/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
index 4c0ee9b..c13695e 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
@@ -32,7 +32,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-open class RoomSQLiteQuery private constructor(
+class RoomSQLiteQuery private constructor(
@field:VisibleForTesting val capacity: Int
) : SupportSQLiteQuery, SupportSQLiteProgram {
@Volatile
@@ -58,11 +58,10 @@
private val bindingTypes: IntArray
// number of arguments in the query
- @JvmField
- @VisibleForTesting
- var argCount = 0
+ override var argCount = 0
+ private set
- open fun init(query: String, initArgCount: Int) {
+ fun init(query: String, initArgCount: Int) {
this.query = query
argCount = initArgCount
}
@@ -83,29 +82,24 @@
* After released, the statement might be returned when [.acquire] is called
* so you should never re-use it after releasing.
*/
- open fun release() {
+ fun release() {
synchronized(queryPool) {
queryPool[capacity] = this
prunePoolLocked()
}
}
- override fun getSql(): String {
- return requireNotNull(query)
- }
+ override val sql: String
+ get() = checkNotNull(this.query)
- override fun getArgCount(): Int {
- return argCount
- }
-
- override fun bindTo(program: SupportSQLiteProgram) {
+ override fun bindTo(statement: SupportSQLiteProgram) {
for (index in 1..argCount) {
when (bindingTypes[index]) {
- NULL -> program.bindNull(index)
- LONG -> program.bindLong(index, longBindings[index])
- DOUBLE -> program.bindDouble(index, doubleBindings[index])
- STRING -> program.bindString(index, requireNotNull(stringBindings[index]))
- BLOB -> program.bindBlob(index, requireNotNull(blobBindings[index]))
+ NULL -> statement.bindNull(index)
+ LONG -> statement.bindLong(index, longBindings[index])
+ DOUBLE -> statement.bindDouble(index, doubleBindings[index])
+ STRING -> statement.bindString(index, requireNotNull(stringBindings[index]))
+ BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
}
}
}
@@ -143,7 +137,7 @@
*
* @param other The other query, which holds the arguments to be copied.
*/
- open fun copyArgumentsFrom(other: RoomSQLiteQuery) {
+ fun copyArgumentsFrom(other: RoomSQLiteQuery) {
val argCount = other.argCount + 1 // +1 for the binding offsets
System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)
System.arraycopy(other.longBindings, 0, longBindings, 0, argCount)
diff --git a/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java b/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
index 3ff9773..0738a49 100644
--- a/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
+++ b/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
@@ -45,7 +45,7 @@
public void acquireBasic() {
RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
assertThat(query.getSql(), is("abc"));
- assertThat(query.argCount, is(3));
+ assertThat(query.getArgCount(), is(3));
assertThat(query.blobBindings.length, is(4));
assertThat(query.longBindings.length, is(4));
assertThat(query.stringBindings.length, is(4));
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 8f170c3..657c4f3 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -25,6 +25,7 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
api(project(":sqlite:sqlite"))
+ implementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -2,12 +2,19 @@
package androidx.sqlite.db {
public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
- ctor public SimpleSQLiteQuery(String, Object![]?);
- ctor public SimpleSQLiteQuery(String);
- method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+ ctor public SimpleSQLiteQuery(String query);
+ method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public int argCount;
+ property public String sql;
+ field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+ }
+
+ public static final class SimpleSQLiteQuery.Companion {
+ method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
}
public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
}
public interface SupportSQLiteProgram extends java.io.Closeable {
- method public void bindBlob(int, byte[]);
- method public void bindDouble(int, double);
- method public void bindLong(int, long);
- method public void bindNull(int);
- method public void bindString(int, String);
+ method public void bindBlob(int index, byte[] value);
+ method public void bindDouble(int index, double value);
+ method public void bindLong(int index, long value);
+ method public void bindNull(int index);
+ method public void bindString(int index, String value);
method public void clearBindings();
}
public interface SupportSQLiteQuery {
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public abstract int argCount;
+ property public abstract String sql;
}
public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/api/public_plus_experimental_current.txt b/sqlite/sqlite/api/public_plus_experimental_current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/public_plus_experimental_current.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_current.txt
@@ -2,12 +2,19 @@
package androidx.sqlite.db {
public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
- ctor public SimpleSQLiteQuery(String, Object![]?);
- ctor public SimpleSQLiteQuery(String);
- method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+ ctor public SimpleSQLiteQuery(String query);
+ method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public int argCount;
+ property public String sql;
+ field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+ }
+
+ public static final class SimpleSQLiteQuery.Companion {
+ method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
}
public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
}
public interface SupportSQLiteProgram extends java.io.Closeable {
- method public void bindBlob(int, byte[]);
- method public void bindDouble(int, double);
- method public void bindLong(int, long);
- method public void bindNull(int);
- method public void bindString(int, String);
+ method public void bindBlob(int index, byte[] value);
+ method public void bindDouble(int index, double value);
+ method public void bindLong(int index, long value);
+ method public void bindNull(int index);
+ method public void bindString(int index, String value);
method public void clearBindings();
}
public interface SupportSQLiteQuery {
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public abstract int argCount;
+ property public abstract String sql;
}
public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -2,12 +2,19 @@
package androidx.sqlite.db {
public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
- ctor public SimpleSQLiteQuery(String, Object![]?);
- ctor public SimpleSQLiteQuery(String);
- method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+ ctor public SimpleSQLiteQuery(String query);
+ method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public int argCount;
+ property public String sql;
+ field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+ }
+
+ public static final class SimpleSQLiteQuery.Companion {
+ method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
}
public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
}
public interface SupportSQLiteProgram extends java.io.Closeable {
- method public void bindBlob(int, byte[]);
- method public void bindDouble(int, double);
- method public void bindLong(int, long);
- method public void bindNull(int);
- method public void bindString(int, String);
+ method public void bindBlob(int index, byte[] value);
+ method public void bindDouble(int index, double value);
+ method public void bindLong(int index, long value);
+ method public void bindNull(int index);
+ method public void bindString(int index, String value);
method public void clearBindings();
}
public interface SupportSQLiteQuery {
- method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public int getArgCount();
method public String getSql();
+ property public abstract int argCount;
+ property public abstract String sql;
}
public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 63ddf32..d3f6c60 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -19,10 +19,12 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("org.jetbrains.kotlin.android")
}
dependencies {
api("androidx.annotation:annotation:1.0.0")
+ implementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testImplementation(libs.mockitoCore)
}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
deleted file mode 100644
index b5e3ba7..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2017 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.sqlite.db;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
- * binds args based on the passed in Object type.
- */
-public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
- private final String mQuery;
- @Nullable
- private final Object[] mBindArgs;
-
- /**
- * Creates an SQL query with the sql string and the bind arguments.
- *
- * @param query The query string, can include bind arguments (.e.g ?).
- * @param bindArgs The bind argument value that will replace the placeholders in the query.
- */
- public SimpleSQLiteQuery(@NonNull String query, @Nullable Object[] bindArgs) {
- mQuery = query;
- mBindArgs = bindArgs;
- }
-
- /**
- * Creates an SQL query without any bind arguments.
- *
- * @param query The SQL query to execute. Cannot include bind parameters.
- */
- public SimpleSQLiteQuery(@NonNull String query) {
- this(query, null);
- }
-
- @NonNull
- @Override
- public String getSql() {
- return mQuery;
- }
-
- @Override
- public void bindTo(@NonNull SupportSQLiteProgram statement) {
- bind(statement, mBindArgs);
- }
-
- @Override
- public int getArgCount() {
- return mBindArgs == null ? 0 : mBindArgs.length;
- }
-
- /**
- * Binds the given arguments into the given sqlite statement.
- *
- * @param statement The sqlite statement
- * @param bindArgs The list of bind arguments
- */
- public static void bind(@NonNull SupportSQLiteProgram statement, @Nullable Object[] bindArgs) {
- if (bindArgs == null) {
- return;
- }
- final int limit = bindArgs.length;
- for (int i = 0; i < limit; i++) {
- final Object arg = bindArgs[i];
- bind(statement, i + 1, arg);
- }
- }
-
- private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
- // extracted from android.database.sqlite.SQLiteConnection
- if (arg == null) {
- statement.bindNull(index);
- } else if (arg instanceof byte[]) {
- statement.bindBlob(index, (byte[]) arg);
- } else if (arg instanceof Float) {
- statement.bindDouble(index, (Float) arg);
- } else if (arg instanceof Double) {
- statement.bindDouble(index, (Double) arg);
- } else if (arg instanceof Long) {
- statement.bindLong(index, (Long) arg);
- } else if (arg instanceof Integer) {
- statement.bindLong(index, (Integer) arg);
- } else if (arg instanceof Short) {
- statement.bindLong(index, (Short) arg);
- } else if (arg instanceof Byte) {
- statement.bindLong(index, (Byte) arg);
- } else if (arg instanceof String) {
- statement.bindString(index, (String) arg);
- } else if (arg instanceof Boolean) {
- statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
- } else {
- throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
- + " Supported types: null, byte[], float, double, long, int, short, byte,"
- + " string");
- }
- }
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
new file mode 100644
index 0000000..2e6a9d9
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 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.sqlite.db
+
+import android.annotation.SuppressLint
+
+/**
+ * A basic implementation of [SupportSQLiteQuery] which receives a query and its args and
+ * binds args based on the passed in Object type.
+ *
+ * @constructor Creates an SQL query with the sql string and the bind arguments.
+ *
+ * @param query The query string, can include bind arguments (.e.g ?).
+ * @param bindArgs The bind argument value that will replace the placeholders in the query.
+ */
+class SimpleSQLiteQuery(
+ private val query: String,
+ @Suppress("ArrayReturn") // Due to legacy API
+ private val bindArgs: Array<Any?>?
+ ) : SupportSQLiteQuery {
+
+ /**
+ * Creates an SQL query without any bind arguments.
+ *
+ * @param query The SQL query to execute. Cannot include bind parameters.
+ */
+ constructor(query: String) : this(query, null)
+
+ override val sql: String
+ get() = this.query
+
+ /**
+ * Creates an SQL query without any bind arguments.
+ *
+ * @param [statement] The SQL query to execute. Cannot include bind parameters.
+ */
+ override fun bindTo(statement: SupportSQLiteProgram) {
+ bind(statement, bindArgs)
+ }
+
+ override val argCount: Int
+ get() = bindArgs?.size ?: 0
+
+ companion object {
+ /**
+ * Binds the given arguments into the given sqlite statement.
+ *
+ * @param [statement] The sqlite statement
+ * @param [bindArgs] The list of bind arguments
+ */
+ @SuppressLint("SyntheticAccessor")
+ @JvmStatic
+ fun bind(
+ statement: SupportSQLiteProgram,
+ @Suppress("ArrayReturn") // Due to legacy API
+ bindArgs: Array<Any?>?
+ ) {
+ if (bindArgs == null) {
+ return
+ }
+
+ val limit = bindArgs.size
+ for (i in 0 until limit) {
+ val arg = bindArgs[i]
+ bind(statement, i + 1, arg)
+ }
+ }
+
+ private fun bind(statement: SupportSQLiteProgram, index: Int, arg: Any?) {
+ // extracted from android.database.sqlite.SQLiteConnection
+ if (arg == null) {
+ statement.bindNull(index)
+ } else if (arg is ByteArray) {
+ statement.bindBlob(index, arg)
+ } else if (arg is Float) {
+ statement.bindDouble(index, arg.toDouble())
+ } else if (arg is Double) {
+ statement.bindDouble(index, arg)
+ } else if (arg is Long) {
+ statement.bindLong(index, arg)
+ } else if (arg is Int) {
+ statement.bindLong(index, arg.toLong())
+ } else if (arg is Short) {
+ statement.bindLong(index, arg.toLong())
+ } else if (arg is Byte) {
+ statement.bindLong(index, arg.toLong())
+ } else if (arg is String) {
+ statement.bindString(index, arg)
+ } else if (arg is Boolean) {
+ statement.bindLong(index, if (arg) 1 else 0)
+ } else {
+ throw IllegalArgumentException(
+ "Cannot bind $arg at index $index Supported types: Null, ByteArray, " +
+ "Float, Double, Long, Int, Short, Byte, String"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java
deleted file mode 100644
index 286d449..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java
+++ /dev/null
@@ -1,321 +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.sqlite.db;
-
-import android.app.ActivityManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.OperationCanceledException;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import java.io.File;
-import java.util.List;
-
-/**
- * Helper for accessing features in {@link SupportSQLiteOpenHelper}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class SupportSQLiteCompat {
- private SupportSQLiteCompat() { }
- /**
- * Class for accessing functions that require SDK version 16 and higher.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(16)
- public static final class Api16Impl {
-
- /**
- * Cancels the operation and signals the cancellation listener. If the operation has not yet
- * started, then it will be canceled as soon as it does.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void cancel(@NonNull CancellationSignal cancellationSignal) {
- cancellationSignal.cancel();
- }
-
- /**
- * Creates a cancellation signal, initially not canceled.
- *
- * @return a new cancellation signal
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public static CancellationSignal createCancellationSignal() {
- return new CancellationSignal();
- }
-
- /**
- * Deletes a database including its journal file and other auxiliary files
- * that may have been created by the database engine.
- *
- * @param file The database file path.
- * @return True if the database was successfully deleted.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @SuppressWarnings("StreamFiles")
- public static boolean deleteDatabase(@NonNull File file) {
- return SQLiteDatabase.deleteDatabase(file);
- }
-
- /**
- * Runs the provided SQL and returns a cursor over the result set.
- *
- * @param sql the SQL query. The SQL string must not be ; terminated
- * @param selectionArgs You may include ?s in where clause in the query,
- * which will be replaced by the values from selectionArgs. The
- * values will be bound as Strings.
- * @param editTable the name of the first table, which is editable
- * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
- * If the operation is canceled, then {@link OperationCanceledException} will be thrown
- * when the query is executed.
- * @param cursorFactory the cursor factory to use, or null for the default factory
- * @return A {@link Cursor} object, which is positioned before the first entry. Note that
- * {@link Cursor}s are not synchronized, see the documentation for more details.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public static Cursor rawQueryWithFactory(@NonNull SQLiteDatabase sQLiteDatabase,
- @NonNull String sql, @NonNull String[] selectionArgs,
- @NonNull String editTable, @NonNull CancellationSignal cancellationSignal,
- @NonNull SQLiteDatabase.CursorFactory cursorFactory) {
- return sQLiteDatabase.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable,
- cancellationSignal);
- }
-
- /**
- * Sets whether foreign key constraints are enabled for the database.
- *
- * @param enable True to enable foreign key constraints, false to disable them.
- *
- * @throws IllegalStateException if the are transactions is in progress
- * when this method is called.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void setForeignKeyConstraintsEnabled(@NonNull SQLiteDatabase sQLiteDatabase,
- boolean enable) {
- sQLiteDatabase.setForeignKeyConstraintsEnabled(enable);
- }
-
- /**
- * This method disables the features enabled by
- * {@link SQLiteDatabase#enableWriteAheadLogging()}.
- *
- * @throws IllegalStateException if there are transactions in progress at the
- * time this method is called. WAL mode can only be changed when there are no
- * transactions in progress.
- *
- * @see SQLiteDatabase#enableWriteAheadLogging
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void disableWriteAheadLogging(@NonNull SQLiteDatabase sQLiteDatabase) {
- sQLiteDatabase.disableWriteAheadLogging();
- }
-
- /**
- * Returns true if write-ahead logging has been enabled for this database.
- *
- * @return True if write-ahead logging has been enabled for this database.
- *
- * @see SQLiteDatabase#enableWriteAheadLogging
- * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static boolean isWriteAheadLoggingEnabled(@NonNull SQLiteDatabase sQLiteDatabase) {
- return sQLiteDatabase.isWriteAheadLoggingEnabled();
- }
-
- /**
- * Sets {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag if {@code enabled} is {@code
- * true}, unsets otherwise.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void setWriteAheadLoggingEnabled(@NonNull SQLiteOpenHelper sQLiteOpenHelper,
- boolean enabled) {
- sQLiteOpenHelper.setWriteAheadLoggingEnabled(enabled);
- }
-
- private Api16Impl() {}
- }
-
- /**
- * Helper for accessing functions that require SDK version 19 and higher.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(19)
- public static final class Api19Impl {
- /**
- * Return the URI at which notifications of changes in this Cursor's data
- * will be delivered.
- *
- * @return Returns a URI that can be used with
- * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
- * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
- * data. May be null if no notification URI has been set.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public static Uri getNotificationUri(@NonNull Cursor cursor) {
- return cursor.getNotificationUri();
- }
-
-
- /**
- * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM
- * is ultimately up to the device configuration, but currently it generally means
- * something with 1GB or less of RAM. This is mostly intended to be used by apps
- * to determine whether they should turn off certain features that require more RAM.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static boolean isLowRamDevice(@NonNull ActivityManager activityManager) {
- return activityManager.isLowRamDevice();
- }
-
- private Api19Impl() {}
- }
-
- /**
- * Helper for accessing functions that require SDK version 21 and higher.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(21)
- public static final class Api21Impl {
-
- /**
- * Returns the absolute path to the directory on the filesystem.
- *
- * @return The path of the directory holding application files that will not
- * be automatically backed up to remote storage.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public static File getNoBackupFilesDir(@NonNull Context context) {
- return context.getNoBackupFilesDir();
- }
-
- private Api21Impl() {}
- }
-
- /**
- * Helper for accessing functions that require SDK version 23 and higher.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(23)
- public static final class Api23Impl {
-
- /**
- * Sets a {@link Bundle} that will be returned by {@link Cursor#getExtras()}.
- *
- * @param extras {@link Bundle} to set, or null to set an empty bundle.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void setExtras(@NonNull Cursor cursor, @NonNull Bundle extras) {
- cursor.setExtras(extras);
- }
-
- private Api23Impl() {}
- }
-
- /**
- * Helper for accessing functions that require SDK version 29 and higher.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @RequiresApi(29)
- public static final class Api29Impl {
-
- /**
- * Similar to {@link Cursor#setNotificationUri(ContentResolver, Uri)}, except this version
- * allows to watch multiple content URIs for changes.
- *
- * @param cr The content resolver from the caller's context. The listener attached to
- * this resolver will be notified.
- * @param uris The content URIs to watch.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void setNotificationUris(@NonNull Cursor cursor, @NonNull ContentResolver cr,
- @NonNull List<Uri> uris) {
- cursor.setNotificationUris(cr, uris);
- }
-
- /**
- * Return the URIs at which notifications of changes in this Cursor's data
- * will be delivered, as previously set by {@link #setNotificationUris}.
- *
- * @return Returns URIs that can be used with
- * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
- * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
- * data. May be null if no notification URI has been set.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public static List<Uri> getNotificationUris(@NonNull Cursor cursor) {
- return cursor.getNotificationUris();
- }
-
- private Api29Impl() {}
- }
-
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
new file mode 100644
index 0000000..c0d7fd8
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
@@ -0,0 +1,315 @@
+/*
+ * 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.sqlite.db
+
+import android.app.ActivityManager
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteDatabase.CursorFactory
+import android.database.sqlite.SQLiteOpenHelper
+import android.net.Uri
+import android.os.Bundle
+import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import java.io.File
+
+/**
+ * Helper for accessing features in [SupportSQLiteOpenHelper].
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SupportSQLiteCompat private constructor() {
+ /**
+ * Class for accessing functions that require SDK version 16 and higher.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(16)
+ object Api16Impl {
+ /**
+ * Cancels the operation and signals the cancellation listener. If the operation has not yet
+ * started, then it will be canceled as soon as it does.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun cancel(cancellationSignal: CancellationSignal) {
+ cancellationSignal.cancel()
+ }
+
+ /**
+ * Creates a cancellation signal, initially not canceled.
+ *
+ * @return a new cancellation signal
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun createCancellationSignal(): CancellationSignal {
+ return CancellationSignal()
+ }
+
+ /**
+ * Deletes a database including its journal file and other auxiliary files
+ * that may have been created by the database engine.
+ *
+ * @param file The database file path.
+ * @return True if the database was successfully deleted.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun deleteDatabase(file: File): Boolean {
+ return SQLiteDatabase.deleteDatabase(file)
+ }
+
+ /**
+ * Runs the provided SQL and returns a cursor over the result set.
+ *
+ * @param sql the SQL query. The SQL string must not be ; terminated
+ * @param selectionArgs You may include ?s in where clause in the query,
+ * which will be replaced by the values from selectionArgs. The
+ * values will be bound as Strings.
+ * @param editTable the name of the first table, which is editable
+ * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then [OperationCanceledException] will be thrown
+ * when the query is executed.
+ * @param cursorFactory the cursor factory to use, or null for the default factory
+ * @return A [Cursor] object, which is positioned before the first entry. Note that
+ * [Cursor]s are not synchronized, see the documentation for more details.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun rawQueryWithFactory(
+ sQLiteDatabase: SQLiteDatabase,
+ sql: String,
+ selectionArgs: Array<String?>,
+ editTable: String?,
+ cancellationSignal: CancellationSignal,
+ cursorFactory: CursorFactory
+ ): Cursor {
+ return sQLiteDatabase.rawQueryWithFactory(
+ cursorFactory, sql, selectionArgs, editTable,
+ cancellationSignal
+ )
+ }
+
+ /**
+ * Sets whether foreign key constraints are enabled for the database.
+ *
+ * @param enable True to enable foreign key constraints, false to disable them.
+ *
+ * @throws [IllegalStateException] if the are transactions is in progress
+ * when this method is called.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun setForeignKeyConstraintsEnabled(
+ sQLiteDatabase: SQLiteDatabase,
+ enable: Boolean
+ ) {
+ sQLiteDatabase.setForeignKeyConstraintsEnabled(enable)
+ }
+
+ /**
+ * This method disables the features enabled by
+ * [SQLiteDatabase.enableWriteAheadLogging].
+ *
+ * @throws - if there are transactions in progress at the
+ * time this method is called. WAL mode can only be changed when there are no
+ * transactions in progress.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun disableWriteAheadLogging(sQLiteDatabase: SQLiteDatabase) {
+ sQLiteDatabase.disableWriteAheadLogging()
+ }
+
+ /**
+ * Returns true if [SQLiteDatabase.enableWriteAheadLogging] logging has been enabled for
+ * this database.
+ *
+ * For details, see [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING].
+ *
+ * @return True if write-ahead logging has been enabled for this database.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun isWriteAheadLoggingEnabled(sQLiteDatabase: SQLiteDatabase): Boolean {
+ return sQLiteDatabase.isWriteAheadLoggingEnabled
+ }
+
+ /**
+ * Sets [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING] flag if `enabled` is `true`, unsets
+ * otherwise.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun setWriteAheadLoggingEnabled(
+ sQLiteOpenHelper: SQLiteOpenHelper,
+ enabled: Boolean
+ ) {
+ sQLiteOpenHelper.setWriteAheadLoggingEnabled(enabled)
+ }
+ }
+
+ /**
+ * Helper for accessing functions that require SDK version 19 and higher.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(19)
+ object Api19Impl {
+ /**
+ * Return the URI at which notifications of changes in this Cursor's data
+ * will be delivered.
+ *
+ * @return Returns a URI that can be used with [ContentResolver.registerContentObserver] to
+ * find out about changes to this Cursor's data. May be null if no notification URI has been
+ * set.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun getNotificationUri(cursor: Cursor): Uri {
+ return cursor.notificationUri
+ }
+
+ /**
+ * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM
+ * is ultimately up to the device configuration, but currently it generally means
+ * something with 1GB or less of RAM. This is mostly intended to be used by apps
+ * to determine whether they should turn off certain features that require more RAM.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun isLowRamDevice(activityManager: ActivityManager): Boolean {
+ return activityManager.isLowRamDevice
+ }
+ }
+
+ /**
+ * Helper for accessing functions that require SDK version 21 and higher.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(21)
+ object Api21Impl {
+ /**
+ * Returns the absolute path to the directory on the filesystem.
+ *
+ * @return The path of the directory holding application files that will not be
+ * automatically backed up to remote storage.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun getNoBackupFilesDir(context: Context): File {
+ return context.noBackupFilesDir
+ }
+ }
+
+ /**
+ * Helper for accessing functions that require SDK version 23 and higher.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(23)
+ object Api23Impl {
+ /**
+ * Sets a [Bundle] that will be returned by [Cursor.getExtras].
+ *
+ * @param extras [Bundle] to set, or null to set an empty bundle.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun setExtras(cursor: Cursor, extras: Bundle) {
+ cursor.extras = extras
+ }
+ }
+
+ /**
+ * Helper for accessing functions that require SDK version 29 and higher.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(29)
+ object Api29Impl {
+ /**
+ * Similar to [Cursor.setNotificationUri], except this version
+ * allows to watch multiple content URIs for changes.
+ *
+ * @param cr The content resolver from the caller's context. The listener attached to
+ * this resolver will be notified.
+ * @param uris The content URIs to watch.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun setNotificationUris(
+ cursor: Cursor,
+ cr: ContentResolver,
+ uris: List<Uri?>
+ ) {
+ cursor.setNotificationUris(cr, uris)
+ }
+
+ /**
+ * Return the URIs at which notifications of changes in this Cursor's data
+ * will be delivered, as previously set by [setNotificationUris].
+ *
+ * @return Returns URIs that can be used with [ContentResolver.registerContentObserver]
+ * to find out about changes to this Cursor's data. May be null if no notification URI has
+ * been set.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun getNotificationUris(cursor: Cursor): List<Uri> {
+ return cursor.notificationUris!!
+ }
+ }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
similarity index 69%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
index cd0a5ef..2397f94 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
@@ -13,65 +13,60 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package androidx.sqlite.db
-package androidx.sqlite.db;
-
-import androidx.annotation.NonNull;
-
-import java.io.Closeable;
+import java.io.Closeable
/**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}.
+ * An interface to map the behavior of [android.database.sqlite.SQLiteProgram].
*/
-
-@SuppressWarnings("unused")
-public interface SupportSQLiteProgram extends Closeable {
+interface SupportSQLiteProgram : Closeable {
/**
* Bind a NULL value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
+ * [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind null to
*/
- void bindNull(int index);
+ fun bindNull(index: Int)
/**
* Bind a long value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *addToBindArgs
+ * [clearBindings] is called.
+ * addToBindArgs
* @param index The 1-based index to the parameter to bind
* @param value The value to bind
*/
- void bindLong(int index, long value);
+ fun bindLong(index: Int, value: Long)
/**
* Bind a double value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
+ * [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind
*/
- void bindDouble(int index, double value);
+ fun bindDouble(index: Int, value: Double)
/**
* Bind a String value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
+ * [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind, must not be null
*/
- void bindString(int index, @NonNull String value);
+ fun bindString(index: Int, value: String)
/**
* Bind a byte array value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
+ * [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind, must not be null
*/
- void bindBlob(int index, @NonNull byte[] value);
+ fun bindBlob(index: Int, value: ByteArray)
/**
* Clears all existing bindings. Unset bindings are treated as NULL.
*/
- void clearBindings();
-}
+ fun clearBindings()
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
similarity index 67%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
index 82909fc..8108f2d 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
@@ -13,37 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.sqlite.db;
-
-import androidx.annotation.NonNull;
+package androidx.sqlite.db
/**
* A query with typed bindings. It is better to use this API instead of
- * {@link android.database.sqlite.SQLiteDatabase#rawQuery(String, String[])} because it allows
+ * [android.database.sqlite.SQLiteDatabase.rawQuery] because it allows
* binding type safe parameters.
*/
-public interface SupportSQLiteQuery {
+interface SupportSQLiteQuery {
/**
* The SQL query. This query can have placeholders(?) for bind arguments.
- *
- * @return The SQL query to compile
*/
- @NonNull
- String getSql();
+ val sql: String
/**
* Callback to bind the query parameters to the compiled statement.
*
* @param statement The compiled statement
*/
- void bindTo(@NonNull SupportSQLiteProgram statement);
+ fun bindTo(statement: SupportSQLiteProgram)
/**
- * Returns the number of arguments in this query. This is equal to the number of placeholders
+ * Is the number of arguments in this query. This is equal to the number of placeholders
* in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
- *
- * @return The number of arguments in the query.
*/
- int getArgCount();
-}
+ val argCount: Int
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
similarity index 66%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
index ed00b46..097ffb5 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
@@ -13,34 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.sqlite.db;
-
-import androidx.annotation.Nullable;
+package androidx.sqlite.db
/**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteStatement}.
+ * An interface to map the behavior of [android.database.sqlite.SQLiteStatement].
*/
-@SuppressWarnings("unused")
-public interface SupportSQLiteStatement extends SupportSQLiteProgram {
+interface SupportSQLiteStatement : SupportSQLiteProgram {
/**
* Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
* CREATE / DROP table, view, trigger, index etc.
*
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
+ * @throws [android.database.SQLException] If the SQL string is invalid for
+ * some reason
*/
- void execute();
+ fun execute()
/**
* Execute this SQL statement, if the the number of rows affected by execution of this SQL
* statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
*
* @return the number of rows affected by this SQL statement execution.
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
+ * @throws [android.database.SQLException] If the SQL string is invalid for
+ * some reason
*/
- int executeUpdateDelete();
+ fun executeUpdateDelete(): Int
/**
* Execute this SQL statement and return the ID of the row inserted due to this call.
@@ -48,10 +44,10 @@
*
* @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
*
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
+ * @throws [android.database.SQLException] If the SQL string is invalid for
+ * some reason
*/
- long executeInsert();
+ fun executeInsert(): Long
/**
* Execute a statement that returns a 1 by 1 table with a numeric value.
@@ -59,17 +55,17 @@
*
* @return The result of the query.
*
- * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ * @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
*/
- long simpleQueryForLong();
+ fun simpleQueryForLong(): Long
+
/**
* Execute a statement that returns a 1 by 1 table with a text value.
* For example, SELECT COUNT(*) FROM table;
*
* @return The result of the query.
*
- * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ * @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
*/
- @Nullable
- String simpleQueryForString();
-}
+ fun simpleQueryForString(): String?
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt
new file mode 100644
index 0000000..bf6edb4
--- /dev/null
+++ b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.sqlite.db
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+@RunWith(JUnit4::class)
+class SimpleSQLiteQueryTest {
+ @Test
+ fun sql() {
+ val query = SimpleSQLiteQuery("foo", emptyArray())
+ assertThat(query.sql, `is`("foo"))
+ }
+ @Test
+ fun bindTo_noArgs() {
+ val query = SimpleSQLiteQuery("foo", emptyArray())
+ val program: SupportSQLiteProgram = Mockito.mock(SupportSQLiteProgram::class.java)
+ query.bindTo(program)
+ verifyNoMoreInteractions(program)
+ }
+ @Test
+ fun bindTo_withArgs() {
+ val bytes = ByteArray(3)
+ val query = SimpleSQLiteQuery("foo", arrayOf("bar", 2, true, 0.5f, null, bytes))
+ val program: SupportSQLiteProgram = Mockito.mock(SupportSQLiteProgram::class.java)
+ query.bindTo(program)
+ verify(program).bindString(1, "bar")
+ verify(program).bindLong(2, 2)
+ verify(program).bindLong(3, 1)
+ verify(program).bindDouble(
+ 4,
+ (0.5f).toDouble()
+ )
+ verify(program).bindNull(5)
+ verify(program).bindBlob(6, bytes)
+ verifyNoMoreInteractions(program)
+ }
+ @Test
+ fun argCount_withArgs() {
+ val query = SimpleSQLiteQuery("foo", arrayOf("bar", 2, true))
+ assertThat(query.argCount, `is`(3))
+ }
+ @Test
+ fun argCount_noArgs() {
+ val query = SimpleSQLiteQuery("foo", emptyArray())
+ assertThat(query.argCount, `is`(0))
+ }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java
deleted file mode 100644
index 7b04a2b..0000000
--- a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.sqlite.db;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-@RunWith(JUnit4.class)
-public class SimpleSQLiteQueryTestTest {
-
- @Test
- public void getSql() {
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
- assertThat(query.getSql(), is("foo"));
- }
-
- @Test
- public void bindTo_noArgs() {
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
- SupportSQLiteProgram program = Mockito.mock(SupportSQLiteProgram.class);
- query.bindTo(program);
- verifyNoMoreInteractions(program);
- }
-
- @Test
- public void bindTo_withArgs() {
- byte[] bytes = new byte[3];
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo",
- new Object[]{"bar", 2, true, .5f, null, bytes});
- SupportSQLiteProgram program = Mockito.mock(SupportSQLiteProgram.class);
- query.bindTo(program);
- verify(program).bindString(1, "bar");
- verify(program).bindLong(2, 2);
- verify(program).bindLong(3, 1);
- verify(program).bindDouble(4, .5f);
- verify(program).bindNull(5);
- verify(program).bindBlob(6, bytes);
- verifyNoMoreInteractions(program);
- }
-
- @Test
- public void getArgCount_withArgs() {
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo",
- new Object[]{"bar", 2, true});
- assertThat(query.getArgCount(), is(3));
- }
-
- @Test
- public void getArgCount_noArgs() {
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
- assertThat(query.getArgCount(), is(0));
- }
-}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
index da46fd6..fa4f09d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
@@ -494,52 +494,52 @@
@Test
public void testPinchClose() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
pinchArea.pinchClose(1f);
scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
- float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
}
@Test
public void testPinchClose_withSpeed() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
pinchArea.pinchClose(.75f, SPEED_MS);
scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
- float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
}
@Test
public void testPinchOpen() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
pinchArea.pinchOpen(.5f);
scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
- float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
}
@Test
public void testPinchOpen_withSpeed() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
pinchArea.pinchOpen(.25f, SPEED_MS);
scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
- float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
}
@@ -683,7 +683,7 @@
@Test
public void testSetGestureMargin() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
@@ -711,7 +711,7 @@
@Test
public void testSetGestureMargins() {
- launchTestActivity(UiObject2TestPinchActivity.class);
+ launchTestActivity(PinchTestActivity.class);
UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index 9b898d5..7d76076 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -625,12 +625,61 @@
assertFalse(text3.exists());
}
+ @Test
+ public void testPinchOut() throws Exception {
+ launchTestActivity(PinchTestActivity.class);
+
+ UiObject pinchArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/pinch_area"));
+ UiObject scaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/scale_factor"));
+
+ UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/scale_factor").text("1.0f"));
+
+ assertTrue(pinchArea.pinchOut(100, 10));
+ assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
+ assertTrue(String.format(
+ "Expected scale text to be greater than 1f after pinchOut(), but got [%f]",
+ scaleValueAfterPinch), scaleValueAfterPinch > 1f);
+ }
+
+ @Test
+ public void testPinchIn() throws Exception {
+ launchTestActivity(PinchTestActivity.class);
+
+ UiObject pinchArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/pinch_area"));
+ UiObject scaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/scale_factor"));
+
+ UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/scale_factor").text("1.0f"));
+
+ assertTrue(pinchArea.pinchIn(100, 10));
+ assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
+ float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
+ assertTrue(String.format("Expected scale value to be less than 1f after pinchIn(), "
+ + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
+ }
+
+ @Test
+ public void testPinchFamily_throwsExceptions() {
+ launchTestActivity(PinchTestActivity.class);
+
+ UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
+ UiObject smallArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ + "/small_area"));
+
+ assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchOut(100, 10));
+ assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchIn(100, 10));
+ assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
+ assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
+ }
+
/* TODO(b/241158642): Implement these tests, and the tests for exceptions of each tested method.
- public void testPinchOut() {}
-
- public void testPinchIn() {}
-
public void testPerformTwoPointerGesture() {}
public void testPerformMultiPointerGesture() {}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
index 521b327..2e3d6d6 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -127,6 +127,13 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
+ <activity android:name=".PinchTestActivity"
+ android:exported="true"
+ android:theme="@android:style/Theme.Holo.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
<activity android:name=".SplitScreenTestActivity"
android:exported="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@@ -157,13 +164,6 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
- <activity android:name=".UiObject2TestPinchActivity"
- android:exported="true"
- android:theme="@android:style/Theme.Holo.NoActionBar">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- </intent-filter>
- </activity>
<activity android:name=".UntilTestActivity"
android:exported="true"
android:theme="@android:style/Theme.Holo.NoActionBar">
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java
new file mode 100644
index 0000000..3c30df3
--- /dev/null
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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.test.uiautomator.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+public class PinchTestActivity extends Activity {
+
+ private ScaleGestureDetector mScaleDetector;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.pinch_test_activity);
+
+ final TextView scaleFactor = findViewById(R.id.scale_factor);
+
+ mScaleDetector = new ScaleGestureDetector(this, new SimpleOnScaleGestureListener() {
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ float scale = detector.getScaleFactor();
+ scaleFactor.setText(Float.toString(scale));
+ }
+ });
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return mScaleDetector.onTouchEvent(event);
+ }
+}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java
deleted file mode 100644
index d205048..0000000
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2014 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.test.uiautomator.testapp;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-public class UiObject2TestPinchActivity extends Activity {
-
- private ScaleGestureDetector mScaleDetector;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.uiobject2_testpinch_activity);
-
- final TextView scaleFactor = (TextView)findViewById(R.id.scale_factor);
-
- mScaleDetector = new ScaleGestureDetector(this, new SimpleOnScaleGestureListener() {
- @Override
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- float scale = detector.getScaleFactor();
- Log.d("FOO", String.format("Beginning scale: %s", scale));
- float span = detector.getCurrentSpan();
- Log.d("FOO", String.format("Beginning span: %s", span));
- Log.d("FOO", String.format("Beginning span, X: %s, Y:%s", detector.getCurrentSpanX(), detector.getCurrentSpanY()));
- Log.d("FOO", String.format("Beginning focus: %s, %s", detector.getFocusX(), detector.getFocusY()));
- return true;
-
- }
-
- @Override
- public void onScaleEnd(ScaleGestureDetector detector) {
- float scale = detector.getScaleFactor();
- Log.d("FOO", String.format("Ending scale: %s", scale));
- float span = detector.getCurrentSpan();
- Log.d("FOO", String.format("Ending span: %s", span));
- Log.d("FOO", String.format("Ending span, X: %s, Y:%s", detector.getCurrentSpanX(), detector.getCurrentSpanY()));
- Log.d("FOO", String.format("Ending focus: %s, %s", detector.getFocusX(), detector.getFocusY()));
- scaleFactor.setText(Float.toString(scale));
- }
- });
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return mScaleDetector.onTouchEvent(event);
- }
-}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
similarity index 86%
rename from test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml
rename to test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
index 5f1ecbd..f952013 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
@@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- tools:context=".UiObject2TestPinchActivity">
+ tools:context=".PinchTestActivity">
<LinearLayout
android:id="@+id/pinch_area"
@@ -35,6 +35,12 @@
android:text="1.0f"
android:textColor="@android:color/white" />
+ <TextView
+ android:id="@+id/small_area"
+ android:layout_width="1dp"
+ android:layout_height="1dp"
+ android:text="small area" />
+
</LinearLayout>
</LinearLayout>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
index 7f8f775..5c82419 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
@@ -22,53 +22,58 @@
import android.view.accessibility.AccessibilityWindowInfo;
import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-/**
- * This class contains static helper methods to work with
- * {@link AccessibilityNodeInfo}
- */
+/** Static helper methods for working with {@link AccessibilityNodeInfo}s. */
class AccessibilityNodeInfoHelper {
private AccessibilityNodeInfoHelper() {}
/**
- * Returns the node's bounds clipped to the size of the display
+ * Returns the visible bounds of an {@link AccessibilityNodeInfo}.
*
- * @param node
- * @param width pixel width of the display
- * @param height pixel height of the display
- * @return null if node is null, else a Rect containing visible bounds
+ * @param node node to analyze
+ * @param width display width in pixels
+ * @param height display height in pixels
+ * @return {@link Rect} containing the visible bounds
*/
- @SuppressWarnings("RectIntersectReturnValueIgnored")
- static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
- if (node == null) {
- return null;
- }
- // targeted node's bounds
- Rect nodeRect = new Rect();
- node.getBoundsInScreen(nodeRect);
+ @NonNull
+ static Rect getVisibleBoundsInScreen(@NonNull AccessibilityNodeInfo node, int width,
+ int height) {
+ Rect nodeBounds = new Rect();
+ node.getBoundsInScreen(nodeBounds);
- Rect displayRect = new Rect();
- displayRect.top = 0;
- displayRect.left = 0;
- displayRect.right = width;
- displayRect.bottom = height;
+ // Trim portions that are outside the specified display bounds.
+ Rect displayBounds = new Rect(0, 0, width, height);
+ nodeBounds = intersect(nodeBounds, displayBounds);
- nodeRect.intersect(displayRect);
-
- // On platforms that give us access to the node's window
+ // Trim portions that are outside the window bounds on API 21+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- // Trim any portion of the bounds that are outside the window
- Rect bounds = new Rect();
+ Rect windowBounds = new Rect();
AccessibilityWindowInfo window = Api21Impl.getWindow(node);
if (window != null) {
- Api21Impl.getBoundsInScreen(window, bounds);
- nodeRect.intersect(bounds);
+ Api21Impl.getBoundsInScreen(window, windowBounds);
+ nodeBounds = intersect(nodeBounds, windowBounds);
}
}
- return nodeRect;
+ // Trim portions that are outside the first scrollable ancestor.
+ for (AccessibilityNodeInfo ancestor = node.getParent(); ancestor != null;
+ ancestor = ancestor.getParent()) {
+ if (ancestor.isScrollable()) {
+ Rect ancestorBounds = getVisibleBoundsInScreen(ancestor, width, height);
+ nodeBounds = intersect(nodeBounds, ancestorBounds);
+ break;
+ }
+ }
+
+ return nodeBounds;
+ }
+
+ /** Returns the intersection of two rectangles, or an empty rectangle if they do not overlap. */
+ private static Rect intersect(Rect first, Rect second) {
+ return first.intersect(second) ? first : new Rect();
}
@RequiresApi(21)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 22c1a0f..0db19ba 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -26,6 +26,8 @@
import android.view.MotionEvent.PointerCoords;
import android.view.accessibility.AccessibilityNodeInfo;
+import androidx.annotation.NonNull;
+
/**
* A UiObject is a representation of a view. It is not in any way directly bound to a
* view as an object reference. A UiObject contains information to help it
@@ -350,56 +352,12 @@
rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
}
- /**
- * Finds the visible bounds of a partially visible UI element
- *
- * @param node
- * @return null if node is null, else a Rect containing visible bounds
- */
- @SuppressWarnings("RectIntersectReturnValueIgnored")
- private Rect getVisibleBounds(AccessibilityNodeInfo node) {
- if (node == null) {
- return null;
- }
-
- // targeted node's bounds
+ /** Returns the visible bounds of an {@link AccessibilityNodeInfo}. */
+ @NonNull
+ private Rect getVisibleBounds(@NonNull AccessibilityNodeInfo node) {
int w = getDevice().getDisplayWidth();
int h = getDevice().getDisplayHeight();
- Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
-
- // is the targeted node within a scrollable container?
- AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node);
- if(scrollableParentNode == null) {
- // nothing to adjust for so return the node's Rect as is
- return nodeRect;
- }
-
- // Scrollable parent's visible bounds
- Rect parentRect = AccessibilityNodeInfoHelper
- .getVisibleBoundsInScreen(scrollableParentNode, w, h);
- // adjust for partial clipping of targeted by parent node if required
- nodeRect.intersect(parentRect);
- return nodeRect;
- }
-
- /**
- * Walks up the layout hierarchy to find a scrollable parent. A scrollable parent
- * indicates that this node might be in a container where it is partially
- * visible due to scrolling. In this case, its clickable center might not be visible and
- * the click coordinates should be adjusted.
- *
- * @param node
- * @return The accessibility node info.
- */
- private AccessibilityNodeInfo getScrollableParent(AccessibilityNodeInfo node) {
- AccessibilityNodeInfo parent = node;
- while(parent != null) {
- parent = parent.getParent();
- if (parent != null && parent.isScrollable()) {
- return parent;
- }
- }
- return null;
+ return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
}
/**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 19ede49..75445bf 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -32,6 +32,7 @@
import android.view.accessibility.AccessibilityWindowInfo;
import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
@@ -269,57 +270,21 @@
return false;
}
- /** Returns the visible bounds of {@code node} in screen coordinates. */
- @SuppressWarnings("RectIntersectReturnValueIgnored")
- private Rect getVisibleBounds(AccessibilityNodeInfo node) {
- // Get the object bounds in screen coordinates
- Rect ret = new Rect();
- node.getBoundsInScreen(ret);
-
- // Trim any portion of the bounds that are not on the screen
- final int displayId = getDisplayId();
- if (displayId == Display.DEFAULT_DISPLAY) {
- final Rect screen =
- new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
- ret.intersect(screen);
- } else {
- final DisplayManager dm =
- (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
- Service.DISPLAY_SERVICE);
- final Display display = dm.getDisplay(getDisplayId());
- if (display != null) {
- final Point size = new Point();
- display.getRealSize(size);
- final Rect screen = new Rect(0, 0, size.x, size.y);
- ret.intersect(screen);
- }
+ /** Returns the visible bounds of an {@link AccessibilityNodeInfo}. */
+ @NonNull
+ private Rect getVisibleBounds(@NonNull AccessibilityNodeInfo node) {
+ DisplayManager displayManager =
+ (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
+ Service.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(getDisplayId());
+ if (display != null) {
+ Point displaySize = new Point();
+ display.getRealSize(displaySize);
+ return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+ node, displaySize.x, displaySize.y);
}
-
- // On platforms that give us access to the node's window
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- // Trim any portion of the bounds that are outside the window
- Rect bounds = new Rect();
- AccessibilityWindowInfo window = Api21Impl.getWindow(node);
- if (window != null) {
- Api21Impl.getBoundsInScreen(window, bounds);
- ret.intersect(bounds);
- }
- }
-
- // Find the visible bounds of our first scrollable ancestor
- AccessibilityNodeInfo ancestor = null;
- for (ancestor = node.getParent(); ancestor != null; ancestor = ancestor.getParent()) {
- // If this ancestor is scrollable
- if (ancestor.isScrollable()) {
- // Trim any portion of the bounds that are hidden by the non-visible portion of our
- // ancestor
- Rect ancestorRect = getVisibleBounds(ancestor);
- ret.intersect(ancestorRect);
- break;
- }
- }
-
- return ret;
+ return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+ node, Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/** Returns a point in the center of the visible bounds of this object. */
@@ -796,12 +761,6 @@
static AccessibilityWindowInfo getWindow(AccessibilityNodeInfo accessibilityNodeInfo) {
return accessibilityNodeInfo.getWindow();
}
-
- @DoNotInline
- static void getBoundsInScreen(AccessibilityWindowInfo accessibilityWindowInfo,
- Rect outBounds) {
- accessibilityWindowInfo.getBoundsInScreen(outBounds);
- }
}
@RequiresApi(30)
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
index e2c9747..53d17d4 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
@@ -104,7 +104,7 @@
* Returns whether fallbackLineSpacing is enabled for the given layout.
*
* @param layout StaticLayout instance
- * @param useFallbackLineSpacing fallbackLineSpacing canfiguration passed while creating the
+ * @param useFallbackLineSpacing fallbackLineSpacing configuration passed while creating the
* StaticLayout.
*/
fun isFallbackLineSpacingEnabled(
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
index c389acf..0ebd08a 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
@@ -34,6 +34,7 @@
private val component1 = ComponentName("a.b.c", "a.b.c.TestActivity")
private val component2 = ComponentName("d.e.f", "d.e.f.TestActivity")
private val wildcard = ComponentName("*", "*")
+ private val classWildcard = ComponentName("a.b.c", "*")
private val intent = Intent()
private val activity = mock<Activity> {
on { intent } doReturn intent
@@ -94,4 +95,15 @@
"of null component")
.that(filter.matchesActivity(activity)).isTrue()
}
+
+ @Test
+ fun testMatchActivity_MatchIntentWithPackage() {
+ val filter = ActivityFilter(classWildcard, null /* intentAction */)
+
+ intent.component = null
+ intent.`package` = classWildcard.packageName
+
+ assertWithMessage("#matchActivity must be true because intent.package matches")
+ .that(filter.matchesActivity(activity)).isTrue()
+ }
}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
new file mode 100644
index 0000000..0ac37b0
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.window.embedding
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Intent
+import androidx.window.core.ExperimentalWindowApi
+import com.google.common.truth.Truth.assertWithMessage
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.mock
+import org.junit.Test
+
+@OptIn(ExperimentalWindowApi::class)
+class SplitPairFilterTest {
+ private val component1 = ComponentName("a.b.c", "a.b.c.TestActivity")
+ private val component2 = ComponentName("d.e.f", "d.e.f.TestActivity")
+ private val intentClassWildcard = ComponentName("d.e.f", "*")
+ private val intent = Intent()
+ private val activity = mock<Activity> {
+ on { intent } doReturn intent
+ on { componentName } doReturn component1
+ }
+ private val filter =
+ SplitPairFilter(component1, intentClassWildcard, null /* secondaryActivityIntentAction */)
+
+ @Test
+ fun testMatchActivityIntentPair_MatchIntentComponent() {
+ assertWithMessage("#matchesActivityIntentPair must be false because intent is empty")
+ .that(filter.matchesActivityIntentPair(activity, intent)).isFalse()
+
+ intent.component = component2
+
+ assertWithMessage("#matchesActivityIntentPair must be true because intent.component" +
+ " matches")
+ .that(filter.matchesActivityIntentPair(activity, intent)).isTrue()
+ }
+
+ @Test
+ fun testMatchActivityIntentPair_MatchIntentPackage() {
+ intent.`package` = intentClassWildcard.packageName
+
+ assertWithMessage("#matchesActivityIntentPair must be true because intent.package matches")
+ .that(filter.matchesActivityIntentPair(activity, intent)).isTrue()
+
+ intent.component = component1
+
+ assertWithMessage("#matchesActivityIntentPair must be false because intent.component" +
+ " doesn't match")
+ .that(filter.matchesActivityIntentPair(activity, intent)).isFalse()
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index 06ff616..fd3d828 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -72,7 +72,7 @@
fun matchesIntent(intent: Intent): Boolean {
val match =
- if (!MatcherUtils.areComponentsMatching(intent.component, componentName)) {
+ if (!MatcherUtils.isIntentMatching(intent, componentName)) {
false
} else {
intentAction == null || intentAction == intent.action
@@ -89,7 +89,7 @@
fun matchesActivity(activity: Activity): Boolean {
val match =
- MatcherUtils.areActivityOrIntentComponentsMatching(activity, componentName) &&
+ MatcherUtils.isActivityOrIntentMatching(activity, componentName) &&
(intentAction == null || intentAction == activity.intent?.action)
if (sDebugMatchers) {
val matchString = if (match) "MATCH" else "NO MATCH"
diff --git a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
index 712c94f..2574912 100644
--- a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
+++ b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
@@ -17,6 +17,7 @@
import android.app.Activity
import android.content.ComponentName
+import android.content.Intent
import android.util.Log
import androidx.window.core.ExperimentalWindowApi
@@ -54,20 +55,36 @@
}
/**
- * Returns `true` if [Activity.getComponentName] match or
- * [Component][android.content.Intent.getComponent] of [Activity.getIntent] match allowing
+ * Returns `true` if [Activity.getComponentName] match or [Activity.getIntent] match allowing
* wildcard patterns.
*/
- internal fun areActivityOrIntentComponentsMatching(
+ internal fun isActivityOrIntentMatching(
activity: Activity,
ruleComponent: ComponentName
): Boolean {
if (areComponentsMatching(activity.componentName, ruleComponent)) {
return true
}
- // Returns false if activity's intent doesn't exist or its intent's Component doesn't match.
- return activity.intent?.component ?.let {
- component -> areComponentsMatching(component, ruleComponent) } ?: false
+ // Returns false if activity's intent doesn't exist or its intent doesn't match.
+ return activity.intent ?.let { intent -> isIntentMatching(intent, ruleComponent) } ?: false
+ }
+
+ /**
+ * Returns `true` if [Intent.getComponent] match or [Intent.getPackage] match allowing wildcard
+ * patterns.
+ */
+ internal fun isIntentMatching(
+ intent: Intent,
+ ruleComponent: ComponentName
+ ): Boolean {
+ if (intent.component != null) {
+ // Compare the component if set.
+ return areComponentsMatching(intent.component, ruleComponent)
+ }
+ // Check if there is wildcard match for Intent that only specifies the packageName.
+ val packageName = intent.`package` ?: return false
+ return (packageName == ruleComponent.packageName ||
+ wildcardMatch(packageName, ruleComponent.packageName)) && ruleComponent.className == "*"
}
/**
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index c633a0e..f13ddd5 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -21,6 +21,7 @@
import android.util.Log
import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.MatcherUtils.areComponentsMatching
+import androidx.window.embedding.MatcherUtils.isIntentMatching
import androidx.window.embedding.MatcherUtils.sDebugMatchers
import androidx.window.embedding.MatcherUtils.sMatchersTag
@@ -85,7 +86,7 @@
) {
false
} else if (
- !areComponentsMatching(secondaryActivityIntent.component, secondaryActivityName)
+ !isIntentMatching(secondaryActivityIntent, secondaryActivityName)
) {
false
} else {