Merge "Assert on executor being idle in WriteAheadLoggingTest" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 1b5b012..55e0c22 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -144,6 +144,13 @@
*/
const val KMP_ENABLE_LINUX = "androidx.kmp.linux.enabled"
+/**
+ * If true, include all native targets when building KMP.
+ * Replaces KMP_ENABLE_MAC and KMP_ENABLE_LINUX in collections, and will eventually be
+ * consolidated into the AndroidX plugin.
+ */
+const val KMP_ENABLE_NATIVE = "androidx.kmp.native.enabled"
+
val ALL_ANDROIDX_PROPERTIES = setOf(
ALL_WARNINGS_AS_ERRORS,
ALTERNATIVE_PROJECT_URL,
@@ -171,7 +178,8 @@
KMP_GITHUB_BUILD,
KMP_ENABLE_MAC,
KMP_ENABLE_JS,
- KMP_ENABLE_LINUX
+ KMP_ENABLE_LINUX,
+ KMP_ENABLE_NATIVE
)
/**
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 602b86b..a35c78f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -27,31 +27,40 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.media.ImageReader
+import android.media.ImageWriter
import android.os.Build
import android.os.Handler
import android.os.Looper
+import android.os.SystemClock
import android.util.Size
import android.view.Surface
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.internal.compat.CameraManagerCompat
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.internal.util.RequestProcessorRequest
import androidx.camera.camera2.interop.CaptureRequestOptions
+import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
import androidx.camera.core.impl.CameraCaptureResult
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.OutputSurface
+import androidx.camera.core.impl.RequestProcessor
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SessionProcessorSurface
import androidx.camera.core.impl.TagBundle
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.CameraDeviceHolder
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.fakes.FakeSessionProcessor
import androidx.concurrent.futures.await
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -70,6 +79,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -78,6 +88,7 @@
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+const val FAKE_CAPTURE_SEQUENCE_ID = 1
const val JPEG_ORIENTATION_VALUE = 90
const val JPEG_QUALITY_VALUE: Byte = 50
@@ -94,9 +105,9 @@
@SdkSuppress(minSdkVersion = 23)
class ProcessingCaptureSessionTest(
private var lensFacing: Int,
- // The pair specifies (Output image format to Input image format). SessionProcessor will
- // create the surface even if input format is the same as output format. But if the
- // output format is null, it means no conversion and original surface is used directly.
+ // The pair specifies (Origin image format to Transformed format), SessionProcessor will
+ // create the surface even if transformed format is the same as origin format but if the
+ // transformed format is null, it means no conversion and original surface is used directly.
private var previewFormatConvert: Pair<Int, Int?>,
private var captureFormatConvert: Pair<Int, Int?>,
) {
@@ -133,7 +144,7 @@
private lateinit var cameraDeviceHolder: CameraDeviceHolder
private lateinit var captureSessionRepository: CaptureSessionRepository
private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSessionOpener.Builder
- private lateinit var sessionProcessor: FakeSessionProcessor
+ private lateinit var sessionProcessor: CustomSessionProcessor
private lateinit var executor: Executor
private lateinit var handler: Handler
private lateinit var sessionConfigParameters: SessionConfigParameters
@@ -151,10 +162,7 @@
handler = Handler(Looper.getMainLooper())
executor = CameraXExecutors.newHandlerExecutor(handler)
- sessionProcessor = FakeSessionProcessor(
- inputFormatPreview = previewFormatConvert.second,
- inputFormatCapture = captureFormatConvert.second
- )
+ sessionProcessor = CustomSessionProcessor()
val cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing)!!
camera2CameraInfo = Camera2CameraInfoImpl(cameraId, cameraManagerCompat)
@@ -815,7 +823,7 @@
)
captureOutputDeferrableSurface = ImmediateSurface(
captureImageReader.surface, Size(640, 480),
- captureFormat
+ JPEG
)
captureOutputDeferrableSurface.setContainerClass(ImageCapture::class.java)
captureOutputDeferrableSurface.terminationFuture.addListener(
@@ -925,4 +933,297 @@
closeOutputSurfaces()
}
}
+
+ inner class CustomSessionProcessor : SessionProcessor {
+ private lateinit var previewProcessorSurface: DeferrableSurface
+ private lateinit var captureProcessorSurface: DeferrableSurface
+ private var intermediaPreviewImageReader: ImageReader? = null
+ private var intermediaCaptureImageReader: ImageReader? = null
+ private var intermediaPreviewImageWriter: ImageWriter? = null
+ private var intermediaCaptureImageWriter: ImageWriter? = null
+
+ private val previewOutputConfigId = 1
+ private val captureOutputConfigId = 2
+
+ private var requestProcessor: RequestProcessor? = null
+
+ // Values of these Deferred are the timestamp to complete.
+ private val initSessionCalled = CompletableDeferred<Long>()
+ private val deInitSessionCalled = CompletableDeferred<Long>()
+ private val onCaptureSessionStartCalled = CompletableDeferred<Long>()
+ private val onCaptureSessionEndCalled = CompletableDeferred<Long>()
+ private val startRepeatingCalled = CompletableDeferred<Long>()
+ private val startCaptureCalled = CompletableDeferred<Long>()
+ private val setParametersCalled = CompletableDeferred<Config>()
+ private var latestParameters: Config = OptionsBundle.emptyBundle()
+ private var blockRunAfterInitSession: () -> Unit = {}
+
+ fun releaseSurfaces() {
+ intermediaPreviewImageReader?.close()
+ intermediaCaptureImageReader?.close()
+ }
+
+ fun runAfterInitSession(block: () -> Unit) {
+ blockRunAfterInitSession = block
+ }
+
+ override fun initSession(
+ cameraInfo: CameraInfo,
+ previewSurfaceConfig: OutputSurface,
+ imageCaptureSurfaceConfig: OutputSurface,
+ imageAnalysisSurfaceConfig: OutputSurface?
+ ): SessionConfig {
+ initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
+ val handler = Handler(Looper.getMainLooper())
+
+ var sessionBuilder = SessionConfig.Builder()
+
+ // Preview
+ lateinit var previewTransformedSurface: Surface
+ if (previewFormatConvert.second == null) { // no conversion, use origin surface.
+ previewTransformedSurface = previewSurfaceConfig.surface
+ } else {
+ intermediaPreviewImageReader = ImageReader.newInstance(
+ 640, 480,
+ previewFormatConvert.second!!, 2
+ )
+ previewTransformedSurface = intermediaPreviewImageReader!!.surface
+
+ intermediaPreviewImageWriter = ImageWriter.newInstance(
+ previewSurfaceConfig.surface, 2
+ )
+
+ intermediaPreviewImageReader!!.setOnImageAvailableListener(
+ {
+ it.acquireNextImage().use {
+ val imageDequeued = intermediaPreviewImageWriter!!.dequeueInputImage()
+ intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
+ }
+ },
+ handler
+ )
+ }
+ previewProcessorSurface =
+ SessionProcessorSurface(previewTransformedSurface, previewOutputConfigId)
+ previewProcessorSurface.terminationFuture.addListener(
+ {
+ intermediaPreviewImageReader?.close()
+ intermediaPreviewImageWriter?.close()
+ },
+ CameraXExecutors.directExecutor()
+ )
+ sessionBuilder.addSurface(previewProcessorSurface)
+
+ // Capture
+ lateinit var captureTransformedSurface: Surface
+ if (captureFormatConvert.second == null) { // no conversion, use origin surface.
+ captureTransformedSurface = imageCaptureSurfaceConfig.surface
+ } else {
+ intermediaCaptureImageReader = ImageReader.newInstance(
+ 640, 480,
+ captureFormatConvert.second!!, 2
+ )
+ captureTransformedSurface = intermediaCaptureImageReader!!.surface
+
+ intermediaCaptureImageWriter = ImageWriter.newInstance(
+ imageCaptureSurfaceConfig.surface, 2
+ )
+
+ intermediaCaptureImageReader!!.setOnImageAvailableListener(
+ {
+ it.acquireNextImage().use {
+ val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
+ intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
+ }
+ },
+ handler
+ )
+ }
+ captureProcessorSurface =
+ SessionProcessorSurface(captureTransformedSurface, captureOutputConfigId)
+
+ captureProcessorSurface.terminationFuture.addListener(
+ {
+ intermediaCaptureImageReader?.close()
+ intermediaCaptureImageWriter?.close()
+ },
+ CameraXExecutors.directExecutor()
+ )
+ sessionBuilder.addSurface(captureProcessorSurface)
+
+ sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
+ val sessionConfig = sessionBuilder.build()
+ blockRunAfterInitSession()
+ return sessionConfig
+ }
+
+ override fun deInitSession() {
+ deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
+ previewProcessorSurface.close()
+ captureProcessorSurface.close()
+ }
+
+ override fun setParameters(config: Config) {
+ setParametersCalled.complete(config)
+ latestParameters = config
+ }
+
+ override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
+ onCaptureSessionStartCalled.complete(SystemClock.elapsedRealtimeNanos())
+ requestProcessor = _requestProcessor
+ }
+
+ override fun onCaptureSessionEnd() {
+ onCaptureSessionEndCalled.complete(SystemClock.elapsedRealtimeNanos())
+ }
+
+ fun getLatestParameters(): Config {
+ return latestParameters
+ }
+
+ override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
+ startRepeatingCalled.complete(SystemClock.elapsedRealtimeNanos())
+ val builder = RequestProcessorRequest.Builder().apply {
+ addTargetOutputConfigId(previewOutputConfigId)
+ setParameters(latestParameters)
+ setTemplateId(CameraDevice.TEMPLATE_PREVIEW)
+ }
+
+ requestProcessor!!.setRepeating(
+ builder.build(),
+ object : RequestProcessor.Callback {
+ override fun onCaptureStarted(
+ request: RequestProcessor.Request,
+ frameNumber: Long,
+ timestamp: Long
+ ) {}
+
+ override fun onCaptureProgressed(
+ request: RequestProcessor.Request,
+ captureResult: CameraCaptureResult
+ ) {}
+
+ override fun onCaptureCompleted(
+ request: RequestProcessor.Request,
+ captureResult: CameraCaptureResult
+ ) {
+ callback.onCaptureSequenceCompleted(1)
+ }
+
+ override fun onCaptureFailed(
+ request: RequestProcessor.Request,
+ captureFailure: CameraCaptureFailure
+ ) {}
+
+ override fun onCaptureBufferLost(
+ request: RequestProcessor.Request,
+ frameNumber: Long,
+ outputConfigId: Int
+ ) {}
+
+ override fun onCaptureSequenceCompleted(
+ sequenceId: Int,
+ frameNumber: Long
+ ) {}
+
+ override fun onCaptureSequenceAborted(sequenceId: Int) {
+ }
+ }
+ )
+ return FAKE_CAPTURE_SEQUENCE_ID
+ }
+
+ override fun stopRepeating() {
+ }
+
+ override fun startCapture(callback: SessionProcessor.CaptureCallback): Int {
+ startCaptureCalled.complete(SystemClock.elapsedRealtimeNanos())
+ val request = RequestProcessorRequest.Builder().apply {
+ addTargetOutputConfigId(captureOutputConfigId)
+ setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ }.build()
+
+ requestProcessor!!.submit(
+ request,
+ object : RequestProcessor.Callback {
+ override fun onCaptureCompleted(
+ request: RequestProcessor.Request,
+ captureResult: CameraCaptureResult
+ ) {
+ callback.onCaptureSequenceCompleted(1)
+ }
+
+ override fun onCaptureStarted(
+ request: RequestProcessor.Request,
+ frameNumber: Long,
+ timestamp: Long
+ ) {}
+
+ override fun onCaptureProgressed(
+ request: RequestProcessor.Request,
+ captureResult: CameraCaptureResult
+ ) {}
+
+ override fun onCaptureFailed(
+ request: RequestProcessor.Request,
+ captureFailure: CameraCaptureFailure
+ ) {
+ callback.onCaptureFailed(1)
+ }
+
+ override fun onCaptureBufferLost(
+ request: RequestProcessor.Request,
+ frameNumber: Long,
+ outputConfigId: Int
+ ) {}
+
+ override fun onCaptureSequenceCompleted(sequenceId: Int, frameNumber: Long) {}
+
+ override fun onCaptureSequenceAborted(sequenceId: Int) {}
+ }
+ )
+ return FAKE_CAPTURE_SEQUENCE_ID
+ }
+
+ override fun abortCapture(captureSequenceId: Int) {
+ }
+
+ suspend fun assertInitSessionInvoked(): Long {
+ return initSessionCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun wasInitSessionInvoked(): Boolean {
+ val result = withTimeoutOrNull(3000) { initSessionCalled.await() }
+ return result != null
+ }
+
+ suspend fun assertDeInitSessionInvoked(): Long {
+ return deInitSessionCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun assertOnCaptureSessionStartInvoked(): Long {
+ return onCaptureSessionStartCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun wasOnCaptureSessionStartInvoked(): Boolean {
+ val result = withTimeoutOrNull(3000) { onCaptureSessionStartCalled.await() }
+ return result != null
+ }
+
+ suspend fun assertOnCaptureEndInvoked(): Long {
+ return onCaptureSessionEndCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun assertStartRepeatingInvoked(): Long {
+ return startRepeatingCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun assertStartCaptureInvoked(): Long {
+ return startCaptureCalled.awaitWithTimeout(3000)
+ }
+
+ suspend fun assertSetParametersInvoked(): Config {
+ return setParametersCalled.awaitWithTimeout(3000)
+ }
+ }
}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index b3dd1ee..203c4dc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -17,7 +17,6 @@
package androidx.camera.camera2.internal;
import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CaptureRequest;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -344,25 +343,9 @@
break;
case ON_CAPTURE_SESSION_STARTED:
mIsExecutingStillCaptureRequest = true;
- CaptureRequestOptions.Builder builder =
- CaptureRequestOptions.Builder.from(
- captureConfig.getImplementationOptions());
-
- if (captureConfig.getImplementationOptions().containsOption(
- CaptureConfig.OPTION_ROTATION)) {
- builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
- captureConfig.getImplementationOptions().retrieveOption(
- CaptureConfig.OPTION_ROTATION));
- }
-
- if (captureConfig.getImplementationOptions().containsOption(
- CaptureConfig.OPTION_JPEG_QUALITY)) {
- builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
- captureConfig.getImplementationOptions().retrieveOption(
- CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
- }
-
- mStillCaptureOptions = builder.build();
+ mStillCaptureOptions =
+ CaptureRequestOptions.Builder.from(captureConfig.getImplementationOptions())
+ .build();
updateParameters(mSessionOptions, mStillCaptureOptions);
mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
@Override
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
index 50d851e..f831878 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -166,7 +166,7 @@
// Act.
pipeline.executeCapture(
listOf(singleRequest),
- ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ FLASH_MODE_OFF,
)
// Assert.
diff --git a/camera/camera-core/lint-baseline.xml b/camera/camera-core/lint-baseline.xml
index aaff45e..4aba0fb 100644
--- a/camera/camera-core/lint-baseline.xml
+++ b/camera/camera-core/lint-baseline.xml
@@ -128,6 +128,116 @@
errorLine1=" @Override"
errorLine2=" ^">
<location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="53"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="76"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="103"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="108"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="113"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="118"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="123"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Nullable"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="128"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="134"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+ line="146"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Override"
+ errorLine2=" ^">
+ <location
file="src/main/java/androidx/camera/core/ForwardingImageProxy.java"
line="65"
column="5"/>
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
deleted file mode 100644
index be57eaf..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import androidx.camera.core.impl.ImageReaderProxy
-import androidx.camera.core.impl.MutableTagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito
-import org.mockito.Mockito.spy
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ModifiableImageReaderProxyTest {
- private lateinit var imageReader: ImageReader
- private lateinit var imageReaderProxy: ModifiableImageReaderProxy
- private var imageWriter: ImageWriter? = null
-
- @Before
- fun setUp() {
- imageReader = spy(ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2))
- imageReaderProxy = ModifiableImageReaderProxy(imageReader)
- }
-
- @After
- fun tearDown() {
- imageReaderProxy.close()
- imageWriter?.close()
- }
-
- @Test
- fun canModifyImageTagBundle_acquireNext() {
- generateImage(imageReader)
-
- val tagBundle = MutableTagBundle.create()
- imageReaderProxy.setImageTagBundle(tagBundle)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
- }
-
- @Test
- fun canModifyImageTagBundle_acquireLatest() {
- generateImage(imageReader)
-
- val tagBundle = MutableTagBundle.create()
- imageReaderProxy.setImageTagBundle(tagBundle)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageTimestamp_acquireNext() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageTimeStamp(1000)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageTimestamp_acquireLatest() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageTimeStamp(1000)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageRotationDegrees_acquireNext() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageRotationDegrees(90)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageRotationDegress_acquireLatest() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageRotationDegrees(90)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageMatrix_acquireNext() {
- generateImage(imageReader)
-
- val matrix = Matrix()
- imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageMatrix_acquireLatest() {
- generateImage(imageReader)
-
- val matrix = Matrix()
- imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
- imageProxy.close()
- }
-
- private fun generateImage(imageReader: ImageReader) {
- imageWriter = ImageWriter.newInstance(imageReader.surface, 2)
- val image = imageWriter!!.dequeueInputImage()
- imageWriter!!.queueInputImage(image)
- }
-
- @Test
- fun parametersMatchesInnerImageReader() {
- assertThat(imageReaderProxy.width).isEqualTo(640)
- assertThat(imageReaderProxy.height).isEqualTo(480)
- assertThat(imageReaderProxy.imageFormat).isEqualTo(ImageFormat.YUV_420_888)
- assertThat(imageReaderProxy.maxImages).isEqualTo(2)
- assertThat(imageReaderProxy.surface).isEqualTo(imageReader.surface)
- }
-
- @Test
- fun setOnImageAvailableListener_innerReaderIsInvoked() {
- val listener = Mockito.mock(
- ImageReaderProxy.OnImageAvailableListener::class.java
- )
-
- imageReaderProxy.setOnImageAvailableListener(
- listener,
- CameraXExecutors.directExecutor()
- )
-
- val transformedListenerCaptor = ArgumentCaptor.forClass(
- ImageReader.OnImageAvailableListener::class.java
- )
- val handlerCaptor = ArgumentCaptor.forClass(
- Handler::class.java
- )
- Mockito.verify(imageReader, Mockito.times(1))
- .setOnImageAvailableListener(
- transformedListenerCaptor.capture(), handlerCaptor.capture()
- )
-
- transformedListenerCaptor.value.onImageAvailable(imageReader)
- Mockito.verify(listener, Mockito.times(1)).onImageAvailable(imageReaderProxy)
- }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
index 92efe7d..ea2a97e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
@@ -36,10 +36,9 @@
* ImageReader}.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class AndroidImageReaderProxy implements ImageReaderProxy {
- @GuardedBy("mLock")
+final class AndroidImageReaderProxy implements ImageReaderProxy {
+ @GuardedBy("this")
private final ImageReader mImageReader;
- private final Object mLock = new Object();
/**
* Creates a new instance which wraps the given image reader.
@@ -53,52 +52,48 @@
@Override
@Nullable
- public ImageProxy acquireLatestImage() {
- synchronized (mLock) {
- Image image;
- try {
- image = mImageReader.acquireLatestImage();
- } catch (RuntimeException e) {
+ public synchronized ImageProxy acquireLatestImage() {
+ Image image;
+ try {
+ image = mImageReader.acquireLatestImage();
+ } catch (RuntimeException e) {
/* In API level 23 or below, it will throw "java.lang.RuntimeException:
ImageReaderContext is not initialized" when ImageReader is closed. To make the
behavior consistent as newer API levels, we make it return null Image instead.*/
- if (isImageReaderContextNotInitializedException(e)) {
- image = null;
- } else {
- throw e; // only catch RuntimeException:ImageReaderContext is not initialized
- }
+ if (isImageReaderContextNotInitializedException(e)) {
+ image = null;
+ } else {
+ throw e; // only catch RuntimeException:ImageReaderContext is not initialized
}
-
- if (image == null) {
- return null;
- }
- return new AndroidImageProxy(image);
}
+
+ if (image == null) {
+ return null;
+ }
+ return new AndroidImageProxy(image);
}
@Override
@Nullable
- public ImageProxy acquireNextImage() {
- synchronized (mLock) {
- Image image;
- try {
- image = mImageReader.acquireNextImage();
- } catch (RuntimeException e) {
+ public synchronized ImageProxy acquireNextImage() {
+ Image image;
+ try {
+ image = mImageReader.acquireNextImage();
+ } catch (RuntimeException e) {
/* In API level 23 or below, it will throw "java.lang.RuntimeException:
ImageReaderContext is not initialized" when ImageReader is closed. To make the
behavior consistent as newer API levels, we make it return null Image instead.*/
- if (isImageReaderContextNotInitializedException(e)) {
- image = null;
- } else {
- throw e; // only catch RuntimeException:ImageReaderContext is not initialized
- }
+ if (isImageReaderContextNotInitializedException(e)) {
+ image = null;
+ } else {
+ throw e; // only catch RuntimeException:ImageReaderContext is not initialized
}
-
- if (image == null) {
- return null;
- }
- return new AndroidImageProxy(image);
}
+
+ if (image == null) {
+ return null;
+ }
+ return new AndroidImageProxy(image);
}
private boolean isImageReaderContextNotInitializedException(RuntimeException e) {
@@ -106,66 +101,50 @@
}
@Override
- public void close() {
- synchronized (mLock) {
- mImageReader.close();
- }
+ public synchronized void close() {
+ mImageReader.close();
}
@Override
- public int getHeight() {
- synchronized (mLock) {
- return mImageReader.getHeight();
- }
+ public synchronized int getHeight() {
+ return mImageReader.getHeight();
}
@Override
- public int getWidth() {
- synchronized (mLock) {
- return mImageReader.getWidth();
- }
+ public synchronized int getWidth() {
+ return mImageReader.getWidth();
}
@Override
- public int getImageFormat() {
- synchronized (mLock) {
- return mImageReader.getImageFormat();
- }
+ public synchronized int getImageFormat() {
+ return mImageReader.getImageFormat();
}
@Override
- public int getMaxImages() {
- synchronized (mLock) {
- return mImageReader.getMaxImages();
- }
+ public synchronized int getMaxImages() {
+ return mImageReader.getMaxImages();
}
@Nullable
@Override
- public Surface getSurface() {
- synchronized (mLock) {
- return mImageReader.getSurface();
- }
+ public synchronized Surface getSurface() {
+ return mImageReader.getSurface();
}
@Override
- public void setOnImageAvailableListener(
+ public synchronized void setOnImageAvailableListener(
@NonNull OnImageAvailableListener listener,
@NonNull Executor executor) {
- synchronized (mLock) {
- // ImageReader does not accept an executor. As a workaround, the callback is run on main
- // handler then immediately posted to the executor.
- ImageReader.OnImageAvailableListener transformedListener = (imageReader) ->
- executor.execute(() -> listener.onImageAvailable(AndroidImageReaderProxy.this));
- mImageReader.setOnImageAvailableListener(transformedListener,
- MainThreadAsyncHandler.getInstance());
- }
+ // ImageReader does not accept an executor. As a workaround, the callback is run on main
+ // handler then immediately posted to the executor.
+ ImageReader.OnImageAvailableListener transformedListener = (imageReader) ->
+ executor.execute(() -> listener.onImageAvailable(AndroidImageReaderProxy.this));
+ mImageReader.setOnImageAvailableListener(transformedListener,
+ MainThreadAsyncHandler.getInstance());
}
@Override
- public void clearOnImageAvailableListener() {
- synchronized (mLock) {
- mImageReader.setOnImageAvailableListener(null, null);
- }
+ public synchronized void clearOnImageAvailableListener() {
+ mImageReader.setOnImageAvailableListener(null, null);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index b1b4c0c..053af09 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -31,7 +31,6 @@
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_PROCESSOR_ENABLED;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO;
@@ -91,7 +90,6 @@
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.MutableTagBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
@@ -312,11 +310,6 @@
*/
private boolean mUseSoftwareJpeg = false;
- /**
- * Whether SessionProcessor is enabled.
- */
- private boolean mIsSessionProcessorEnabled = true;
-
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@@ -390,54 +383,6 @@
resolution.getHeight(), getImageFormat(), MAX_IMAGES, 0));
mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
};
- } else if (mIsSessionProcessorEnabled) {
- ImageReaderProxy imageReader;
- if (getImageFormat() == ImageFormat.JPEG) {
- imageReader =
- new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
- resolution.getHeight(), getImageFormat(), MAX_IMAGES));
- } else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
- if (Build.VERSION.SDK_INT >= 26) {
- // Jpeg rotation / quality will be set to softwareJpegProcessor later in
- // ImageCaptureRequestProcessor.
- mYuvToJpegProcessor =
- new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
-
- ModifiableImageReaderProxy inputReader =
- new ModifiableImageReaderProxy(
- ImageReader.newInstance(resolution.getWidth(),
- resolution.getHeight(),
- ImageFormat.YUV_420_888,
- MAX_IMAGES));
-
- CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- inputReader,
- captureBundle,
- mYuvToJpegProcessor
- ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
- // Ensure the ImageProxy contains the same capture stage id expected from the
- // ProcessingImageReader.
- MutableTagBundle tagBundle = MutableTagBundle.create();
- tagBundle.putTag(processingImageReader.getTagBundleKey(),
- captureBundle.getCaptureStages().get(0).getId());
- inputReader.setImageTagBundle(tagBundle);
-
- YuvToJpegProcessor processorToClose = mYuvToJpegProcessor;
- processingImageReader.getCloseFuture().addListener(() -> {
- processorToClose.close();
- }, CameraXExecutors.directExecutor());
-
- imageReader = processingImageReader;
- } else {
- throw new UnsupportedOperationException("Does not support API level < 26");
- }
- } else {
- throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
- }
- mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {};
- mImageReader = new SafeCloseImageReaderProxy(imageReader);
} else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
// Capture processor set from configuration takes precedence over software JPEG.
CaptureProcessor captureProcessor = mCaptureProcessor;
@@ -468,14 +413,11 @@
}
// TODO: To allow user to use an Executor for the image processing.
- mProcessingImageReader = new ProcessingImageReader.Builder(
- resolution.getWidth(),
- resolution.getHeight(),
- inputFormat,
- mMaxCaptureStages,
+ mProcessingImageReader = new ProcessingImageReader.Builder(resolution.getWidth(),
+ resolution.getHeight(), inputFormat, mMaxCaptureStages,
getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
- captureProcessor
- ).setPostProcessExecutor(mExecutor).setOutputFormat(outputFormat).build();
+ captureProcessor).setPostProcessExecutor(mExecutor).setOutputFormat(
+ outputFormat).build();
mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
@@ -712,21 +654,7 @@
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.YUV_420_888);
} else {
- List<Pair<Integer, Size[]>> supportedSizes =
- builder.getMutableConfig().retrieveOption(OPTION_SUPPORTED_RESOLUTIONS,
- null);
- if (supportedSizes == null) {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
- } else {
- // Use Jpeg first if supported.
- if (isImageFormatSupported(supportedSizes, ImageFormat.JPEG)) {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
- ImageFormat.JPEG);
- } else if (isImageFormatSupported(supportedSizes, ImageFormat.YUV_420_888)) {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
- ImageFormat.YUV_420_888);
- }
- }
+ builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
}
}
@@ -737,18 +665,6 @@
return builder.getUseCaseConfig();
}
- private static boolean isImageFormatSupported(List<Pair<Integer, Size[]>> supportedSizes,
- int imageFormat) {
- if (supportedSizes == null) {
- return false;
- }
- for (Pair<Integer, Size[]> supportedSize : supportedSizes) {
- if (supportedSize.first.equals(imageFormat)) {
- return true;
- }
- }
- return false;
- }
/**
* Configures flash mode to CameraControlInternal once it is ready.
*
@@ -1638,7 +1554,6 @@
// This will only be set to true if software JPEG was requested and
// enforceSoftwareJpegConstraints() hasn't removed the request.
mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
- mIsSessionProcessorEnabled = useCaseConfig.isSessionProcessorEnabled();
CameraInternal camera = getCamera();
Preconditions.checkNotNull(camera, "Attached camera cannot be null");
@@ -2591,17 +2506,6 @@
}
/**
- * Set the flag to indicate whether SessionProcessor is enabled.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setSessionProcessorEnabled(boolean enabled) {
- getMutableConfig().insertOption(OPTION_SESSION_PROCESSOR_ENABLED, enabled);
- return this;
- }
-
- /**
* Sets the max number of {@link CaptureStage}.
*
* @param maxCaptureStages The max CaptureStage number.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
deleted file mode 100644
index 4d9250b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.Matrix;
-import android.media.ImageReader;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.TagBundle;
-
-/**
- * An ImageReaderProxy implementation that allows to modify the ImageInfo data of the images
- * retrieved.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class ModifiableImageReaderProxy extends AndroidImageReaderProxy {
- private volatile TagBundle mTagBundle = null;
- private volatile Long mTimestamp = null;
- private volatile Integer mRotationDegrees = null;
- private volatile Matrix mSensorToBufferTransformMatrix = null;
-
- ModifiableImageReaderProxy(@NonNull ImageReader imageReader) {
- super(imageReader);
- }
-
- void setImageTagBundle(@NonNull TagBundle tagBundle) {
- mTagBundle = tagBundle;
- }
-
- void setImageTimeStamp(long timestamp) {
- mTimestamp = timestamp;
- }
-
- void setImageRotationDegrees(int rotationDegrees) {
- mRotationDegrees = rotationDegrees;
- }
-
- void setImageSensorToBufferTransformaMatrix(@NonNull Matrix matrix) {
- mSensorToBufferTransformMatrix = matrix;
- }
-
- @Nullable
- @Override
- public ImageProxy acquireLatestImage() {
- return modifyImage(super.acquireNextImage());
- }
-
- @Nullable
- @Override
- public ImageProxy acquireNextImage() {
- return modifyImage(super.acquireNextImage());
- }
-
- private ImageProxy modifyImage(ImageProxy imageProxy) {
- ImageInfo origin = imageProxy.getImageInfo();
- ImageInfo imageInfo = ImmutableImageInfo.create(
- mTagBundle != null ? mTagBundle : origin.getTagBundle(),
- mTimestamp != null ? mTimestamp.longValue() : origin.getTimestamp(),
- mRotationDegrees != null ? mRotationDegrees.intValue() :
- origin.getRotationDegrees(),
- mSensorToBufferTransformMatrix != null ? mSensorToBufferTransformMatrix :
- origin.getSensorToBufferTransformMatrix());
- return new SettableImageProxy(imageProxy, imageInfo);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index bf4d35b..9a92907 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -26,7 +26,6 @@
import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.PreviewConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_RGBA8888_SURFACE_REQUIRED;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
@@ -205,9 +204,8 @@
mSessionDeferrableSurface.close();
}
- boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
- isRGBA8888SurfaceRequired);
+ captureProcessor != null);
mCurrentSurfaceRequest = surfaceRequest;
if (sendSurfaceRequestIfReady()) {
@@ -1033,18 +1031,6 @@
return this;
}
- /**
- * Sets if the surface requires RGBA8888 format.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setIsRgba8888SurfaceRequired(boolean isRgba8888SurfaceRequired) {
- getMutableConfig().insertOption(
- OPTION_RGBA8888_SURFACE_REQUIRED, isRgba8888SurfaceRequired);
- return this;
- }
-
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
index 406ba95..e702f3b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
@@ -144,7 +144,7 @@
boolean mProcessing = false;
@GuardedBy("mLock")
- final ImageReaderProxy mInputImageReader;
+ final MetadataImageReader mInputImageReader;
@GuardedBy("mLock")
final ImageReaderProxy mOutputImageReader;
@@ -403,11 +403,7 @@
@Nullable
CameraCaptureCallback getCameraCaptureCallback() {
synchronized (mLock) {
- if (mInputImageReader instanceof MetadataImageReader) {
- return ((MetadataImageReader) mInputImageReader).getCameraCaptureCallback();
- } else {
- return new CameraCaptureCallback() {};
- }
+ return mInputImageReader.getCameraCaptureCallback();
}
}
@@ -458,7 +454,7 @@
*/
static final class Builder {
@NonNull
- protected final ImageReaderProxy mInputImageReader;
+ protected final MetadataImageReader mInputImageReader;
@NonNull
protected final CaptureBundle mCaptureBundle;
@NonNull
@@ -477,7 +473,7 @@
* @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
* ready
*/
- Builder(@NonNull ImageReaderProxy imageReader, @NonNull CaptureBundle captureBundle,
+ Builder(@NonNull MetadataImageReader imageReader, @NonNull CaptureBundle captureBundle,
@NonNull CaptureProcessor captureProcessor) {
mInputImageReader = imageReader;
mCaptureBundle = captureBundle;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
index fc37406..0f233fb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
@@ -62,8 +62,6 @@
Option.create("camerax.core.imageCapture.flashType", int.class);
public static final Option<Integer> OPTION_JPEG_COMPRESSION_QUALITY =
Option.create("camerax.core.imageCapture.jpegCompressionQuality", int.class);
- public static final Option<Boolean> OPTION_SESSION_PROCESSOR_ENABLED =
- Option.create("camerax.core.imageCapture.sessionProcessorEnabled", boolean.class);
// *********************************************************************************************
@@ -290,13 +288,6 @@
return retrieveOption(OPTION_JPEG_COMPRESSION_QUALITY);
}
- /**
- * Returns if the SessionProcessor is enabled.
- */
- public boolean isSessionProcessorEnabled() {
- return retrieveOption(OPTION_SESSION_PROCESSOR_ENABLED, false);
- }
-
// Implementations of IO default methods
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
index 6d235ab..146c52a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
@@ -37,8 +37,6 @@
"camerax.core.preview.imageInfoProcessor", ImageInfoProcessor.class);
public static final Option<CaptureProcessor> OPTION_PREVIEW_CAPTURE_PROCESSOR =
Option.create("camerax.core.preview.captureProcessor", CaptureProcessor.class);
- public static final Option<Boolean> OPTION_RGBA8888_SURFACE_REQUIRED =
- Option.create("camerax.core.preview.isRgba8888SurfaceRequired", Boolean.class);
private final OptionsBundle mConfig;
/** Creates a new configuration instance. */
@@ -91,12 +89,6 @@
}
/**
- * Returns if the preview surface requires RGBA8888 format.
- */
- public boolean isRgba8888SurfaceRequired(boolean valueIfMissing) {
- return retrieveOption(OPTION_RGBA8888_SURFACE_REQUIRED, valueIfMissing);
- }
- /**
* Retrieves the format of the image that is fed as input.
*
* <p>This should be YUV_420_888, when processing is run on the image. Otherwise it is PRIVATE.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 70bdbc4..db0e00f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -16,6 +16,8 @@
package androidx.camera.core.impl;
+import android.util.Range;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -75,6 +77,13 @@
Option<CameraSelector> OPTION_CAMERA_SELECTOR =
Config.Option.create("camerax.core.useCase.cameraSelector", CameraSelector.class);
+ /**
+ * Option: camerax.core.useCase.targetFramerate
+ */
+ Option<Range<Integer>> OPTION_TARGET_FRAME_RATE =
+ Config.Option.create("camerax.core.useCase.targetFrameRate", CameraSelector.class);
+
+
// *********************************************************************************************
/**
@@ -250,6 +259,28 @@
}
/**
+ * Retrieves target frame rate
+ * @param valueIfMissing
+ * @return the stored value or <code>valueIfMissing</code> if the value does not exist in
+ * this configuration
+ */
+ @Nullable
+ default Range<Integer> getTargetFramerate(@Nullable Range<Integer> valueIfMissing) {
+ return retrieveOption(OPTION_TARGET_FRAME_RATE, valueIfMissing);
+ }
+
+ /**
+ * Retrieves the target frame rate
+ *
+ * @return The stored value, if it exists in this configuration.
+ * @throws IllegalArgumentException if the option does not exist in this configuration.
+ */
+ @NonNull
+ default Range<Integer> getTargetFramerate() {
+ return retrieveOption(OPTION_TARGET_FRAME_RATE);
+ }
+
+ /**
* Builder for a {@link UseCase}.
*
* @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt
new file mode 100644
index 0000000..0f6df49
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.impl
+
+import android.os.Build
+import android.util.Range
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@RunWith(
+ RobolectricTestRunner::class
+)
+@DoNotInstrument
+class UseCaseConfigTest {
+ @Test
+ fun canGetTargetFrameRate() {
+ val useCaseBuilder = FakeUseCaseConfig.Builder()
+ val range = Range(10, 20)
+ useCaseBuilder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, range)
+ Truth.assertThat(useCaseBuilder.useCaseConfig.targetFramerate).isEqualTo(range)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
index b7927fb..4204cc3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
@@ -17,6 +17,9 @@
package androidx.camera.extensions;
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -42,7 +45,9 @@
import androidx.camera.extensions.internal.VendorExtender;
import androidx.camera.extensions.internal.Version;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* A class for querying extensions related information.
@@ -227,6 +232,11 @@
private static VendorExtender getVendorExtender(int mode) {
boolean isAdvancedExtenderSupported = isAdvancedExtenderSupported();
+ // Disable Advanced Extender until it is well tested.
+ if (isAdvancedExtenderSupported) {
+ return new DisabledVendorExtender();
+ }
+
VendorExtender vendorExtender;
if (isAdvancedExtenderSupported) {
vendorExtender = new AdvancedVendorExtender(mode);
@@ -270,4 +280,47 @@
}
return id;
}
+
+ static class DisabledVendorExtender implements VendorExtender {
+ @Override
+ public boolean isExtensionAvailable(@NonNull String cameraId,
+ @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+ return false;
+ }
+
+ @Override
+ public void init(@NonNull CameraInfo cameraInfo) {
+
+ }
+
+ @Nullable
+ @Override
+ public Range<Long> getEstimatedCaptureLatencyRange(@Nullable Size size) {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions() {
+ return Collections.emptyList();
+ }
+
+ @NonNull
+ @Override
+ public List<Pair<Integer, Size[]>> getSupportedCaptureOutputResolutions() {
+ return Collections.emptyList();
+ }
+
+ @NonNull
+ @Override
+ public Size[] getSupportedYuvAnalysisResolutions() {
+ return new Size[0];
+ }
+
+ @Nullable
+ @Override
+ public SessionProcessor createSessionProcessor(@NonNull Context context) {
+ return null;
+ }
+ }
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index a11ff2b..72a78cf 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -180,11 +180,16 @@
return map.getOutputSizes(imageFormat);
}
- private int getPreviewOutputImageFormat() {
- return ImageFormat.PRIVATE;
+ private int getPreviewInputImageFormat() {
+ if (mPreviewExtenderImpl != null && mPreviewExtenderImpl.getProcessorType()
+ == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+ return ImageFormat.YUV_420_888;
+ } else {
+ return ImageFormat.PRIVATE;
+ }
}
- private int getCaptureOutputImageFormat() {
+ private int getCaptureInputImageFormat() {
if (mImageCaptureExtenderImpl != null
&& mImageCaptureExtenderImpl.getCaptureProcessor() != null) {
return ImageFormat.YUV_420_888;
@@ -213,7 +218,7 @@
// Returns output sizes from stream configuration map if OEM returns null or OEM does not
// implement the function. It is required to return all supported sizes so it must fetch
// all sizes from the stream configuration map here.
- int imageformat = getPreviewOutputImageFormat();
+ int imageformat = getPreviewInputImageFormat();
return Arrays.asList(new Pair<>(imageformat, getOutputSizes(imageformat)));
}
@@ -237,7 +242,7 @@
// Returns output sizes from stream configuration map if OEM returns null or OEM does not
// implement the function. It is required to return all supported sizes so it must fetch
// all sizes from the stream configuration map here.
- int imageFormat = getCaptureOutputImageFormat();
+ int imageFormat = getCaptureInputImageFormat();
return Arrays.asList(new Pair<>(imageFormat, getOutputSizes(imageFormat)));
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index a09bf72..c280363 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -118,8 +118,6 @@
} else {
Logger.e(TAG, "ImageCaptureExtenderImpl is null!");
}
- } else { // Advanced vendor interface
- builder.setSessionProcessorEnabled(true);
}
builder.getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE,
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index 9fa8a86..3da8a12 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -105,7 +105,6 @@
AdaptingPreviewProcessor(
(PreviewImageProcessorImpl) previewExtenderImpl.getProcessor());
builder.setCaptureProcessor(adaptingPreviewProcessor);
- builder.setIsRgba8888SurfaceRequired(true);
previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
adaptingPreviewProcessor);
break;
@@ -119,10 +118,6 @@
} else {
Logger.e(TAG, "PreviewExtenderImpl is null!");
}
- } else { // Advanced extensions interface.
- // Set RGB8888 = true always since we have no way to tell if the OEM implementation does
- // the processing or not.
- builder.setIsRgba8888SurfaceRequired(true);
}
builder.getMutableConfig().insertOption(OPTION_PREVIEW_CONFIG_PROVIDER_MODE, effectMode);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
index 110695c..e9443ca 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
@@ -101,30 +101,6 @@
}
/**
- * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture} which
- * is suitable to be used in testing that doesn't actually show camera preview but just need
- * a surface for preview.
- *
- * <p> The {@link SurfaceTexture} will be released when it is no longer needed.
- *
- */
- @NonNull
- public static Preview.SurfaceProvider createSurfaceTextureProvider() {
- return createSurfaceTextureProvider(new SurfaceTextureCallback() {
- @Override
- public void onSurfaceTextureReady(@NonNull SurfaceTexture surfaceTexture,
- @NonNull Size resolution) {
- // no op
- }
-
- @Override
- public void onSafeToRelease(@NonNull SurfaceTexture surfaceTexture) {
- surfaceTexture.release();
- }
- });
- }
-
- /**
* Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture}.
*
* <p>This method also creates a backing OpenGL thread that will automatically drain frames
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
deleted file mode 100644
index f862765..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ /dev/null
@@ -1,396 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.testing.fakes
-
-import android.hardware.camera2.CameraDevice
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import android.os.Looper
-import android.os.SystemClock
-import android.view.Surface
-import androidx.annotation.RequiresApi
-import androidx.camera.core.CameraInfo
-import androidx.camera.core.impl.CameraCaptureFailure
-import androidx.camera.core.impl.CameraCaptureResult
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.OptionsBundle
-import androidx.camera.core.impl.OutputSurface
-import androidx.camera.core.impl.RequestProcessor
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.SessionProcessor
-import androidx.camera.core.impl.SessionProcessorSurface
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.withTimeout
-import kotlinx.coroutines.withTimeoutOrNull
-
-const val FAKE_CAPTURE_SEQUENCE_ID = 1
-
-@RequiresApi(23)
-class FakeSessionProcessor(
- val inputFormatPreview: Int?,
- val inputFormatCapture: Int?
-) : SessionProcessor {
- private lateinit var previewProcessorSurface: DeferrableSurface
- private lateinit var captureProcessorSurface: DeferrableSurface
- private var intermediaPreviewImageReader: ImageReader? = null
- private var intermediaCaptureImageReader: ImageReader? = null
- private var intermediaPreviewImageWriter: ImageWriter? = null
- private var intermediaCaptureImageWriter: ImageWriter? = null
-
- private val previewOutputConfigId = 1
- private val captureOutputConfigId = 2
-
- private var requestProcessor: RequestProcessor? = null
-
- // Values of these Deferred are the timestamp to complete.
- private val initSessionCalled = CompletableDeferred<Long>()
- private val deInitSessionCalled = CompletableDeferred<Long>()
- private val onCaptureSessionStartCalled = CompletableDeferred<Long>()
- private val onCaptureSessionEndCalled = CompletableDeferred<Long>()
- private val startRepeatingCalled = CompletableDeferred<Long>()
- private val startCaptureCalled = CompletableDeferred<Long>()
- private val setParametersCalled = CompletableDeferred<Config>()
- private var latestParameters: Config = OptionsBundle.emptyBundle()
- private var blockRunAfterInitSession: () -> Unit = {}
-
- fun releaseSurfaces() {
- intermediaPreviewImageReader?.close()
- intermediaCaptureImageReader?.close()
- }
-
- fun runAfterInitSession(block: () -> Unit) {
- blockRunAfterInitSession = block
- }
-
- override fun initSession(
- cameraInfo: CameraInfo,
- previewSurfaceConfig: OutputSurface,
- imageCaptureSurfaceConfig: OutputSurface,
- imageAnalysisSurfaceConfig: OutputSurface?
- ): SessionConfig {
- initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
- val handler = Handler(Looper.getMainLooper())
-
- var sessionBuilder = SessionConfig.Builder()
-
- // Preview
- lateinit var previewTransformedSurface: Surface
- if (inputFormatPreview == null) { // no conversion, use origin surface.
- previewTransformedSurface = previewSurfaceConfig.surface
- } else {
- intermediaPreviewImageReader = ImageReader.newInstance(
- 640, 480,
- inputFormatPreview, 2
- )
- previewTransformedSurface = intermediaPreviewImageReader!!.surface
-
- intermediaPreviewImageWriter = ImageWriter.newInstance(
- previewSurfaceConfig.surface, 2
- )
-
- intermediaPreviewImageReader!!.setOnImageAvailableListener(
- {
- it.acquireNextImage().use {
- val imageDequeued = intermediaPreviewImageWriter!!.dequeueInputImage()
- intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
- }
- },
- handler
- )
- }
- previewProcessorSurface =
- SessionProcessorSurface(previewTransformedSurface, previewOutputConfigId)
- previewProcessorSurface.terminationFuture.addListener(
- {
- intermediaPreviewImageReader?.close()
- intermediaPreviewImageWriter?.close()
- },
- CameraXExecutors.directExecutor()
- )
- sessionBuilder.addSurface(previewProcessorSurface)
-
- // Capture
- lateinit var captureTransformedSurface: Surface
- if (inputFormatCapture == null) { // no conversion, use origin surface.
- captureTransformedSurface = imageCaptureSurfaceConfig.surface
- } else {
- intermediaCaptureImageReader = ImageReader.newInstance(
- 640, 480,
- inputFormatCapture, 2
- )
- captureTransformedSurface = intermediaCaptureImageReader!!.surface
-
- intermediaCaptureImageWriter = ImageWriter.newInstance(
- imageCaptureSurfaceConfig.surface, 2
- )
-
- intermediaCaptureImageReader!!.setOnImageAvailableListener(
- {
- it.acquireNextImage().use {
- val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
- intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
- }
- },
- handler
- )
- }
- captureProcessorSurface =
- SessionProcessorSurface(captureTransformedSurface, captureOutputConfigId)
-
- captureProcessorSurface.terminationFuture.addListener(
- {
- intermediaCaptureImageReader?.close()
- intermediaCaptureImageWriter?.close()
- },
- CameraXExecutors.directExecutor()
- )
- sessionBuilder.addSurface(captureProcessorSurface)
-
- sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
- val sessionConfig = sessionBuilder.build()
- blockRunAfterInitSession()
- return sessionConfig
- }
-
- override fun deInitSession() {
- deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
- previewProcessorSurface.close()
- captureProcessorSurface.close()
- }
-
- override fun setParameters(config: Config) {
- setParametersCalled.complete(config)
- latestParameters = config
- }
-
- override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
- onCaptureSessionStartCalled.complete(SystemClock.elapsedRealtimeNanos())
- requestProcessor = _requestProcessor
- }
-
- override fun onCaptureSessionEnd() {
- onCaptureSessionEndCalled.complete(SystemClock.elapsedRealtimeNanos())
- }
-
- fun getLatestParameters(): Config {
- return latestParameters
- }
-
- override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
- startRepeatingCalled.complete(SystemClock.elapsedRealtimeNanos())
- val builder = RequestProcessorRequest.Builder().apply {
- addTargetOutputConfigId(previewOutputConfigId)
- setParameters(latestParameters)
- setTemplateId(CameraDevice.TEMPLATE_PREVIEW)
- }
-
- requestProcessor!!.setRepeating(
- builder.build(),
- object : RequestProcessor.Callback {
- override fun onCaptureStarted(
- request: RequestProcessor.Request,
- frameNumber: Long,
- timestamp: Long
- ) {}
-
- override fun onCaptureProgressed(
- request: RequestProcessor.Request,
- captureResult: CameraCaptureResult
- ) {}
-
- override fun onCaptureCompleted(
- request: RequestProcessor.Request,
- captureResult: CameraCaptureResult
- ) {
- callback.onCaptureSequenceCompleted(1)
- }
-
- override fun onCaptureFailed(
- request: RequestProcessor.Request,
- captureFailure: CameraCaptureFailure
- ) {}
-
- override fun onCaptureBufferLost(
- request: RequestProcessor.Request,
- frameNumber: Long,
- outputConfigId: Int
- ) {}
-
- override fun onCaptureSequenceCompleted(
- sequenceId: Int,
- frameNumber: Long
- ) {}
-
- override fun onCaptureSequenceAborted(sequenceId: Int) {
- }
- }
- )
- return FAKE_CAPTURE_SEQUENCE_ID
- }
-
- override fun stopRepeating() {
- }
-
- override fun startCapture(callback: SessionProcessor.CaptureCallback): Int {
- startCaptureCalled.complete(SystemClock.elapsedRealtimeNanos())
- val request = RequestProcessorRequest.Builder().apply {
- addTargetOutputConfigId(captureOutputConfigId)
- setParameters(latestParameters)
- setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE)
- }.build()
-
- requestProcessor!!.submit(
- request,
- object : RequestProcessor.Callback {
- override fun onCaptureCompleted(
- request: RequestProcessor.Request,
- captureResult: CameraCaptureResult
- ) {
- callback.onCaptureSequenceCompleted(1)
- }
-
- override fun onCaptureStarted(
- request: RequestProcessor.Request,
- frameNumber: Long,
- timestamp: Long
- ) {}
-
- override fun onCaptureProgressed(
- request: RequestProcessor.Request,
- captureResult: CameraCaptureResult
- ) {}
-
- override fun onCaptureFailed(
- request: RequestProcessor.Request,
- captureFailure: CameraCaptureFailure
- ) {
- callback.onCaptureFailed(1)
- }
-
- override fun onCaptureBufferLost(
- request: RequestProcessor.Request,
- frameNumber: Long,
- outputConfigId: Int
- ) {}
-
- override fun onCaptureSequenceCompleted(sequenceId: Int, frameNumber: Long) {}
-
- override fun onCaptureSequenceAborted(sequenceId: Int) {}
- }
- )
- return FAKE_CAPTURE_SEQUENCE_ID
- }
-
- override fun abortCapture(captureSequenceId: Int) {
- }
-
- suspend fun assertInitSessionInvoked(): Long {
- return initSessionCalled.awaitWithTimeout(3000)
- }
-
- suspend fun wasInitSessionInvoked(): Boolean {
- val result = withTimeoutOrNull(3000) { initSessionCalled.await() }
- return result != null
- }
-
- suspend fun assertDeInitSessionInvoked(): Long {
- return deInitSessionCalled.awaitWithTimeout(3000)
- }
-
- suspend fun assertOnCaptureSessionStartInvoked(): Long {
- return onCaptureSessionStartCalled.awaitWithTimeout(3000)
- }
-
- suspend fun wasOnCaptureSessionStartInvoked(): Boolean {
- val result = withTimeoutOrNull(3000) { onCaptureSessionStartCalled.await() }
- return result != null
- }
-
- suspend fun assertOnCaptureEndInvoked(): Long {
- return onCaptureSessionEndCalled.awaitWithTimeout(3000)
- }
-
- suspend fun assertStartRepeatingInvoked(): Long {
- return startRepeatingCalled.awaitWithTimeout(3000)
- }
-
- suspend fun assertStartCaptureInvoked(): Long {
- return startCaptureCalled.awaitWithTimeout(3000)
- }
-
- suspend fun assertSetParametersInvoked(): Config {
- return setParametersCalled.awaitWithTimeout(3000)
- }
-
- private suspend fun <T> Deferred<T>.awaitWithTimeout(timeMillis: Long): T {
- return withTimeout(timeMillis) {
- await()
- }
- }
-}
-
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class RequestProcessorRequest(
- private val targetOutputConfigIds: List<Int>,
- private val parameters: Config,
- private val templateId: Int
-) : RequestProcessor.Request {
- override fun getTargetOutputConfigIds(): List<Int> {
- return targetOutputConfigIds
- }
-
- override fun getParameters(): Config {
- return parameters
- }
-
- override fun getTemplateId(): Int {
- return templateId
- }
-
- class Builder {
- private var targetOutputConfigIds: MutableList<Int> = ArrayList()
- private var parameters: Config = OptionsBundle.emptyBundle()
- private var templateId = CameraDevice.TEMPLATE_PREVIEW
-
- fun addTargetOutputConfigId(targetOutputConfigId: Int): Builder {
- targetOutputConfigIds.add(targetOutputConfigId)
- return this
- }
-
- fun setParameters(parameters: Config): Builder {
- this.parameters = parameters
- return this
- }
-
- fun setTemplateId(templateId: Int): Builder {
- this.templateId = templateId
- return this
- }
-
- fun build(): RequestProcessorRequest {
- return RequestProcessorRequest(
- targetOutputConfigIds.toList(),
- OptionsBundle.from(parameters),
- templateId
- )
- }
- }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 342ffd0..b06614e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -41,37 +41,26 @@
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
-import androidx.camera.core.CameraFilter
-import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
-import androidx.camera.core.Preview
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
-import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CaptureBundle
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.CaptureProcessor
import androidx.camera.core.impl.CaptureStage
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.Identifier
import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.MutableOptionsBundle
-import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.Exif
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCaptureStage
import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSessionProcessor
import androidx.core.content.ContextCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -1414,139 +1403,6 @@
simpleCaptureProcessor.close()
}
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun returnJpegImage_whenSessionProcessorIsSet_outputFormantYuv() = runBlocking {
- skipTestOnCameraPipeConfig()
-
- val builder = ImageCapture.Builder()
- val sessionProcessor = FakeSessionProcessor(
- inputFormatPreview = null, // null means using the same output surface
- inputFormatCapture = ImageFormat.YUV_420_888
- )
-
- val imageCapture = builder
- .setSessionProcessorEnabled(true)
- .setSupportedResolutions(
- listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480)))))
- .build()
-
- val preview = Preview.Builder().build()
-
- var camera: Camera
- withContext(Dispatchers.Main) {
- preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
- val cameraSelector =
- getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
- camera = cameraProvider.bindToLifecycle(
- fakeLifecycleOwner, cameraSelector, imageCapture, preview)
- }
-
- val callback = FakeImageCaptureCallback(capturesCount = 1)
- imageCapture.takePicture(mainExecutor, callback)
-
- // Wait for the signal that the image has been captured.
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
- val imageProperties = callback.results.first()
-
- // Check the output image rotation degrees value is correct.
- assertThat(imageProperties.rotationDegrees).isEqualTo(
- camera.cameraInfo.getSensorRotationDegrees(imageCapture.targetRotation)
- )
- // Check the output format is correct.
- assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun returnJpegImage_whenSessionProcessorIsSet_outputFormantJpeg() = runBlocking {
- assumeFalse(
- "Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
- Build.MODEL.contains("Cuttlefish")
- )
- skipTestOnCameraPipeConfig()
-
- val builder = ImageCapture.Builder()
- val sessionProcessor = FakeSessionProcessor(
- inputFormatPreview = null, // null means using the same output surface
- inputFormatCapture = null
- )
-
- val imageCapture = builder
- .setSessionProcessorEnabled(true)
- .setSupportedResolutions(
- listOf(android.util.Pair(ImageFormat.JPEG, arrayOf(Size(640, 480)))))
- .build()
-
- val preview = Preview.Builder().build()
-
- withContext(Dispatchers.Main) {
- preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
- val cameraSelector =
- getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
- cameraProvider.bindToLifecycle(
- fakeLifecycleOwner, cameraSelector, imageCapture, preview)
- }
-
- val callback = FakeImageCaptureCallback(capturesCount = 1)
- imageCapture.takePicture(mainExecutor, callback)
-
- // Wait for the signal that the image has been captured.
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
- val imageProperties = callback.results.first()
-
- // Check the output image rotation degrees value is correct.
- assertThat(imageProperties.rotationDegrees).isEqualTo(imageProperties.exif!!.rotation)
-
- // Check the output format is correct.
- assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
- }
-
- private fun getCameraSelectorWithSessionProcessor(
- cameraSelector: CameraSelector,
- sessionProcessor: SessionProcessor
- ): CameraSelector {
- val identifier = Identifier.create("idStr")
- ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
- object : CameraConfig {
- override fun getConfig(): Config {
- return MutableOptionsBundle.create()
- }
-
- override fun getCompatibilityId(): Identifier {
- return Identifier.create(0)
- }
-
- override fun getSessionProcessor(
- valueIfMissing: SessionProcessor?
- ): SessionProcessor? {
- return sessionProcessor
- }
-
- override fun getSessionProcessor(): SessionProcessor {
- return sessionProcessor
- }
- }
- }
-
- val builder = CameraSelector.Builder.fromSelector(cameraSelector)
- builder.addCameraFilter(object : CameraFilter {
- override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
- val newCameraInfos = mutableListOf<CameraInfo>()
- newCameraInfos.addAll(cameraInfos)
- return newCameraInfos
- }
-
- override fun getIdentifier(): Identifier {
- return identifier
- }
- })
-
- return builder.build()
- }
-
// Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
// API level is at least 29.
@Test
diff --git a/car/app/app-automotive/src/main/res/values-ky/strings.xml b/car/app/app-automotive/src/main/res/values-ky/strings.xml
index 192fef2..a27b786 100644
--- a/car/app/app-automotive/src/main/res/values-ky/strings.xml
+++ b/car/app/app-automotive/src/main/res/values-ky/strings.xml
@@ -23,8 +23,8 @@
<string name="error_message_client_side_error" msgid="3323186720368387787">"Колдонмодо ката кетти. Бул катаны колдонмонун иштеп чыгуучусуна кабарлаңыз"</string>
<string name="error_message_host_error" msgid="5484419926049675696">"Тутум катасы"</string>
<string name="error_message_host_connection_lost" msgid="5723205987837759151">"Тутум убактылуу жеткиликсиз"</string>
- <string name="error_message_host_not_found" msgid="3241065067065670113">"Тутумду жаңыртуу керек"</string>
- <string name="error_message_host_incompatible" msgid="160406216155183851">"Тутумду жаңыртуу керек"</string>
+ <string name="error_message_host_not_found" msgid="3241065067065670113">"Системаны жаңыртуу керек"</string>
+ <string name="error_message_host_incompatible" msgid="160406216155183851">"Системаны жаңыртуу керек"</string>
<string name="error_message_multiple_hosts" msgid="2591031904206928207">"Шайкеш келбеген тутум"</string>
<string name="error_message_unknown_error" msgid="1918523834689044166">"Белгисиз ката"</string>
<string name="error_message_no_vending" msgid="5866202078252905802">"Унаа кызматтары менен байланышыңыз"</string>
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index bb4dead..eedae0f 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -16,16 +16,35 @@
import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("AndroidXPlugin")
id("org.jetbrains.kotlin.multiplatform")
}
+// This should be true when building from GitHub, and false when building
+// from AOSP. Use this to block out any features or code that we're not
+// ready to build yet in AOSP
+
+def boolProperty(name) {
+ return project.providers.gradleProperty(name).getOrNull()?.toBoolean()
+}
+
+def playground = androidx.build.StudioType.isPlayground(project)
+//Native on by default in playground
+def enableNative = boolProperty('androidx.kmp.native.enabled') ?: playground
+
kotlin {
jvm {
withJava()
}
+ if (enableNative) {
+ macosX64()
+ macosArm64()
+ linuxX64()
+ mingwX64()
+ }
sourceSets {
commonMain {
@@ -40,6 +59,19 @@
}
}
+ if (enableNative) {
+ nativeMain
+
+ configure([linuxX64Main, macosX64Main, macosArm64Main, mingwX64Main]) {
+ dependsOn nativeMain
+ }
+
+ nativeTest
+ configure([linuxX64Test, macosX64Test, macosArm64Test, mingwX64Test]) {
+ dependsOn nativeTest
+ }
+ }
+
jvmMain {
dependencies {
api(libs.kotlinStdlib)
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
similarity index 87%
rename from collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt
rename to collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
index 80a7b91..6230ddb 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
@@ -16,6 +16,9 @@
package androidx.collection
+import androidx.collection.CollectionPlatformUtils.createIndexOutOfBoundsException
+import kotlin.jvm.JvmOverloads
+
/**
* CircularArray is a generic circular array data structure that provides O(1) random read, O(1)
* prepend and O(1) append. The CircularArray automatically grows its capacity when number of added
@@ -40,8 +43,8 @@
// If minCapacity isn't a power of 2, round up to the next highest
// power of 2.
- val arrayCapacity: Int = if (Integer.bitCount(minCapacity) != 1) {
- Integer.highestOneBit(minCapacity - 1) shl 1
+ val arrayCapacity: Int = if (minCapacity.countOneBits() != 1) {
+ (minCapacity - 1).takeHighestOneBit() shl 1
} else {
minCapacity
}
@@ -97,11 +100,11 @@
* Remove first element from front of the [CircularArray] and return it.
*
* @return The element removed.
- * @throws ArrayIndexOutOfBoundsException if [CircularArray] is empty.
+ * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty (on jvm)
*/
public fun popFirst(): E {
if (head == tail) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
val result = elements[head]
elements[head] = null
@@ -115,11 +118,11 @@
* Remove last element from end of the [CircularArray] and return it.
*
* @return The element removed.
- * @throws ArrayIndexOutOfBoundsException if [CircularArray] is empty.
+ * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
*/
public fun popLast(): E {
if (head == tail) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
val t = (tail - 1) and capacityBitmask
val result = elements[t]
@@ -142,14 +145,14 @@
* is less than or equal to 0.
*
* @param count Number of elements to remove.
- * @throws ArrayIndexOutOfBoundsException if [count] is larger than [size]
+ * @throws [ArrayIndexOutOfBoundsException] if [count] is larger than [size]
*/
public fun removeFromStart(count: Int) {
if (count <= 0) {
return
}
if (count > size()) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
var numOfElements = count
@@ -177,14 +180,14 @@
* is less than or equals to 0.
*
* @param count Number of elements to remove.
- * @throws ArrayIndexOutOfBoundsException if [count] is larger than [size]
+ * @throws [ArrayIndexOutOfBoundsException] if [count] is larger than [size]
*/
public fun removeFromEnd(count: Int) {
if (count <= 0) {
return
}
if (count > size()) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
var numOfElements = count
@@ -213,12 +216,12 @@
* Get first element of the [CircularArray].
*
* @return The first element.
- * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty.
+ * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
*/
public val first: E
get() {
if (head == tail) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
return elements[head]!!
}
@@ -227,12 +230,12 @@
* Get last element of the [CircularArray].
*
* @return The last element.
- * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty.
+ * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
*/
public val last: E
get() {
if (head == tail) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
return elements[tail - 1 and capacityBitmask]!!
}
@@ -242,11 +245,11 @@
*
* @param index The zero based element index in the [CircularArray].
* @return The nth element.
- * @throws [ArrayIndexOutOfBoundsException] if n < 0 or n >= size().
+ * @throws [ArrayIndexOutOfBoundsException] if n < 0 or n >= size()
*/
public operator fun get(index: Int): E {
if (index < 0 || index >= size()) {
- throw ArrayIndexOutOfBoundsException()
+ throw createIndexOutOfBoundsException()
}
return elements[(head + index) and capacityBitmask]!!
}
@@ -266,4 +269,4 @@
* @return `true` if [size] is 0.
*/
public fun isEmpty(): Boolean = head == tail
-}
+}
\ No newline at end of file
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
new file mode 100644
index 0000000..f3cec22
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.collection
+
+/**
+ * Internal utils for handling implementation differences for the various targets of Collections.
+ */
+internal expect object CollectionPlatformUtils {
+
+ /**
+ * IndexOutOfBoundsException is the nearest kotlin common ancestor for the native and jvm
+ * specific implementations of ArrayIndexOutOfBoundsException. Actuals should throw an
+ * exception specific to their target platform.
+ */
+ internal inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException
+}
diff --git a/collection/collection/src/jvmTest/java/androidx/collection/CircularArrayTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/CircularArrayTest.kt
similarity index 100%
rename from collection/collection/src/jvmTest/java/androidx/collection/CircularArrayTest.kt
rename to collection/collection/src/commonTest/kotlin/androidx/collection/CircularArrayTest.kt
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
new file mode 100644
index 0000000..0233729
--- /dev/null
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.collection
+
+/**
+ * JVM actual of internal utils for handling target differences in collection code.
+ */
+internal actual object CollectionPlatformUtils {
+
+ @Suppress("NOTHING_TO_INLINE")
+ internal actual inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException {
+ return ArrayIndexOutOfBoundsException()
+ }
+}
diff --git a/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java b/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java
deleted file mode 100644
index 6e14244..0000000
--- a/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.collection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(JUnit4.class)
-public class LruCacheTest {
-
- private int mExpectedCreateCount;
- private int mExpectedPutCount;
- private int mExpectedHitCount;
- private int mExpectedMissCount;
- private int mExpectedEvictionCount;
-
- @Test
- public void testStatistics() {
- LruCache<String, String> cache = new LruCache<String, String>(3);
- assertStatistics(cache);
- assertEquals(null, cache.put("a", "A"));
- mExpectedPutCount++;
- assertStatistics(cache);
- assertHit(cache, "a", "A");
- assertSnapshot(cache, "a", "A");
- assertEquals(null, cache.put("b", "B"));
- mExpectedPutCount++;
- assertStatistics(cache);
- assertHit(cache, "a", "A");
- assertHit(cache, "b", "B");
- assertSnapshot(cache, "a", "A", "b", "B");
- assertEquals(null, cache.put("c", "C"));
- mExpectedPutCount++;
- assertStatistics(cache);
- assertHit(cache, "a", "A");
- assertHit(cache, "b", "B");
- assertHit(cache, "c", "C");
- assertSnapshot(cache, "a", "A", "b", "B", "c", "C");
- assertEquals(null, cache.put("d", "D"));
- mExpectedPutCount++;
- mExpectedEvictionCount++; // a should have been evicted
- assertStatistics(cache);
- assertMiss(cache, "a");
- assertHit(cache, "b", "B");
- assertHit(cache, "c", "C");
- assertHit(cache, "d", "D");
- assertHit(cache, "b", "B");
- assertHit(cache, "c", "C");
- assertSnapshot(cache, "d", "D", "b", "B", "c", "C");
- assertEquals(null, cache.put("e", "E"));
- mExpectedPutCount++;
- mExpectedEvictionCount++; // d should have been evicted
- assertStatistics(cache);
- assertMiss(cache, "d");
- assertMiss(cache, "a");
- assertHit(cache, "e", "E");
- assertHit(cache, "b", "B");
- assertHit(cache, "c", "C");
- assertSnapshot(cache, "e", "E", "b", "B", "c", "C");
- }
-
- @Test
- public void testStatisticsWithCreate() {
- LruCache<String, String> cache = newCreatingCache();
- assertStatistics(cache);
- assertCreated(cache, "aa", "created-aa");
- assertHit(cache, "aa", "created-aa");
- assertSnapshot(cache, "aa", "created-aa");
- assertCreated(cache, "bb", "created-bb");
- assertMiss(cache, "c");
- assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb");
- assertCreated(cache, "cc", "created-cc");
- assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc");
- mExpectedEvictionCount++; // aa will be evicted
- assertCreated(cache, "dd", "created-dd");
- assertSnapshot(cache, "bb", "created-bb", "cc", "created-cc", "dd", "created-dd");
- mExpectedEvictionCount++; // bb will be evicted
- assertCreated(cache, "aa", "created-aa");
- assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa");
- }
-
- @Test
- public void testCreateOnCacheMiss() {
- LruCache<String, String> cache = newCreatingCache();
- String created = cache.get("aa");
- assertEquals("created-aa", created);
- }
-
- @Test
- public void testNoCreateOnCacheHit() {
- LruCache<String, String> cache = newCreatingCache();
- cache.put("aa", "put-aa");
- assertEquals("put-aa", cache.get("aa"));
- }
-
- @Test
- public void testConstructorDoesNotAllowZeroCacheSize() {
- try {
- new LruCache<String, String>(0);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testCannotPutNullKey() {
- LruCache<String, String> cache = new LruCache<String, String>(3);
- try {
- cache.put(null, "A");
- fail();
- } catch (NullPointerException expected) {
- }
- }
-
- @Test
- public void testCannotPutNullValue() {
- LruCache<String, String> cache = new LruCache<String, String>(3);
- try {
- cache.put("a", null);
- fail();
- } catch (NullPointerException expected) {
- }
- }
-
- @Test
- public void testToString() {
- LruCache<String, String> cache = new LruCache<String, String>(3);
- assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString());
- cache.put("a", "A");
- cache.put("b", "B");
- cache.put("c", "C");
- cache.put("d", "D");
- cache.get("a"); // miss
- cache.get("b"); // hit
- cache.get("c"); // hit
- cache.get("d"); // hit
- cache.get("e"); // miss
- assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString());
- }
-
- @Test
- public void testEvictionWithSingletonCache() {
- LruCache<String, String> cache = new LruCache<String, String>(1);
- cache.put("a", "A");
- cache.put("b", "B");
- assertSnapshot(cache, "b", "B");
- }
-
- @Test
- public void testEntryEvictedWhenFull() {
- List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = newRemovalLogCache(log);
- cache.put("a", "A");
- cache.put("b", "B");
- cache.put("c", "C");
- assertEquals(Collections.<String>emptyList(), log);
- cache.put("d", "D");
- assertEquals(Arrays.asList("a=A"), log);
- }
-
- /**
- * Replacing the value for a key doesn't cause an eviction but it does bring
- * the replaced entry to the front of the queue.
- */
- @Test
- public void testPutCauseEviction() {
- List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = newRemovalLogCache(log);
- cache.put("a", "A");
- cache.put("b", "B");
- cache.put("c", "C");
- cache.put("b", "B2");
- assertEquals(Arrays.asList("b=B>B2"), log);
- assertSnapshot(cache, "a", "A", "c", "C", "b", "B2");
- }
-
- @Test
- public void testCustomSizesImpactsSize() {
- LruCache<String, String> cache = new LruCache<String, String>(10) {
- @Override protected int sizeOf(String key, String value) {
- return key.length() + value.length();
- }
- };
- assertEquals(0, cache.size());
- cache.put("a", "AA");
- assertEquals(3, cache.size());
- cache.put("b", "BBBB");
- assertEquals(8, cache.size());
- cache.put("a", "");
- assertEquals(6, cache.size());
- }
-
- @Test
- public void testEvictionWithCustomSizes() {
- LruCache<String, String> cache = new LruCache<String, String>(4) {
- @Override protected int sizeOf(String key, String value) {
- return value.length();
- }
- };
- cache.put("a", "AAAA");
- assertSnapshot(cache, "a", "AAAA");
- cache.put("b", "BBBB"); // should evict a
- assertSnapshot(cache, "b", "BBBB");
- cache.put("c", "CC"); // should evict b
- assertSnapshot(cache, "c", "CC");
- cache.put("d", "DD");
- assertSnapshot(cache, "c", "CC", "d", "DD");
- cache.put("e", "E"); // should evict c
- assertSnapshot(cache, "d", "DD", "e", "E");
- cache.put("f", "F");
- assertSnapshot(cache, "d", "DD", "e", "E", "f", "F");
- cache.put("g", "G"); // should evict d
- assertSnapshot(cache, "e", "E", "f", "F", "g", "G");
- cache.put("h", "H");
- assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H");
- cache.put("i", "III"); // should evict e, f, and g
- assertSnapshot(cache, "h", "H", "i", "III");
- cache.put("j", "JJJ"); // should evict h and i
- assertSnapshot(cache, "j", "JJJ");
- }
-
- @Test
- public void testEvictionThrowsWhenSizesAreInconsistent() {
- LruCache<String, int[]> cache = new LruCache<String, int[]>(4) {
- @Override protected int sizeOf(String key, int[] value) {
- return value[0];
- }
- };
- int[] a = { 4 };
- cache.put("a", a);
- // get the cache size out of sync
- a[0] = 1;
- assertEquals(4, cache.size());
- // evict something
- try {
- cache.put("b", new int[] { 2 });
- fail();
- } catch (IllegalStateException expected) {
- }
- }
-
- @Test
- public void testEvictionThrowsWhenSizesAreNegative() {
- LruCache<String, String> cache = new LruCache<String, String>(4) {
- @Override protected int sizeOf(String key, String value) {
- return -1;
- }
- };
- try {
- cache.put("a", "A");
- fail();
- } catch (IllegalStateException expected) {
- }
- }
-
- /**
- * Naive caches evict at most one element at a time. This is problematic
- * because evicting a small element may be insufficient to make room for a
- * large element.
- */
- @Test
- public void testDifferentElementSizes() {
- LruCache<String, String> cache = new LruCache<String, String>(10) {
- @Override protected int sizeOf(String key, String value) {
- return value.length();
- }
- };
- cache.put("a", "1");
- cache.put("b", "12345678");
- cache.put("c", "1");
- assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1");
- cache.put("d", "12345678"); // should evict a and b
- assertSnapshot(cache, "c", "1", "d", "12345678");
- cache.put("e", "12345678"); // should evict c and d
- assertSnapshot(cache, "e", "12345678");
- }
-
- @Test
- public void testEvictAll() {
- List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = newRemovalLogCache(log);
- cache.put("a", "A");
- cache.put("b", "B");
- cache.put("c", "C");
- cache.evictAll();
- assertEquals(0, cache.size());
- assertEquals(Arrays.asList("a=A", "b=B", "c=C"), log);
- }
-
- @Test
- public void testEvictAllEvictsSizeZeroElements() {
- LruCache<String, String> cache = new LruCache<String, String>(10) {
- @Override protected int sizeOf(String key, String value) {
- return 0;
- }
- };
- cache.put("a", "A");
- cache.put("b", "B");
- cache.evictAll();
- assertSnapshot(cache);
- }
-
- @Test
- public void testRemoveWithCustomSizes() {
- LruCache<String, String> cache = new LruCache<String, String>(10) {
- @Override protected int sizeOf(String key, String value) {
- return value.length();
- }
- };
- cache.put("a", "123456");
- cache.put("b", "1234");
- cache.remove("a");
- assertEquals(4, cache.size());
- }
-
- @Test
- public void testRemoveAbsentElement() {
- LruCache<String, String> cache = new LruCache<String, String>(10);
- cache.put("a", "A");
- cache.put("b", "B");
- assertEquals(null, cache.remove("c"));
- assertEquals(2, cache.size());
- }
-
- @Test
- public void testRemoveNullThrows() {
- LruCache<String, String> cache = new LruCache<String, String>(10);
- try {
- cache.remove(null);
- fail();
- } catch (NullPointerException expected) {
- }
- }
-
- @Test
- public void testRemoveCallsEntryRemoved() {
- List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = newRemovalLogCache(log);
- cache.put("a", "A");
- cache.remove("a");
- assertEquals(Arrays.asList("a=A>null"), log);
- }
-
- @Test
- public void testPutCallsEntryRemoved() {
- List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = newRemovalLogCache(log);
- cache.put("a", "A");
- cache.put("a", "A2");
- assertEquals(Arrays.asList("a=A>A2"), log);
- }
-
- @Test
- public void testEntryRemovedIsCalledWithoutSynchronization() {
- LruCache<String, String> cache = new LruCache<String, String>(3) {
- @Override protected void entryRemoved(
- boolean evicted, String key, String oldValue, String newValue) {
- assertFalse(Thread.holdsLock(this));
- }
- };
- cache.put("a", "A");
- cache.put("a", "A2"); // replaced
- cache.put("b", "B");
- cache.put("c", "C");
- cache.put("d", "D"); // single eviction
- cache.remove("a"); // removed
- cache.evictAll(); // multiple eviction
- }
-
- @Test
- public void testCreateIsCalledWithoutSynchronization() {
- LruCache<String, String> cache = new LruCache<String, String>(3) {
- @Override protected String create(String key) {
- assertFalse(Thread.holdsLock(this));
- return null;
- }
- };
- cache.get("a");
- }
-
- /**
- * Test what happens when a value is added to the map while create is
- * working. The map value should be returned by get(), and the created value
- * should be released with entryRemoved().
- */
- @Test
- public void testCreateWithConcurrentPut() {
- final List<String> log = new ArrayList<String>();
- LruCache<String, String> cache = new LruCache<String, String>(3) {
- @Override protected String create(String key) {
- put(key, "B");
- return "A";
- }
- @Override protected void entryRemoved(
- boolean evicted, String key, String oldValue, String newValue) {
- log.add(key + "=" + oldValue + ">" + newValue);
- }
- };
- assertEquals("B", cache.get("a"));
- assertEquals(Arrays.asList("a=A>B"), log);
- }
-
- /**
- * Test what happens when two creates happen concurrently. The result from
- * the first create to return is returned by both gets. The other created
- * values should be released with entryRemove().
- */
- @Test
- public void testCreateWithConcurrentCreate() {
- final List<String> log = new ArrayList<String>();
- LruCache<String, Integer> cache = new LruCache<String, Integer>(3) {
- int mCallCount = 0;
- @Override protected Integer create(String key) {
- if (mCallCount++ == 0) {
- assertEquals(2, get(key).intValue());
- return 1;
- } else {
- return 2;
- }
- }
- @Override protected void entryRemoved(
- boolean evicted, String key, Integer oldValue, Integer newValue) {
- log.add(key + "=" + oldValue + ">" + newValue);
- }
- };
- assertEquals(2, cache.get("a").intValue());
- assertEquals(Arrays.asList("a=1>2"), log);
- }
-
- /** Makes sure that LruCache operations are correctly synchronized to guarantee consistency. */
- @Test
- public void consistentMultithreadedAccess() {
- class Tally {
- int mNonNullValues;
- int mNullValues;
- int mValuesPut;
- int mConflicts;
- int mRemoved;
- }
-
- final Tally tally = new Tally();
- final int rounds = 10000;
- final String key = "key";
- final int value = 42;
- final LruCache<String, Integer> cache = new LruCache<String, Integer>(1) {
- @Override
- protected Integer create(String key) {
- return value;
- }
- };
-
- Runnable r0 = new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < rounds; i++) {
- if (cache.get(key) != null) {
- tally.mNonNullValues++;
- } else {
- tally.mNullValues++;
- }
- }
- }
- };
-
- Runnable r1 = new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < rounds; i++) {
- if (i % 2 == 0) {
- if (cache.put(key, value) != null) {
- tally.mConflicts++;
- } else {
- tally.mValuesPut++;
- }
- } else {
- cache.remove(key);
- tally.mRemoved++;
- }
- }
- }
- };
-
-
- Thread t0 = new Thread(r0);
- Thread t1 = new Thread(r1);
-
- t0.start();
- t1.start();
- try {
- t0.join();
- t1.join();
- } catch (InterruptedException e) {
- fail();
- }
-
- assertEquals(rounds, tally.mNonNullValues);
- assertEquals(0, tally.mNullValues);
- assertEquals(rounds, tally.mValuesPut + tally.mConflicts + tally.mRemoved);
- }
-
- private LruCache<String, String> newCreatingCache() {
- return new LruCache<String, String>(3) {
- @Override protected String create(String key) {
- return (key.length() > 1) ? ("created-" + key) : null;
- }
- };
- }
-
- private LruCache<String, String> newRemovalLogCache(final List<String> log) {
- return new LruCache<String, String>(3) {
- @Override protected void entryRemoved(
- boolean evicted, String key, String oldValue, String newValue) {
- String message = evicted
- ? (key + "=" + oldValue)
- : (key + "=" + oldValue + ">" + newValue);
- log.add(message);
- }
- };
- }
-
- private void assertHit(LruCache<String, String> cache, String key, String value) {
- assertEquals(value, cache.get(key));
- mExpectedHitCount++;
- assertStatistics(cache);
- }
-
- private void assertMiss(LruCache<String, String> cache, String key) {
- assertEquals(null, cache.get(key));
- mExpectedMissCount++;
- assertStatistics(cache);
- }
-
- private void assertCreated(LruCache<String, String> cache, String key, String value) {
- assertEquals(value, cache.get(key));
- mExpectedMissCount++;
- mExpectedCreateCount++;
- assertStatistics(cache);
- }
-
- private void assertStatistics(LruCache<?, ?> cache) {
- assertEquals("create count", mExpectedCreateCount, cache.createCount());
- assertEquals("put count", mExpectedPutCount, cache.putCount());
- assertEquals("hit count", mExpectedHitCount, cache.hitCount());
- assertEquals("miss count", mExpectedMissCount, cache.missCount());
- assertEquals("eviction count", mExpectedEvictionCount, cache.evictionCount());
- }
-
- @SuppressWarnings("unchecked")
- private <T> void assertSnapshot(LruCache<T, T> cache, T... keysAndValues) {
- List<T> actualKeysAndValues = new ArrayList<T>();
- for (Map.Entry<T, T> entry : cache.snapshot().entrySet()) {
- actualKeysAndValues.add(entry.getKey());
- actualKeysAndValues.add(entry.getValue());
- }
- // assert using lists because order is important for LRUs
- assertEquals(Arrays.asList(keysAndValues), actualKeysAndValues);
- }
-}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt
new file mode 100644
index 0000000..8c0dc29
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.collection
+
+import kotlin.concurrent.thread
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+internal class LruCacheJvmTest {
+
+ @Test
+ fun testEntryRemovedIsCalledWithoutSynchronization() {
+ val cache =
+ object : LruCache<String, String>(3) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ assertFalse(Thread.holdsLock(this))
+ }
+ }
+ cache.put("a", "A")
+ cache.put("a", "A2") // replaced
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("d", "D") // single eviction
+ cache.remove("a") // removed
+ cache.evictAll() // multiple eviction
+ }
+
+ /** Makes sure that LruCache operations are correctly synchronized to guarantee consistency. */
+ @Test
+ fun consistentMultithreadedAccess() {
+ var nonNullValues = 0
+ var nullValues = 0
+ var valuesPut = 0
+ var conflicts = 0
+ var removed = 0
+
+ val rounds = 10000
+ val key = "key"
+ val value = 42
+ val cache =
+ object : LruCache<String, Int>(1) {
+ override fun create(key: String): Int = value
+ }
+
+ val t0 =
+ thread {
+ repeat(rounds) {
+ if (cache[key] != null) {
+ nonNullValues++
+ } else {
+ nullValues++
+ }
+ }
+ }
+
+ val t1 =
+ thread {
+ repeat(rounds) { i ->
+ if (i % 2 == 0) {
+ if (cache.put(key, value) != null) {
+ conflicts++
+ } else {
+ valuesPut++
+ }
+ } else {
+ cache.remove(key)
+ removed++
+ }
+ }
+ }
+
+ t0.join()
+ t1.join()
+
+ assertEquals(rounds, nonNullValues)
+ assertEquals(0, nullValues)
+ assertEquals(rounds, valuesPut + conflicts + removed)
+ }
+}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt
new file mode 100644
index 0000000..98d7ee6
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+
+internal class LruCacheTest {
+
+ private var expectedCreateCount = 0
+ private var expectedPutCount = 0
+ private var expectedHitCount = 0
+ private var expectedMissCount = 0
+ private var expectedEvictionCount = 0
+
+ @Test
+ fun testStatistics() {
+ val cache = LruCache<String, String>(3)
+ assertStatistics(cache)
+ assertNull(cache.put("a", "A"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertSnapshot(cache, "a", "A")
+ assertNull(cache.put("b", "B"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertHit(cache, "b", "B")
+ assertSnapshot(cache, "a", "A", "b", "B")
+ assertNull(cache.put("c", "C"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "a", "A", "b", "B", "c", "C")
+ assertNull(cache.put("d", "D"))
+ expectedPutCount++
+ expectedEvictionCount++ // a should have been evicted
+ assertStatistics(cache)
+ assertMiss(cache, "a")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertHit(cache, "d", "D")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "d", "D", "b", "B", "c", "C")
+ assertNull(cache.put("e", "E"))
+ expectedPutCount++
+ expectedEvictionCount++ // d should have been evicted
+ assertStatistics(cache)
+ assertMiss(cache, "d")
+ assertMiss(cache, "a")
+ assertHit(cache, "e", "E")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "e", "E", "b", "B", "c", "C")
+ }
+
+ @Test
+ fun testStatisticsWithCreate() {
+ val cache = newCreatingCache()
+ assertStatistics(cache)
+ assertCreated(cache, "aa", "created-aa")
+ assertHit(cache, "aa", "created-aa")
+ assertSnapshot(cache, "aa", "created-aa")
+ assertCreated(cache, "bb", "created-bb")
+ assertMiss(cache, "c")
+ assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb")
+ assertCreated(cache, "cc", "created-cc")
+ assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc")
+ expectedEvictionCount++ // aa will be evicted
+ assertCreated(cache, "dd", "created-dd")
+ assertSnapshot(cache, "bb", "created-bb", "cc", "created-cc", "dd", "created-dd")
+ expectedEvictionCount++ // bb will be evicted
+ assertCreated(cache, "aa", "created-aa")
+ assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa")
+ }
+
+ @Test
+ fun testCreateOnCacheMiss() {
+ val cache = newCreatingCache()
+ val created = cache["aa"]
+ assertEquals("created-aa", created)
+ }
+
+ @Test
+ fun testNoCreateOnCacheHit() {
+ val cache = newCreatingCache()
+ cache.put("aa", "put-aa")
+ assertEquals("put-aa", cache["aa"])
+ }
+
+ @Test
+ fun testConstructorDoesNotAllowZeroCacheSize() {
+ assertFailsWith<IllegalArgumentException> {
+ LruCache<String, String>(0)
+ }
+ }
+
+ @Test
+ fun testToString() {
+ val cache = LruCache<String, String>(3)
+ assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString())
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("d", "D")
+ cache["a"] // miss
+ cache["b"] // hit
+ cache["c"] // hit
+ cache["d"] // hit
+ cache["e"] // miss
+ assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString())
+ }
+
+ @Test
+ fun testEvictionWithSingletonCache() {
+ val cache = LruCache<String, String>(1)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ assertSnapshot(cache, "b", "B")
+ }
+
+ @Test
+ fun testEntryEvictedWhenFull() {
+ val log = ArrayList<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ assertContentEquals(emptyList(), log)
+ cache.put("d", "D")
+ assertContentEquals(listOf("a=A"), log)
+ }
+
+ /**
+ * Replacing the value for a key doesn't cause an eviction but it does bring
+ * the replaced entry to the front of the queue.
+ */
+ @Test
+ fun testPutCauseEviction() {
+ val log = ArrayList<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("b", "B2")
+ assertContentEquals(listOf("b=B>B2"), log)
+ assertSnapshot(cache, "a", "A", "c", "C", "b", "B2")
+ }
+
+ @Test
+ fun testCustomSizesImpactsSize() {
+ val cache =
+ object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int =
+ key.length + value.length
+ }
+ assertEquals(0, cache.size())
+ cache.put("a", "AA")
+ assertEquals(3, cache.size())
+ cache.put("b", "BBBB")
+ assertEquals(8, cache.size())
+ cache.put("a", "")
+ assertEquals(6, cache.size())
+ }
+
+ @Test
+ fun testEvictionWithCustomSizes() {
+ val cache =
+ object : LruCache<String, String>(4) {
+ override fun sizeOf(key: String, value: String): Int = value.length
+ }
+ cache.put("a", "AAAA")
+ assertSnapshot(cache, "a", "AAAA")
+ cache.put("b", "BBBB") // should evict a
+ assertSnapshot(cache, "b", "BBBB")
+ cache.put("c", "CC") // should evict b
+ assertSnapshot(cache, "c", "CC")
+ cache.put("d", "DD")
+ assertSnapshot(cache, "c", "CC", "d", "DD")
+ cache.put("e", "E") // should evict c
+ assertSnapshot(cache, "d", "DD", "e", "E")
+ cache.put("f", "F")
+ assertSnapshot(cache, "d", "DD", "e", "E", "f", "F")
+ cache.put("g", "G") // should evict d
+ assertSnapshot(cache, "e", "E", "f", "F", "g", "G")
+ cache.put("h", "H")
+ assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H")
+ cache.put("i", "III") // should evict e, f, and g
+ assertSnapshot(cache, "h", "H", "i", "III")
+ cache.put("j", "JJJ") // should evict h and i
+ assertSnapshot(cache, "j", "JJJ")
+ }
+
+ @Test
+ fun testEvictionThrowsWhenSizesAreInconsistent() {
+ val cache = object : LruCache<String, IntArray>(4) {
+ override fun sizeOf(key: String, value: IntArray): Int = value[0]
+ }
+ val a = intArrayOf(4)
+ cache.put("a", a)
+ // get the cache size out of sync
+ a[0] = 1
+ assertEquals(4, cache.size())
+ // evict something
+ assertFailsWith<IllegalStateException> {
+ cache.put("b", intArrayOf(2))
+ }
+ }
+
+ @Test
+ fun testEvictionThrowsWhenSizesAreNegative() {
+ val cache =
+ object : LruCache<String, String>(4) {
+ override fun sizeOf(key: String, value: String): Int = -1
+ }
+ assertFailsWith<IllegalStateException> {
+ cache.put("a", "A")
+ }
+ }
+
+ /**
+ * Naive caches evict at most one element at a time. This is problematic
+ * because evicting a small element may be insufficient to make room for a
+ * large element.
+ */
+ @Test
+ fun testDifferentElementSizes() {
+ val cache =
+ object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int = value.length
+ }
+ cache.put("a", "1")
+ cache.put("b", "12345678")
+ cache.put("c", "1")
+ assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1")
+ cache.put("d", "12345678") // should evict a and b
+ assertSnapshot(cache, "c", "1", "d", "12345678")
+ cache.put("e", "12345678") // should evict c and d
+ assertSnapshot(cache, "e", "12345678")
+ }
+
+ @Test
+ fun testEvictAll() {
+ val log = ArrayList<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.evictAll()
+ assertEquals(0, cache.size())
+ assertContentEquals(listOf("a=A", "b=B", "c=C"), log)
+ }
+
+ @Test
+ fun testEvictAllEvictsSizeZeroElements() {
+ val cache =
+ object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int = 0
+ }
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.evictAll()
+ assertSnapshot(cache)
+ }
+
+ @Test
+ fun testRemoveWithCustomSizes() {
+ val cache =
+ object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int = value.length
+ }
+ cache.put("a", "123456")
+ cache.put("b", "1234")
+ cache.remove("a")
+ assertEquals(4, cache.size())
+ }
+
+ @Test
+ fun testRemoveAbsentElement() {
+ val cache = LruCache<String, String>(10)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ assertNull(cache.remove("c"))
+ assertEquals(2, cache.size())
+ }
+
+ @Test
+ fun testRemoveCallsEntryRemoved() {
+ val log = ArrayList<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.remove("a")
+ assertContentEquals(listOf("a=A>null"), log)
+ }
+
+ @Test
+ fun testPutCallsEntryRemoved() {
+ val log = ArrayList<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("a", "A2")
+ assertContentEquals(listOf("a=A>A2"), log)
+ }
+
+ /**
+ * Test what happens when a value is added to the map while create is
+ * working. The map value should be returned by get(), and the created value
+ * should be released with entryRemoved().
+ */
+ @Test
+ fun testCreateWithConcurrentPut() {
+ val log = java.util.ArrayList<String>()
+ val cache =
+ object : LruCache<String, String>(3) {
+ override fun create(key: String): String {
+ put(key, "B")
+ return "A"
+ }
+
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ log.add("$key=$oldValue>$newValue")
+ }
+ }
+ assertEquals("B", cache["a"])
+ assertContentEquals(listOf("a=A>B"), log)
+ }
+
+ /**
+ * Test what happens when two creates happen concurrently. The result from
+ * the first create to return is returned by both gets. The other created
+ * values should be released with entryRemove().
+ */
+ @Test
+ fun testCreateWithConcurrentCreate() {
+ val log = ArrayList<String>()
+ val cache =
+ object : LruCache<String, Int>(3) {
+ var mCallCount = 0
+
+ override fun create(key: String): Int =
+ if (mCallCount++ == 0) {
+ assertEquals(2, get(key))
+ 1
+ } else {
+ 2
+ }
+
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: Int,
+ newValue: Int?
+ ) {
+ log.add("$key=$oldValue>$newValue")
+ }
+ }
+ assertEquals(2, cache["a"])
+ assertContentEquals(listOf("a=1>2"), log)
+ }
+
+ private fun newCreatingCache(): LruCache<String, String> =
+ object : LruCache<String, String>(3) {
+ override fun create(key: String): String? =
+ if (key.length > 1) "created-$key" else null
+ }
+
+ private fun newRemovalLogCache(log: MutableList<String>): LruCache<String, String> =
+ object : LruCache<String, String>(3) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ log += if (evicted) "$key=$oldValue" else "$key=$oldValue>$newValue"
+ }
+ }
+
+ private fun assertHit(cache: LruCache<String, String>, key: String, value: String) {
+ assertEquals(value, cache[key])
+ expectedHitCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertMiss(cache: LruCache<String, String>, key: String) {
+ assertEquals(null, cache[key])
+ expectedMissCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertCreated(cache: LruCache<String, String>, key: String, value: String) {
+ assertEquals(value, cache[key])
+ expectedMissCount++
+ expectedCreateCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertStatistics(cache: LruCache<*, *>) {
+ assertEquals(expectedCreateCount, cache.createCount(), "create count")
+ assertEquals(expectedPutCount, cache.putCount(), "put count")
+ assertEquals(expectedHitCount, cache.hitCount(), "hit count")
+ assertEquals(expectedMissCount, cache.missCount(), "miss count")
+ assertEquals(expectedEvictionCount, cache.evictionCount(), "eviction count")
+ }
+
+ private fun <T : Any> assertSnapshot(cache: LruCache<T, T>, vararg keysAndValues: T) {
+ val actualKeysAndValues = cache.snapshot().flatMap { (key, value) -> listOf(key, value) }
+ // assert using lists because order is important for LRUs
+ assertContentEquals(keysAndValues.asList(), actualKeysAndValues)
+ }
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
similarity index 60%
copy from health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
copy to collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
index e2d1362..678d982 100644
--- a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.health.platform.client.error
+package androidx.collection
/**
- * Exception thrown by Health Platform. Contains one of [ErrorCode]s and message with details on the
- * error.
+ * Native actual of internal utils for handling target differences in collection code.
*/
-open class HealthDataException(val errorCode: Int, errorMessage: String? = "") :
- Exception("$errorCode: $errorMessage")
+internal actual object CollectionPlatformUtils {
+
+ internal actual inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException {
+ return ArrayIndexOutOfBoundsException()
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index 143cc90..dd8c203 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.foundation.lazy.layout.ModifierLocalPinnableParent
import androidx.compose.foundation.lazy.layout.PinnableParent
import androidx.compose.foundation.lazy.layout.PinnableParent.PinnedItemsHandle
@@ -33,6 +35,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -57,6 +62,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class FocusableTest {
@@ -77,7 +83,7 @@
}
@Test
- fun focusableTest_defaultSemantics() {
+ fun focusable_defaultSemantics() {
rule.setContent {
Box {
BasicText(
@@ -93,7 +99,7 @@
}
@Test
- fun focusableTest_disabledSemantics() {
+ fun focusable_disabledSemantics() {
rule.setContent {
Box {
BasicText(
@@ -107,9 +113,8 @@
.assert(isNotFocusable())
}
- @ExperimentalComposeUiApi
@Test
- fun focusableTest_focusAcquire() {
+ fun focusable_focusAcquire() {
val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
rule.setContent {
Box {
@@ -147,9 +152,8 @@
.assertIsNotFocused()
}
- @ExperimentalComposeUiApi
@Test
- fun focusableTest_interactionSource() {
+ fun focusable_interactionSource() {
val interactionSource = MutableInteractionSource()
val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
@@ -208,7 +212,7 @@
}
@Test
- fun focusableTest_interactionSource_resetWhenDisposed() {
+ fun focusable_interactionSource_resetWhenDisposed() {
val interactionSource = MutableInteractionSource()
val focusRequester = FocusRequester()
var emitFocusableText by mutableStateOf(true)
@@ -298,16 +302,48 @@
}
@Test
- fun focusableText_testInspectorValue() {
+ fun focusable_inspectorValue() {
+ val modifier = Modifier.focusable() as InspectableValue
+ assertThat(modifier.nameFallback).isEqualTo("focusable")
+ assertThat(modifier.valueOverride).isNull()
+ assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+ .containsExactly(
+ "enabled",
+ "interactionSource"
+ )
+ }
+
+ @Test
+ fun focusable_requestsBringIntoView_whenFocused() {
+ val requestedRects = mutableListOf<Rect>()
+ val bringIntoViewResponder = object : BringIntoViewResponder {
+ override fun calculateRectForParent(localRect: Rect): Rect = localRect
+ override suspend fun bringChildIntoView(localRect: Rect) {
+ requestedRects += localRect
+ }
+ }
+ val focusRequester = FocusRequester()
+
rule.setContent {
- val modifier = Modifier.focusable() as InspectableValue
- assertThat(modifier.nameFallback).isEqualTo("focusable")
- assertThat(modifier.valueOverride).isNull()
- assertThat(modifier.inspectableElements.map { it.name }.asIterable())
- .containsExactly(
- "enabled",
- "interactionSource"
+ with(rule.density) {
+ Box(
+ Modifier
+ .bringIntoViewResponder(bringIntoViewResponder)
+ .focusRequester(focusRequester)
+ .focusable()
+ // Needs a non-zero size.
+ .size(1f.toDp())
)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(requestedRects).isEmpty()
+ focusRequester.requestFocus()
+ }
+
+ rule.runOnIdle {
+ assertThat(requestedRects).containsExactly(Rect(Offset.Zero, Size(1f, 1f)))
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
index 5d682d7..af5d456 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
@@ -25,7 +25,6 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -46,7 +45,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalFoundationApi::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class BringIntoViewRequesterViewIntegrationTest {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
index 70eba2f..5e07173 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
@@ -21,7 +21,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -41,7 +40,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class BringIntoViewResponderTest {
@@ -49,13 +48,13 @@
@get:Rule
val rule = createAndroidComposeRule<TestActivity>()
- fun Float.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
+ private fun Float.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
@Test
fun zeroSizedItem_zeroSizedParent_bringIntoView() {
// Arrange.
val bringIntoViewRequester = BringIntoViewRequester()
- lateinit var requestedRect: Rect
+ var requestedRect: Rect? = null
rule.setContent {
Box(
Modifier
@@ -65,7 +64,9 @@
}
// Act.
- runBlocking { bringIntoViewRequester.bringIntoView() }
+ runBlocking {
+ bringIntoViewRequester.bringIntoView()
+ }
// Assert.
rule.runOnIdle {
@@ -131,8 +132,8 @@
Modifier
.size(1f.toDp())
.fakeScrollable { requestedRect = it }
- .size(20f.toDp(), 10f.toDp())
.bringIntoViewRequester(bringIntoViewRequester)
+ .size(20f.toDp(), 10f.toDp())
)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
index dba0121..0b02623 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -90,6 +90,7 @@
keyboardHelper.waitForKeyboardVisibility(visible = true)
}
+ @FlakyTest(bugId = 229247491)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@Test
fun keyboardHiddenWhenFocusIsLost() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
index 2e23bf6..1f25b23 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation
import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.LayoutCoordinates
@@ -45,7 +44,6 @@
* Note that there may be some cases where the focused bounds change but the callback is _not_
* invoked, but the last [LayoutCoordinates] will always return the most up-to-date bounds.
*/
-@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalFoundationApi
fun Modifier.onFocusedBoundsChanged(onPositioned: (LayoutCoordinates?) -> Unit): Modifier =
composed(
@@ -57,7 +55,6 @@
remember(onPositioned) { FocusedBoundsObserverModifier(onPositioned) }
}
-@OptIn(ExperimentalFoundationApi::class)
private class FocusedBoundsObserverModifier(
private val handler: (LayoutCoordinates?) -> Unit
) : ModifierLocalConsumer,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
index 35aa72c..57efd71 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -19,7 +19,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnGloballyPositionedModifier
+import androidx.compose.ui.layout.OnPlacedModifier
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.modifierLocalOf
@@ -69,7 +69,7 @@
internal abstract class BringIntoViewChildModifier(
private val defaultParent: BringIntoViewParent
) : ModifierLocalConsumer,
- OnGloballyPositionedModifier {
+ OnPlacedModifier {
private var localParent: BringIntoViewParent? = null
@@ -87,7 +87,7 @@
}
}
- override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
layoutCoordinates = coordinates
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
index 9beb190..531f018 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
@@ -28,7 +28,6 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
@@ -61,7 +60,6 @@
* The text magnifier follows horizontal dragging exactly, but is vertically clamped to the current
* line, so when it changes lines we animate it.
*/
-@OptIn(ExperimentalComposeUiApi::class)
@Suppress("ModifierInspectorInfo")
internal fun Modifier.animatedSelectionMagnifier(
magnifierCenter: () -> Offset,
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index a211c10..735f25a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -237,6 +237,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -245,8 +247,8 @@
}
public final class IconButtonKt {
- method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
public final class IconKt {
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 78c1a14..98e9c45 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -313,6 +313,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -325,8 +327,8 @@
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index a211c10..735f25a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -237,6 +237,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -245,8 +247,8 @@
}
public final class IconButtonKt {
- method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
public final class IconKt {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 05448b4..5239032 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -96,10 +96,10 @@
import androidx.compose.material3.samples.TextButtonSample
import androidx.compose.material3.samples.TextFieldSample
import androidx.compose.material3.samples.TextFieldWithErrorState
-import androidx.compose.material3.samples.TextFieldWithHelperMessage
import androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction
import androidx.compose.material3.samples.TextFieldWithIcons
import androidx.compose.material3.samples.TextFieldWithPlaceholder
+import androidx.compose.material3.samples.TextFieldWithSupportingText
import androidx.compose.material3.samples.TextTabs
import androidx.compose.material3.samples.TriStateCheckboxSample
import androidx.compose.runtime.Composable
@@ -684,11 +684,11 @@
TextFieldWithErrorState()
},
Example(
- name = ::TextFieldWithHelperMessage.name,
+ name = ::TextFieldWithSupportingText.name,
description = TextFieldsExampleDescription,
sourceUrl = TextFieldsExampleSourceUrl
) {
- TextFieldWithHelperMessage()
+ TextFieldWithSupportingText()
},
Example(
name = ::PasswordTextField.name,
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
index 44ae71a..b9b8a42 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
@@ -61,7 +61,7 @@
@Composable
fun TextTabs() {
var state by remember { mutableStateOf(0) }
- val titles = listOf("TAB 1", "TAB 2", "TAB 3 WITH LOTS OF TEXT")
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
@@ -106,9 +106,9 @@
fun TextAndIconTabs() {
var state by remember { mutableStateOf(0) }
val titlesAndIcons = listOf(
- "TAB 1" to Icons.Filled.Favorite,
- "TAB 2" to Icons.Filled.Favorite,
- "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+ "Tab 1" to Icons.Filled.Favorite,
+ "Tab 2" to Icons.Filled.Favorite,
+ "Tab 3 with lots of text" to Icons.Filled.Favorite
)
Column {
TabRow(selectedTabIndex = state) {
@@ -133,9 +133,9 @@
fun LeadingIconTabs() {
var state by remember { mutableStateOf(0) }
val titlesAndIcons = listOf(
- "TAB" to Icons.Filled.Favorite,
- "TAB & ICON" to Icons.Filled.Favorite,
- "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+ "Tab" to Icons.Filled.Favorite,
+ "Tab & icon" to Icons.Filled.Favorite,
+ "Tab 3 with lots of text" to Icons.Filled.Favorite
)
Column {
TabRow(selectedTabIndex = state) {
@@ -160,16 +160,16 @@
fun ScrollingTextTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf(
- "TAB 1",
- "TAB 2",
- "TAB 3 WITH LOTS OF TEXT",
- "TAB 4",
- "TAB 5",
- "TAB 6 WITH LOTS OF TEXT",
- "TAB 7",
- "TAB 8",
- "TAB 9 WITH LOTS OF TEXT",
- "TAB 10"
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
)
Column {
ScrollableTabRow(selectedTabIndex = state) {
@@ -193,7 +193,7 @@
@Composable
fun FancyTabs() {
var state by remember { mutableStateOf(0) }
- val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
@@ -212,7 +212,7 @@
@Composable
fun FancyIndicatorTabs() {
var state by remember { mutableStateOf(0) }
- val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3")
// Reuse the default offset animation modifier, but use our own indicator
val indicator = @Composable { tabPositions: List<TabPosition> ->
@@ -244,7 +244,7 @@
@Composable
fun FancyIndicatorContainerTabs() {
var state by remember { mutableStateOf(0) }
- val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3")
val indicator = @Composable { tabPositions: List<TabPosition> ->
FancyAnimatedIndicator(tabPositions = tabPositions, selectedTabIndex = state)
@@ -275,16 +275,16 @@
fun ScrollingFancyIndicatorContainerTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf(
- "TAB 1",
- "TAB 2",
- "TAB 3 WITH LOTS OF TEXT",
- "TAB 4",
- "TAB 5",
- "TAB 6 WITH LOTS OF TEXT",
- "TAB 7",
- "TAB 8",
- "TAB 9 WITH LOTS OF TEXT",
- "TAB 10"
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
)
val indicator = @Composable { tabPositions: List<TabPosition> ->
FancyAnimatedIndicator(tabPositions = tabPositions, selectedTabIndex = state)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
index 4bd25b0..ec9f8e4 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
@@ -48,6 +48,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
@@ -117,33 +118,43 @@
@Sampled
@Composable
fun TextFieldWithErrorState() {
+ val errorMessage = "Email format is invalid"
var text by rememberSaveable { mutableStateOf("") }
var isError by rememberSaveable { mutableStateOf(false) }
fun validate(text: String) {
- isError = text.count() < 5
+ isError = !text.contains('@')
}
- TextField(
- value = text,
- onValueChange = {
- text = it
- isError = false
- },
- singleLine = true,
- label = { Text(if (isError) "Email*" else "Email") },
- isError = isError,
- keyboardActions = KeyboardActions { validate(text) },
- modifier = Modifier.semantics {
- // Provide localized description of the error
- if (isError) error("Email format is invalid.")
- }
- )
+ Column {
+ TextField(
+ value = text,
+ onValueChange = {
+ text = it
+ isError = false
+ },
+ singleLine = true,
+ label = { Text(if (isError) "Email*" else "Email") },
+ isError = isError,
+ keyboardActions = KeyboardActions { validate(text) },
+ modifier = Modifier.semantics {
+ // Provide localized description of the error
+ if (isError) error(errorMessage)
+ }
+ )
+ // Supporting text for error message.
+ Text(
+ text = errorMessage,
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(start = 16.dp, top = 4.dp).alpha(if (isError) 1f else 0f)
+ )
+ }
}
@Sampled
@Composable
-fun TextFieldWithHelperMessage() {
+fun TextFieldWithSupportingText() {
var text by rememberSaveable { mutableStateOf("") }
Column {
@@ -153,10 +164,10 @@
label = { Text("Label") }
)
Text(
- text = "Helper message",
+ text = "Supporting text",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(start = 16.dp)
+ modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 6b3e7e0..29c37c0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -16,6 +16,7 @@
package androidx.compose.material3
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -65,6 +66,8 @@
* [Interaction]s for this icon button. You can create and pass in your own remembered
* [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
* / behavior of this icon button in different states
+ * @param colors an [IconButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.iconButtonColors].
* @param content the content (icon) to be drawn inside the icon button. This is typically an
* [Icon].
*/
@@ -74,6 +77,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
content: @Composable () -> Unit
) {
Box(
@@ -81,6 +85,10 @@
modifier
.minimumTouchTargetSize()
.size(IconButtonTokens.StateLayerSize)
+ .background(
+ color = colors.containerColor(enabled).value,
+ IconButtonTokens.StateLayerShape.toShape()
+ )
.clickable(
onClick = onClick,
enabled = enabled,
@@ -93,13 +101,7 @@
),
contentAlignment = Alignment.Center
) {
- val contentColor =
- if (enabled) {
- IconButtonTokens.UnselectedIconColor.toColor()
- } else {
- IconButtonTokens.DisabledIconColor.toColor()
- .copy(alpha = IconButtonTokens.DisabledIconOpacity)
- }
+ val contentColor = colors.contentColor(enabled).value
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
}
}
@@ -128,6 +130,8 @@
* [Interaction]s for this icon button. You can create and pass in your own remembered
* [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
* / behavior of this icon button in different states
+ * @param colors an [IconToggleButtonColors] that will be used to resolve the colors used for this
+ * icon button in different states. See [IconButtonDefaults.iconToggleButtonColors].
* @param content the content (icon) to be drawn inside the icon button. This is typically an
* [Icon].
*/
@@ -138,6 +142,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
content: @Composable () -> Unit
) {
Box(
@@ -145,6 +150,10 @@
modifier
.minimumTouchTargetSize()
.size(IconButtonTokens.StateLayerSize)
+ .background(
+ color = colors.containerColor(enabled, checked).value,
+ IconButtonTokens.StateLayerShape.toShape()
+ )
.toggleable(
value = checked,
onValueChange = onCheckedChange,
@@ -158,12 +167,7 @@
),
contentAlignment = Alignment.Center
) {
- val contentColor = when {
- !enabled -> IconButtonTokens.DisabledIconColor.toColor()
- .copy(alpha = IconButtonTokens.DisabledIconOpacity)
- !checked -> IconButtonTokens.UnselectedIconColor.toColor()
- else -> IconButtonTokens.SelectedIconColor.toColor()
- }
+ val contentColor = colors.contentColor(enabled, checked).value
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
}
}
@@ -205,7 +209,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FilledIconButtonTokens.ContainerShape,
+ shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -267,7 +271,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FilledIconButtonTokens.ContainerShape,
+ shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -326,7 +330,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FilledIconButtonTokens.ContainerShape,
+ shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -391,7 +395,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FilledIconButtonTokens.ContainerShape,
+ shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -455,7 +459,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = OutlinedIconButtonTokens.ContainerShape,
+ shape: Shape = OutlinedIconButtonTokens.ContainerShape.toShape(),
border: BorderStroke? = IconButtonDefaults.outlinedIconButtonBorder(enabled),
colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonColors(),
content: @Composable () -> Unit
@@ -517,7 +521,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = OutlinedIconButtonTokens.ContainerShape,
+ shape: Shape = OutlinedIconButtonTokens.ContainerShape.toShape(),
border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
content: @Composable () -> Unit
@@ -607,6 +611,59 @@
object IconButtonDefaults {
/**
+ * Creates a [IconButtonColors] that represents the default colors used in a [IconButton].
+ *
+ * @param containerColor the container color of this icon button when enabled.
+ * @param contentColor the content color of this icon button when enabled.
+ * @param disabledContainerColor the container color of this icon button when not enabled.
+ * @param disabledContentColor the content color of this icon button when not enabled.
+ */
+ @Composable
+ fun iconButtonColors(
+ containerColor: Color = Color.Transparent,
+ contentColor: Color = IconButtonTokens.UnselectedIconColor.toColor(),
+ disabledContainerColor: Color = Color.Transparent,
+ disabledContentColor: Color = IconButtonTokens.DisabledIconColor.toColor()
+ .copy(alpha = IconButtonTokens.DisabledIconOpacity)
+ ): IconButtonColors =
+ DefaultIconButtonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ )
+
+ /**
+ * Creates a [IconToggleButtonColors] that represents the default colors used in a
+ * [IconToggleButton].
+ *
+ * @param containerColor the container color of this icon button when enabled.
+ * @param contentColor the content color of this icon button when enabled.
+ * @param disabledContainerColor the container color of this icon button when not enabled.
+ * @param disabledContentColor the content color of this icon button when not enabled.
+ * @param checkedContainerColor the container color of this icon button when checked.
+ * @param checkedContentColor the content color of this icon button when checked.
+ */
+ @Composable
+ fun iconToggleButtonColors(
+ containerColor: Color = Color.Transparent,
+ contentColor: Color = IconButtonTokens.UnselectedIconColor.toColor(),
+ disabledContainerColor: Color = Color.Transparent,
+ disabledContentColor: Color = IconButtonTokens.DisabledIconColor.toColor()
+ .copy(alpha = IconButtonTokens.DisabledIconOpacity),
+ checkedContainerColor: Color = Color.Transparent,
+ checkedContentColor: Color = IconButtonTokens.SelectedIconColor.toColor()
+ ): IconToggleButtonColors =
+ DefaultIconToggleButtonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ checkedContainerColor = checkedContainerColor,
+ checkedContentColor = checkedContentColor,
+ )
+
+ /**
* Creates a [IconButtonColors] that represents the default colors used in a [FilledIconButton].
*
* @param containerColor the container color of this icon button when enabled.
@@ -632,7 +689,7 @@
/**
* Creates a [IconToggleButtonColors] that represents the default colors used in a
- * toggleable [FilledIconButton].
+ * [FilledIconToggleButton].
*
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
@@ -664,7 +721,8 @@
)
/**
- * Creates a [IconButtonColors] that represents the default colors used in a [FilledIconButton].
+ * Creates a [IconButtonColors] that represents the default colors used in a
+ * [FilledTonalIconButton].
*
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
@@ -689,7 +747,7 @@
/**
* Creates a [IconToggleButtonColors] that represents the default colors used in a
- * [FilledIconButton].
+ * [FilledTonalIconToggleButton].
*
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 9759be1..262b681 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -94,7 +94,7 @@
*
* Additionally, you may provide additional message at the bottom:
*
- * @sample androidx.compose.material3.samples.TextFieldWithHelperMessage
+ * @sample androidx.compose.material3.samples.TextFieldWithSupportingText
*
* Password text field example:
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
index 7cc814e..f5fb952 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_93
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -22,7 +22,7 @@
internal object FilledIconButtonTokens {
val ContainerColor = ColorSchemeKeyTokens.Primary
- val ContainerShape = ShapeTokens.CornerFull
+ val ContainerShape = ShapeKeyTokens.CornerFull
val ContainerSize = 40.0.dp
val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
const val DisabledContainerOpacity = 0.12f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
index c82f8c3..cbb9d40 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_93
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -22,7 +22,7 @@
internal object FilledTonalIconButtonTokens {
val ContainerColor = ColorSchemeKeyTokens.SecondaryContainer
- val ContainerShape = ShapeTokens.CornerFull
+ val ContainerShape = ShapeKeyTokens.CornerFull
val ContainerSize = 40.0.dp
val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
const val DisabledContainerOpacity = 0.12f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
index afec766..6cc1bc1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_93
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
index c30f605..cff5ada 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_93
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -21,7 +21,7 @@
import androidx.compose.ui.unit.dp
internal object OutlinedIconButtonTokens {
- val ContainerShape = ShapeTokens.CornerFull
+ val ContainerShape = ShapeKeyTokens.CornerFull
val ContainerSize = 40.0.dp
val DisabledColor = ColorSchemeKeyTokens.OnSurface
const val DisabledOpacity = 0.38f
diff --git a/fragment/fragment/api/1.5.0-beta01.txt b/fragment/fragment/api/1.5.0-beta01.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/1.5.0-beta01.txt
+++ b/fragment/fragment/api/1.5.0-beta01.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt b/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
+++ b/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/restricted_1.5.0-beta01.txt b/fragment/fragment/api/restricted_1.5.0-beta01.txt
index 9e7cc35..7f0aa7c 100644
--- a/fragment/fragment/api/restricted_1.5.0-beta01.txt
+++ b/fragment/fragment/api/restricted_1.5.0-beta01.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 9e7cc35..7f0aa7c 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -6,6 +6,7 @@
ctor public DialogFragment(@LayoutRes int);
method public void dismiss();
method public void dismissAllowingStateLoss();
+ method @MainThread public void dismissNow();
method public android.app.Dialog? getDialog();
method public boolean getShowsDialog();
method @StyleRes public int getTheme();
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index c582362..8ae92e1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -66,6 +66,12 @@
}
}
+object FragmentDismissNow : Operation() {
+ override fun run(dialogFragment: DialogFragment) {
+ dialogFragment.dismissNow()
+ }
+}
+
@LargeTest
@RunWith(Parameterized::class)
class DialogFragmentDismissTest(
@@ -87,6 +93,8 @@
// Run the operation off the main thread
add(arrayOf(operation, false))
}
+ // dismissNow can only be run on the main thread
+ add(arrayOf(FragmentDismissNow, true))
}
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 92ec098..dfd3fbc 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -113,6 +113,27 @@
@UiThreadTest
@Test
+ fun testDialogFragmentDismissNow() {
+ val fragment = TestDialogFragment()
+ fragment.show(activityTestRule.activity.supportFragmentManager, null)
+ activityTestRule.runOnUiThread {
+ activityTestRule.activity.supportFragmentManager.executePendingTransactions()
+ }
+
+ val dialog = fragment.dialog
+ assertWithMessage("Dialog was not being shown")
+ .that(dialog?.isShowing)
+ .isTrue()
+
+ fragment.dismissNow()
+
+ assertWithMessage("Dialog should be removed")
+ .that(dialog?.isShowing)
+ .isFalse()
+ }
+
+ @UiThreadTest
+ @Test
fun testDialogFragmentDismissAllowingStateLoss() {
val viewModelStore = ViewModelStore()
val fc = activityTestRule.startupFragmentController(viewModelStore)
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
index 1477da4..7cf9874 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
@@ -307,7 +307,16 @@
* the fragment.
*/
public void dismiss() {
- dismissInternal(false, false);
+ dismissInternal(false, false, false);
+ }
+
+ /**
+ * Version of {@link #dismiss()} that uses {@link FragmentTransaction#commitNow()}.
+ * See linked documentation for further details.
+ */
+ @MainThread
+ public void dismissNow() {
+ dismissInternal(false, false, true);
}
/**
@@ -317,10 +326,10 @@
* documentation for further details.
*/
public void dismissAllowingStateLoss() {
- dismissInternal(true, false);
+ dismissInternal(true, false, false);
}
- private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) {
+ private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss, boolean immediate) {
if (mDismissed) {
return;
}
@@ -346,14 +355,22 @@
}
mViewDestroyed = true;
if (mBackStackId >= 0) {
- getParentFragmentManager().popBackStack(mBackStackId,
- FragmentManager.POP_BACK_STACK_INCLUSIVE, allowStateLoss);
+ if (immediate) {
+ getParentFragmentManager().popBackStackImmediate(mBackStackId,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ } else {
+ getParentFragmentManager().popBackStack(mBackStackId,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE, allowStateLoss);
+ }
mBackStackId = -1;
} else {
FragmentTransaction ft = getParentFragmentManager().beginTransaction();
ft.setReorderingAllowed(true);
ft.remove(this);
- if (allowStateLoss) {
+ // allowStateLoss and immediate should not both be true
+ if (immediate) {
+ ft.commitNow();
+ } else if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
@@ -633,7 +650,7 @@
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "onDismiss called for DialogFragment " + this);
}
- dismissInternal(true, true);
+ dismissInternal(true, true, false);
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 5c424fe..f598eae 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -105,6 +105,8 @@
*/
public abstract class FragmentManager implements FragmentResultOwner {
static final String SAVED_STATE_TAG = "android:support:fragments";
+ static final String FRAGMENT_MANAGER_STATE_TAG = "state";
+ static final String RESULT_NAME_PREFIX = "result_";
private static boolean DEBUG = false;
/** @hide */
@@ -2347,10 +2349,13 @@
throwException(new IllegalStateException("You cannot use saveAllState when your "
+ "FragmentHostCallback implements SavedStateRegistryOwner."));
}
- return saveAllStateInternal();
+ Bundle savedState = saveAllStateInternal();
+ return savedState.isEmpty() ? null : savedState;
}
- Parcelable saveAllStateInternal() {
+ @NonNull
+ Bundle saveAllStateInternal() {
+ Bundle bundle = new Bundle();
// Make sure all pending operations have now been executed to get
// our state update-to-date.
forcePostponedTransactions();
@@ -2365,46 +2370,50 @@
// And grab all FragmentState objects
ArrayList<FragmentState> savedState = mFragmentStore.getAllSavedState();
-
if (savedState.isEmpty()) {
- if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
- return null;
- }
+ if (isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(TAG, "saveAllState: no fragments!");
+ }
+ } else {
+ // Build list of currently added fragments.
+ ArrayList<String> added = mFragmentStore.saveAddedFragments();
- // Build list of currently added fragments.
- ArrayList<String> added = mFragmentStore.saveAddedFragments();
-
- // Now save back stack.
- BackStackRecordState[] backStack = null;
- if (mBackStack != null) {
- int size = mBackStack.size();
- if (size > 0) {
- backStack = new BackStackRecordState[size];
- for (int i = 0; i < size; i++) {
- backStack[i] = new BackStackRecordState(mBackStack.get(i));
- if (isLoggingEnabled(Log.VERBOSE)) {
- Log.v(TAG, "saveAllState: adding back stack #" + i
- + ": " + mBackStack.get(i));
+ // Now save back stack.
+ BackStackRecordState[] backStack = null;
+ if (mBackStack != null) {
+ int size = mBackStack.size();
+ if (size > 0) {
+ backStack = new BackStackRecordState[size];
+ for (int i = 0; i < size; i++) {
+ backStack[i] = new BackStackRecordState(mBackStack.get(i));
+ if (isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(TAG, "saveAllState: adding back stack #" + i
+ + ": " + mBackStack.get(i));
+ }
}
}
}
+
+ FragmentManagerState fms = new FragmentManagerState();
+ fms.mSavedState = savedState;
+ fms.mActive = active;
+ fms.mAdded = added;
+ fms.mBackStack = backStack;
+ fms.mBackStackIndex = mBackStackIndex.get();
+ if (mPrimaryNav != null) {
+ fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
+ }
+ fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
+ fms.mBackStackStates.addAll(mBackStackStates.values());
+ fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
+ bundle.putParcelable(FRAGMENT_MANAGER_STATE_TAG, fms);
+
+ for (String resultName : mResults.keySet()) {
+ bundle.putBundle(RESULT_NAME_PREFIX + resultName, mResults.get(resultName));
+ }
}
- FragmentManagerState fms = new FragmentManagerState();
- fms.mSavedState = savedState;
- fms.mActive = active;
- fms.mAdded = added;
- fms.mBackStack = backStack;
- fms.mBackStackIndex = mBackStackIndex.get();
- if (mPrimaryNav != null) {
- fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
- }
- fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
- fms.mBackStackStates.addAll(mBackStackStates.values());
- fms.mResultKeys.addAll(mResults.keySet());
- fms.mResults.addAll(mResults.values());
- fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
- return fms;
+ return bundle;
}
@SuppressWarnings("deprecation")
@@ -2425,11 +2434,25 @@
restoreSaveStateInternal(state);
}
+ @SuppressWarnings("deprecation")
void restoreSaveStateInternal(@Nullable Parcelable state) {
// If there is no saved state at all, then there's nothing else to do
if (state == null) return;
- FragmentManagerState fms = (FragmentManagerState) state;
- if (fms.mSavedState == null) return;
+ Bundle bundle = (Bundle) state;
+
+ for (String bundleKey : bundle.keySet()) {
+ if (bundleKey.startsWith(RESULT_NAME_PREFIX)) {
+ Bundle savedResult = bundle.getBundle(bundleKey);
+ if (savedResult != null) {
+ savedResult.setClassLoader(mHost.getContext().getClassLoader());
+ String resultKey = bundleKey.substring(RESULT_NAME_PREFIX.length());
+ mResults.put(resultKey, savedResult);
+ }
+ }
+ }
+
+ FragmentManagerState fms = bundle.getParcelable(FRAGMENT_MANAGER_STATE_TAG);
+ if (fms == null || fms.mSavedState == null) return;
// Restore the saved state of all fragments
mFragmentStore.restoreSaveState(fms.mSavedState);
@@ -2526,14 +2549,6 @@
}
}
- ArrayList<String> savedResultKeys = fms.mResultKeys;
- if (savedResultKeys != null) {
- for (int i = 0; i < savedResultKeys.size(); i++) {
- Bundle savedResult = fms.mResults.get(i);
- savedResult.setClassLoader(mHost.getContext().getClassLoader());
- mResults.put(savedResultKeys.get(i), savedResult);
- }
- }
mLaunchedFragments = new ArrayDeque<>(fms.mLaunchedFragments);
}
@@ -2614,20 +2629,14 @@
SavedStateRegistry registry =
((SavedStateRegistryOwner) mHost).getSavedStateRegistry();
registry.registerSavedStateProvider(SAVED_STATE_TAG, () -> {
- Bundle outState = new Bundle();
- Parcelable p = saveAllStateInternal();
- if (p != null) {
- outState.putParcelable(SAVED_STATE_TAG, p);
- }
- return outState;
+ return saveAllStateInternal();
}
);
Bundle savedInstanceState = registry
.consumeRestoredStateForKey(SAVED_STATE_TAG);
if (savedInstanceState != null) {
- Parcelable p = savedInstanceState.getParcelable(SAVED_STATE_TAG);
- restoreSaveStateInternal(p);
+ restoreSaveStateInternal(savedInstanceState);
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
index a90909a..117e31c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
@@ -17,7 +17,6 @@
package androidx.fragment.app;
import android.annotation.SuppressLint;
-import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,8 +32,6 @@
String mPrimaryNavActiveWho = null;
ArrayList<String> mBackStackStateKeys = new ArrayList<>();
ArrayList<BackStackState> mBackStackStates = new ArrayList<>();
- ArrayList<String> mResultKeys = new ArrayList<>();
- ArrayList<Bundle> mResults = new ArrayList<>();
ArrayList<FragmentManager.LaunchedFragmentInfo> mLaunchedFragments;
public FragmentManagerState() {
@@ -49,8 +46,6 @@
mPrimaryNavActiveWho = in.readString();
mBackStackStateKeys = in.createStringArrayList();
mBackStackStates = in.createTypedArrayList(BackStackState.CREATOR);
- mResultKeys = in.createStringArrayList();
- mResults = in.createTypedArrayList(Bundle.CREATOR);
mLaunchedFragments = in.createTypedArrayList(FragmentManager.LaunchedFragmentInfo.CREATOR);
}
@@ -69,8 +64,6 @@
dest.writeString(mPrimaryNavActiveWho);
dest.writeStringList(mBackStackStateKeys);
dest.writeTypedList(mBackStackStates);
- dest.writeStringList(mResultKeys);
- dest.writeTypedList(mResults);
dest.writeTypedList(mLaunchedFragments);
}
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f426772..431a244 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -13,6 +13,7 @@
<trust group="^com[.]android($|([.].*))" regex="true"/> <!-- b/215430394 -->
<trust group="com.google.testing.platform"/> <!-- b/215430394 -->
<trust group="com.google.android.gms"/> <!-- b/215442095 -->
+ <trust file=".*kotlin-native-prebuilt-macos-.*" regex="true"/> <!-- b/228184608 -->
</trusted-artifacts>
<trusted-keys>
<trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava" name="guava"/>
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
index 0cb371e..29f46ee 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
@@ -33,7 +33,7 @@
import androidx.health.data.client.records.Distance
import androidx.health.data.client.records.ElevationGained
import androidx.health.data.client.records.FloorsClimbed
-import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
import androidx.health.data.client.records.HeartRateVariabilityRmssd
import androidx.health.data.client.records.HeartRateVariabilityS
@@ -52,6 +52,7 @@
import androidx.health.data.client.records.OvulationTest
import androidx.health.data.client.records.OxygenSaturation
import androidx.health.data.client.records.Power
+import androidx.health.data.client.records.Record
import androidx.health.data.client.records.Repetitions
import androidx.health.data.client.records.RespiratoryRate
import androidx.health.data.client.records.RestingHeartRate
@@ -67,6 +68,7 @@
import androidx.health.data.client.records.WaistCircumference
import androidx.health.data.client.records.Weight
import androidx.health.data.client.records.WheelchairPushes
+import kotlin.reflect.KClass
private val ALL_RECORDS_TYPES =
setOf(
@@ -88,7 +90,7 @@
Distance::class,
ElevationGained::class,
FloorsClimbed::class,
- HeartRate::class,
+ HeartRateSeries::class,
HeartRateVariabilityDifferentialIndex::class,
HeartRateVariabilityRmssd::class,
HeartRateVariabilityS::class,
@@ -124,4 +126,5 @@
Weight::class,
)
-val RECORDS_TYPE_NAME_MAP = ALL_RECORDS_TYPES.associateBy { it.simpleName!! }
+val RECORDS_TYPE_NAME_MAP: Map<String, KClass<out Record>> =
+ ALL_RECORDS_TYPES.associateBy { it.simpleName!! }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
index 7eed099..bfefa93 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -34,6 +34,7 @@
import androidx.health.data.client.records.ElevationGained
import androidx.health.data.client.records.FloorsClimbed
import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
import androidx.health.data.client.records.HeartRateVariabilityRmssd
import androidx.health.data.client.records.HeartRateVariabilityS
@@ -69,11 +70,11 @@
import androidx.health.data.client.records.Weight
import androidx.health.data.client.records.WheelchairPushes
import androidx.health.platform.client.proto.DataProto
-import java.lang.RuntimeException
+import java.time.Instant
/** Converts public API object into internal proto for ipc. */
-fun toRecord(proto: DataProto.DataPoint): Record {
- return with(proto) {
+fun toRecord(proto: DataProto.DataPoint): Record =
+ with(proto) {
when (dataType.name) {
"BasalBodyTemperature" ->
BasalBodyTemperature(
@@ -163,12 +164,20 @@
zoneOffset = zoneOffset,
metadata = metadata
)
- "HeartRate" ->
- HeartRate(
- beatsPerMinute = getLong("bpm"),
- time = time,
- zoneOffset = zoneOffset,
- metadata = metadata
+ "HeartRateSeries" ->
+ HeartRateSeries(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples =
+ seriesValuesList.map { value ->
+ HeartRate(
+ time = Instant.ofEpochMilli(value.instantTimeMillis),
+ beatsPerMinute = value.getLong("bpm"),
+ )
+ },
+ metadata = metadata,
)
"Height" ->
Height(
@@ -533,4 +542,3 @@
else -> throw RuntimeException("Unknown data type ${dataType.name}")
}
}
-}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
index 4ad70d8..bbd4a24 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -19,52 +19,60 @@
import androidx.health.data.client.metadata.Device
import androidx.health.data.client.metadata.Metadata
import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.proto.DataProto.DataPointOrBuilder
+import androidx.health.platform.client.proto.DataProto.SeriesValueOrBuilder
import java.time.Instant
import java.time.ZoneOffset
/** Internal helper functions to convert proto to records. */
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.startTime: Instant
get() = Instant.ofEpochMilli(startTimeMillis)
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.endTime: Instant
get() = Instant.ofEpochMilli(endTimeMillis)
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.time: Instant
get() = Instant.ofEpochMilli(instantTimeMillis)
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.startZoneOffset: ZoneOffset?
get() =
if (hasStartZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(startZoneOffsetSeconds) else null
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.endZoneOffset: ZoneOffset?
get() = if (hasEndZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(endZoneOffsetSeconds) else null
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // HealthDataClientImplSafe to use for deserialization
internal val DataProto.DataPoint.zoneOffset: ZoneOffset?
get() = if (hasZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(zoneOffsetSeconds) else null
-internal fun DataProto.DataPoint.getLong(key: String, defaultVal: Long = 0): Long {
- return valuesMap[key]?.longVal ?: defaultVal
-}
+internal fun DataPointOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
+ valuesMap[key]?.longVal ?: defaultVal
-internal fun DataProto.DataPoint.getDouble(key: String, defaultVal: Double = 0.0): Double {
- return valuesMap[key]?.doubleVal ?: defaultVal
-}
+internal fun DataPointOrBuilder.getDouble(key: String, defaultVal: Double = 0.0): Double =
+ valuesMap[key]?.doubleVal ?: defaultVal
-internal fun DataProto.DataPoint.getString(key: String): String? {
- return valuesMap[key]?.stringVal
-}
+internal fun DataPointOrBuilder.getString(key: String): String? = valuesMap[key]?.stringVal
-internal fun DataProto.DataPoint.getEnum(key: String): String? {
+internal fun DataPointOrBuilder.getEnum(key: String): String? {
return valuesMap[key]?.enumVal
}
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+internal fun SeriesValueOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
+ valuesMap[key]?.longVal ?: defaultVal
+
+internal fun SeriesValueOrBuilder.getDouble(key: String, defaultVal: Double = 0.0): Double =
+ valuesMap[key]?.doubleVal ?: defaultVal
+
+internal fun SeriesValueOrBuilder.getString(key: String): String? = valuesMap[key]?.stringVal
+
+internal fun SeriesValueOrBuilder.getEnum(key: String): String? = valuesMap[key]?.enumVal
+
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
internal val DataProto.DataPoint.metadata: Metadata
get() =
Metadata(
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
index 2b78f78..0d59e11 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
@@ -33,7 +33,7 @@
import androidx.health.data.client.records.Distance
import androidx.health.data.client.records.ElevationGained
import androidx.health.data.client.records.FloorsClimbed
-import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
import androidx.health.data.client.records.HeartRateVariabilityRmssd
import androidx.health.data.client.records.HeartRateVariabilityS
@@ -56,6 +56,7 @@
import androidx.health.data.client.records.Repetitions
import androidx.health.data.client.records.RespiratoryRate
import androidx.health.data.client.records.RestingHeartRate
+import androidx.health.data.client.records.SeriesRecord
import androidx.health.data.client.records.SexualActivity
import androidx.health.data.client.records.SleepSession
import androidx.health.data.client.records.SleepStage
@@ -69,12 +70,10 @@
import androidx.health.data.client.records.Weight
import androidx.health.data.client.records.WheelchairPushes
import androidx.health.platform.client.proto.DataProto
-import java.lang.RuntimeException
/** Converts public API object into internal proto for ipc. */
-@SuppressWarnings("NewApi") // Safe to use with java8 desugar
-fun Record.toProto(): DataProto.DataPoint {
- return when (this) {
+fun Record.toProto(): DataProto.DataPoint =
+ when (this) {
is BasalBodyTemperature ->
instantaneousProto()
.setDataType(protoDataType("BasalBodyTemperature"))
@@ -153,11 +152,13 @@
.setDataType(protoDataType("CyclingPedalingCadence"))
.apply { putValues("rpm", doubleVal(revolutionsPerMinute)) }
.build()
- is HeartRate ->
- instantaneousProto()
- .setDataType(protoDataType("HeartRate"))
- .apply { putValues("bpm", longVal(beatsPerMinute)) }
- .build()
+ is HeartRateSeries ->
+ toProto(dataTypeName = "HeartRateSeries") { sample ->
+ DataProto.SeriesValue.newBuilder()
+ .putValues("bpm", longVal(sample.beatsPerMinute))
+ .setInstantTimeMillis(sample.time.toEpochMilli())
+ .build()
+ }
is Height ->
instantaneousProto()
.setDataType(protoDataType("Height"))
@@ -508,4 +509,16 @@
.build()
else -> throw RuntimeException("Unsupported yet!")
}
-}
+
+private fun <T : Any> SeriesRecord<T>.toProto(
+ dataTypeName: String,
+ getSeriesValue: (sample: T) -> DataProto.SeriesValue,
+): DataProto.DataPoint =
+ intervalProto()
+ .setDataType(protoDataType(dataTypeName = dataTypeName))
+ .apply {
+ for (sample in samples) {
+ addSeriesValues(getSeriesValue(sample))
+ }
+ }
+ .build()
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
index a882c33..bb2802b 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
@@ -25,7 +25,7 @@
internal fun protoDataType(dataTypeName: String): DataProto.DataType =
DataProto.DataType.newBuilder().setName(dataTypeName).build()
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
internal fun InstantaneousRecord.instantaneousProto(): DataProto.DataPoint.Builder {
val builder =
DataProto.DataPoint.newBuilder()
@@ -35,7 +35,7 @@
return builder
}
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
internal fun IntervalRecord.intervalProto(): DataProto.DataPoint.Builder {
val builder =
DataProto.DataPoint.newBuilder()
@@ -47,7 +47,7 @@
return builder
}
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
private fun DataProto.DataPoint.Builder.setMetadata(metadata: Metadata) = apply {
metadata.uid?.let { setUid(it) }
if (metadata.dataOrigin.packageName.isNotEmpty()) {
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
index bd49bfd..820fd3a 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
@@ -80,32 +80,32 @@
companion object {
/** Metric identifier to retrieve average systolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_SYSTOLIC_AVG: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "avg", "systolic")
/** Metric identifier to retrieve minimum systolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_SYSTOLIC_MIN: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "min", "systolic")
/** Metric identifier to retrieve maximum systolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_SYSTOLIC_MAX: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "max", "systolic")
/** Metric identifier to retrieve average diastolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_DIASTOLIC_AVG: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "avg", "diastolic")
/** Metric identifier to retrieve minimum diastolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_DIASTOLIC_MIN: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "min", "diastolic")
/** Metric identifier to retrieve maximum diastolic from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val BLOOD_PRESSURE_DIASTOLIC_MAX: DoubleAggregateMetric =
DoubleAggregateMetric("BloodPressure", "max", "diastolic")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
index 8cbb3ea..2fe3c53 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
@@ -65,7 +65,7 @@
internal companion object {
/** Metric identifier to retrieve total distance from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val DISTANCE_TOTAL: DoubleAggregateMetric =
DoubleAggregateMetric("Distance", "total", "distance")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
index f5886d14..f0731db8 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
@@ -56,7 +56,7 @@
internal companion object {
/** Metric identifier to retrieve total elevation gained from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val ELEVATION_TOTAL: DoubleAggregateMetric =
DoubleAggregateMetric("ElevationGained", "total", "elevation")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
index 585f62f..b6c5785d 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
@@ -58,7 +58,7 @@
companion object {
/** Metric identifier to retrieve total floors climbed from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val DISTANCE_TOTAL: DoubleAggregateMetric =
DoubleAggregateMetric("FloorsClimbed", "total", "floors")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt
deleted file mode 100644
index 562d5e5..0000000
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 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.health.data.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.data.client.aggregate.LongAggregateMetric
-import androidx.health.data.client.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/** Captures the user's heart rate. Each record represents a single instantaneous measurement. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRate(
- /** Heart beats per minute. Required field. Validation range: 1-300. */
- public val beatsPerMinute: Long,
- override val time: Instant,
- override val zoneOffset: ZoneOffset?,
- override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is HeartRate) return false
-
- if (beatsPerMinute != other.beatsPerMinute) return false
- if (time != other.time) return false
- if (zoneOffset != other.zoneOffset) return false
- if (metadata != other.metadata) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = 0
- result = 31 * result + beatsPerMinute.hashCode()
- result = 31 * result + time.hashCode()
- result = 31 * result + (zoneOffset?.hashCode() ?: 0)
- result = 31 * result + metadata.hashCode()
- return result
- }
-
- companion object {
- /** Metric identifier to retrieve average heart rate from [AggregateDataRow]. */
- @JvmStatic
- val HEART_RATE_BPM_AVG: LongAggregateMetric = LongAggregateMetric("HeartRate", "avg", "bpm")
-
- /** Metric identifier to retrieve minimum heart rate from [AggregateDataRow]. */
- @JvmStatic
- val HEART_RATE_BPM_MIN: LongAggregateMetric = LongAggregateMetric("HeartRate", "min", "bpm")
-
- /** Metric identifier to retrieve maximum heart rate from [AggregateDataRow]. */
- @JvmStatic
- val HEART_RATE_BPM_MAX: LongAggregateMetric = LongAggregateMetric("HeartRate", "max", "bpm")
- }
-}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt
new file mode 100644
index 0000000..e810ff5b
--- /dev/null
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 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.health.data.client.records
+
+import androidx.annotation.RestrictTo
+import androidx.health.data.client.aggregate.LongAggregateMetric
+import androidx.health.data.client.metadata.Metadata
+import java.time.Instant
+import java.time.ZoneOffset
+
+/** Captures the user's heart rate. Each record represents a series of measurements. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // Will be made public after API reviews
+public class HeartRateSeries(
+ override val startTime: Instant,
+ override val startZoneOffset: ZoneOffset?,
+ override val endTime: Instant,
+ override val endZoneOffset: ZoneOffset?,
+ override val samples: List<HeartRate>,
+ override val metadata: Metadata = Metadata.EMPTY,
+) : SeriesRecord<HeartRate> {
+
+ /*
+ * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HeartRateSeries) return false
+
+ if (startTime != other.startTime) return false
+ if (startZoneOffset != other.startZoneOffset) return false
+ if (endTime != other.endTime) return false
+ if (endZoneOffset != other.endZoneOffset) return false
+ if (samples != other.samples) return false
+ if (metadata != other.metadata) return false
+
+ return true
+ }
+
+ /*
+ * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+ */
+ override fun hashCode(): Int {
+ var result = startTime.hashCode()
+ result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + endTime.hashCode()
+ result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + samples.hashCode()
+ result = 31 * result + metadata.hashCode()
+ return result
+ }
+
+ internal companion object {
+ /** Metric identifier to retrieve average heart rate from [AggregateDataRow]. */
+ @JvmField
+ internal val BPM_AVG: LongAggregateMetric = LongAggregateMetric("HeartRate", "avg", "bpm")
+
+ /** Metric identifier to retrieve minimum heart rate from [AggregateDataRow]. */
+ @JvmField
+ internal val BPM_MIN: LongAggregateMetric = LongAggregateMetric("HeartRate", "min", "bpm")
+
+ /** Metric identifier to retrieve maximum heart rate from [AggregateDataRow]. */
+ @JvmField
+ internal val BPM_MAX: LongAggregateMetric = LongAggregateMetric("HeartRate", "max", "bpm")
+ }
+}
+
+/**
+ * Represents a single measurement of the heart rate.
+ *
+ * @param beatsPerMinute Heart beats per minute. Validation range: 1-300.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // Will be made public after API reviews
+public class HeartRate(
+ val time: Instant,
+ val beatsPerMinute: Long,
+) {
+
+ /*
+ * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HeartRate) return false
+
+ if (time != other.time) return false
+ if (beatsPerMinute != other.beatsPerMinute) return false
+
+ return true
+ }
+
+ /*
+ * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+ */
+ override fun hashCode(): Int {
+ var result = time.hashCode()
+ result = 31 * result + beatsPerMinute.hashCode()
+ return result
+ }
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
index 8d9b426..36f3f4c 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
@@ -51,17 +51,17 @@
internal companion object {
/** Metric identifier to retrieve average height from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val HEIGHT_AVG: DoubleAggregateMetric =
DoubleAggregateMetric("Height", "avg", "height")
/** Metric identifier to retrieve minimum height from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val HEIGHT_MIN: DoubleAggregateMetric =
DoubleAggregateMetric("Height", "min", "height")
/** Metric identifier to retrieve maximum height from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val HEIGHT_MAX: DoubleAggregateMetric =
DoubleAggregateMetric("Height", "max", "height")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
similarity index 67%
rename from health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
rename to health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
index e2d1362..e3c3a81 100644
--- a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.health.platform.client.error
+package androidx.health.data.client.records
-/**
- * Exception thrown by Health Platform. Contains one of [ErrorCode]s and message with details on the
- * error.
- */
-open class HealthDataException(val errorCode: Int, errorMessage: String? = "") :
- Exception("$errorCode: $errorMessage")
+/** A record that contains a series of measurements. */
+// Consider exposing this one and other similar interfaces!
+internal interface SeriesRecord<out T : Any> : IntervalRecord {
+
+ val samples: List<T>
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
index f9e17dc..e0b0077 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
@@ -57,15 +57,15 @@
companion object {
/** Metric identifier to retrieve average speed from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val SPEED_AVG: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "avg", "speed")
/** Metric identifier to retrieve minimum speed from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val SPEED_MIN: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "min", "speed")
/** Metric identifier to retrieve maximum speed from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val SPEED_MAX: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "max", "speed")
}
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
index 18376eb..1b0c7c5 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
@@ -64,7 +64,7 @@
internal companion object {
/** Metric identifier to retrieve total steps count from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val STEPS_COUNT_TOTAL: LongAggregateMetric =
LongAggregateMetric("Steps", "total", "count")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
index dce8edd..87ef22f 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
@@ -61,7 +61,7 @@
companion object {
/** Metric identifier to retrieve total energy from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
val ENERGY_BURNED_TOTAL: DoubleAggregateMetric =
DoubleAggregateMetric("TotalEnergyBurned", "total", "energy")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
index 244fb36..a07b814 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
@@ -51,17 +51,17 @@
internal companion object {
/** Metric identifier to retrieve average weight from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val WEIGHT_AVG: DoubleAggregateMetric =
DoubleAggregateMetric("Weight", "avg", "weight")
/** Metric identifier to retrieve minimum weight from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val WEIGHT_MIN: DoubleAggregateMetric =
DoubleAggregateMetric("Weight", "min", "weight")
/** Metric identifier to retrieve maximum weight from [AggregateDataRow]. */
- @JvmStatic
+ @JvmField
internal val WEIGHT_MAX: DoubleAggregateMetric =
DoubleAggregateMetric("Weight", "max", "weight")
}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt b/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
index e9e1d48..8bd1300 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
@@ -146,4 +146,25 @@
internal fun isOpenEnded(): Boolean =
(localStartTime == null || localEndTime == null) && (startTime == null || endTime == null)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TimeRangeFilter) return false
+
+ if (startTime != other.startTime) return false
+ if (endTime != other.endTime) return false
+ if (localStartTime != other.localStartTime) return false
+ if (localEndTime != other.localEndTime) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = 0
+ result = 31 * result + (startTime?.hashCode() ?: 0)
+ result = 31 * result + (endTime?.hashCode() ?: 0)
+ result = 31 * result + (localStartTime?.hashCode() ?: 0)
+ result = 31 * result + (localEndTime?.hashCode() ?: 0)
+ return result
+ }
}
diff --git a/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
index 1f8ea4c..665c57e 100644
--- a/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -42,6 +42,7 @@
import androidx.health.data.client.records.ElevationGained
import androidx.health.data.client.records.FloorsClimbed
import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
import androidx.health.data.client.records.HeartRateVariabilityRmssd
import androidx.health.data.client.records.HeartRateVariabilityS
@@ -272,13 +273,29 @@
}
@Test
- fun testHeartRate() {
+ fun testHeartRateSeries() {
val data =
- HeartRate(
- beatsPerMinute = 1,
- time = START_TIME,
- zoneOffset = END_ZONE_OFFSET,
- metadata = TEST_METADATA
+ HeartRateSeries(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ samples =
+ listOf(
+ HeartRate(
+ time = START_TIME,
+ beatsPerMinute = 100L,
+ ),
+ HeartRate(
+ time = START_TIME,
+ beatsPerMinute = 110L,
+ ),
+ HeartRate(
+ time = START_TIME,
+ beatsPerMinute = 120L,
+ ),
+ ),
+ metadata = TEST_METADATA,
)
assertThat(toRecord(data.toProto())).isEqualTo(data)
diff --git a/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt b/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
index 893e039..353f050 100644
--- a/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
+++ b/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
@@ -18,7 +18,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import java.time.Instant
import java.time.LocalDateTime
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,4 +66,49 @@
endTime = LocalDateTime.parse("2021-02-01T02:00:00")
)
}
+
+ @Test
+ fun equals() {
+ assertEquals(
+ TimeRangeFilter.between(
+ startTime = Instant.ofEpochMilli(1234L),
+ endTime = Instant.ofEpochMilli(5679L),
+ ),
+ TimeRangeFilter.between(
+ startTime = Instant.ofEpochMilli(1234L),
+ endTime = Instant.ofEpochMilli(5679L),
+ )
+ )
+ assertEquals(
+ TimeRangeFilter.between(
+ startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+ endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+ ),
+ TimeRangeFilter.between(
+ startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+ endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+ )
+ )
+
+ assertNotEquals(
+ TimeRangeFilter.between(
+ startTime = Instant.ofEpochMilli(1234L),
+ endTime = Instant.ofEpochMilli(5678L),
+ ),
+ TimeRangeFilter.between(
+ startTime = Instant.ofEpochMilli(1234L),
+ endTime = Instant.ofEpochMilli(5679L),
+ )
+ )
+ assertNotEquals(
+ TimeRangeFilter.between(
+ startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+ endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+ ),
+ TimeRangeFilter.between(
+ startTime = LocalDateTime.parse("2021-02-01T01:30:00"),
+ endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+ )
+ )
+ }
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
index 47151f7..e2285f3 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
@@ -52,7 +52,7 @@
if (savedStateRegistry.getSavedStateProvider(SAVED_STATE_KEY) == null) {
val provider = SavedStateHandlesProvider(savedStateRegistry, this)
savedStateRegistry.registerSavedStateProvider(SAVED_STATE_KEY, provider)
- savedStateRegistry.runOnNextRecreation(SavedStateHandleAttacher::class.java)
+ lifecycle.addObserver(SavedStateHandleAttacher(provider))
}
}
@@ -184,11 +184,15 @@
}
// it reconnects existent SavedStateHandles to SavedStateRegistryOwner when it is recreated
-internal class SavedStateHandleAttacher : SavedStateRegistry.AutoRecreated {
- override fun onRecreated(owner: SavedStateRegistryOwner) {
- // if SavedStateHandlesProvider wasn't added previously, there's nothing for us to do
- val provider = owner.savedStateRegistry
- .getSavedStateProvider(SAVED_STATE_KEY) as? SavedStateHandlesProvider ?: return
+internal class SavedStateHandleAttacher(
+ private val provider: SavedStateHandlesProvider
+) : LifecycleEventObserver {
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ check(event == Lifecycle.Event.ON_CREATE) {
+ "Next event must be ON_CREATE, it was $event"
+ }
+ source.lifecycle.removeObserver(this)
// onRecreated() is called after the Lifecycle reaches CREATED, so we
// eagerly restore the state as part of this call to ensure it consumed
// even if no ViewModels are actually created during this cycle of the Lifecycle
diff --git a/room/room-paging-guava/build.gradle b/room/room-paging-guava/build.gradle
index 5db83cb..8f23959 100644
--- a/room/room-paging-guava/build.gradle
+++ b/room/room-paging-guava/build.gradle
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import androidx.build.LibraryType
import androidx.build.Publish
@@ -23,9 +24,49 @@
id("org.jetbrains.kotlin.android")
}
+// If set to `true`, we'll use KSP instead of KAPT.
+// Note that the CI does not run tests with KSP yet so this is only for local usage.
+// Once variants are properly supported by both ksp and AndroidX, we'll add support for this.
+// (b/153917176)
+def useKsp = project.properties.getOrDefault("useKsp", "false").toBoolean()
+if (useKsp) {
+ apply plugin: "com.google.devtools.ksp"
+} else {
+ apply plugin: "kotlin-kapt"
+}
+
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation(project(":room:room-paging"))
+ implementation(project(":room:room-guava"))
+ implementation(projectOrArtifact(":paging:paging-guava"))
+
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.testExtJunitKtx)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.kotlinTestJunit)
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.kotlinCoroutinesGuava)
+ androidTestImplementation("androidx.arch.core:core-testing:2.0.1")
+ androidTestImplementation(project(":internal-testutils-common"))
+ // depend on the shadowed version so that it tests with the shipped artifact
+ // this is a temporary attribute until KSP and AndroidX plugin supports variants.
+ if(useKsp) {
+ kspAndroidTest(
+ project(path: ":room:room-compiler", configuration: "shadowAndImplementation")
+ )
+ } else {
+ kaptAndroidTest(
+ project(path: ":room:room-compiler", configuration: "shadowAndImplementation")
+ )
+ }
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
+ }
}
androidx {
diff --git a/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
new file mode 100644
index 0000000..a9fd319
--- /dev/null
+++ b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
@@ -0,0 +1,886 @@
+/*
+ * 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.room.paging.guava
+
+import android.database.Cursor
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.paging.LoadType
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.FutureCallback
+import com.google.common.util.concurrent.Futures.addCallback
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.LinkedList
+import java.util.concurrent.CancellationException
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.guava.await
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val tableName: String = "TestItem"
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LimitOffsetListenableFuturePagingSourceTest {
+
+ @JvmField
+ @Rule
+ val countingTaskExecutorRule = CountingTaskExecutorRule()
+
+ @Test
+ fun initialEmptyLoad_futureIsDone() = setupAndRun { db ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ runTest {
+ val listenableFuture = pagingSource.refresh()
+ val page = listenableFuture.await() as LoadResult.Page
+
+ assertThat(page.data).isEmpty()
+ assertTrue(listenableFuture.isDone)
+ }
+ }
+
+ @Test
+ fun initialLoad_returnsFutureImmediately() =
+ setupAndRunWithTestExecutor { db, _, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ val listenableFuture = pagingSource.refresh()
+ // ensure future is returned even as its result is still pending
+ assertFalse(listenableFuture.isDone)
+ assertThat(pagingSource.itemCount.get()).isEqualTo(-1)
+
+ // now execute db queries
+ transactionExecutor.executeAll() // initial transactional refresh load
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(0, 15)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun append_returnsFutureImmediately() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.append(key = 20)
+ // ensure future is returned even as its result is still pending
+ assertFalse(listenableFuture.isDone)
+
+ // load append
+ queryExecutor.executeNext()
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(20, 25)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun prepend_returnsFutureImmediately() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.prepend(key = 20)
+ // ensure future is returned even as its result is still pending
+ assertFalse(listenableFuture.isDone)
+
+ // load prepend
+ queryExecutor.executeNext()
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(15, 20)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun append_returnsInvalid() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.append(key = 50)
+
+ pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
+ assertTrue(pagingSource.invalid)
+
+ // executing the load Callable
+ queryExecutor.executeNext()
+
+ val result = listenableFuture.await()
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun prepend_returnsInvalid() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.prepend(key = 50)
+
+ pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
+ assertTrue(pagingSource.invalid)
+
+ // executing the load Callable
+ queryExecutor.executeNext()
+
+ val result = listenableFuture.await()
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun refresh_consecutively() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ val listenableFuture1 = pagingSource.refresh(key = 10)
+ val listenableFuture2 = pagingSource2.refresh(key = 15)
+
+ // check that first Future completes first. If the first future didn't complete first,
+ // this await() would not return.
+ val page1 = listenableFuture1.await() as LoadResult.Page
+ assertThat(page1.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(10, 25)
+ )
+
+ val page2 = listenableFuture2.await() as LoadResult.Page
+ assertThat(page2.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(15, 30)
+ )
+ }
+
+ @Test
+ fun append_consecutively() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+ val listenableFuture1 = pagingSource.append(key = 10)
+ val listenableFuture2 = pagingSource.append(key = 15)
+
+ // both appends should be queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+ // run next append in queue and make sure it is the first append
+ queryExecutor.executeNext()
+ val page1 = listenableFuture1.await() as LoadResult.Page
+ assertThat(page1.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(10, 15)
+ )
+
+ // now run the second append
+ queryExecutor.executeNext()
+ val page2 = listenableFuture2.await() as LoadResult.Page
+ assertThat(page2.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(15, 20)
+ )
+ }
+
+ @Test
+ fun prepend_consecutively() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+ val listenableFuture1 = pagingSource.prepend(key = 30)
+ val listenableFuture2 = pagingSource.prepend(key = 25)
+
+ // both prepends should be queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+ // run next prepend in queue and make sure it is the first prepend
+ queryExecutor.executeNext()
+ val page1 = listenableFuture1.await() as LoadResult.Page
+ assertThat(page1.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(25, 30)
+ )
+
+ // now run the second prepend
+ queryExecutor.executeNext()
+ val page2 = listenableFuture2.await() as LoadResult.Page
+ assertThat(page2.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(20, 25)
+ )
+ }
+
+ @Test
+ fun refresh_onSuccess() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ val listenableFuture = pagingSource.refresh(key = 30)
+
+ var onSuccessReceived = false
+ val callbackExecutor = TestExecutor()
+ listenableFuture.onSuccess(callbackExecutor) { result ->
+ val page = result as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(30, 45)
+ )
+ onSuccessReceived = true
+ }
+
+ // wait until Room db's refresh load is complete
+ countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+ assertTrue(listenableFuture.isDone)
+
+ callbackExecutor.executeAll()
+
+ // make sure onSuccess callback was executed
+ assertTrue(onSuccessReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun append_onSuccess() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.append(key = 20)
+ // ensure future is returned even as its result is still pending
+ assertFalse(listenableFuture.isDone)
+
+ var onSuccessReceived = false
+ val callbackExecutor = TestExecutor()
+ listenableFuture.onSuccess(callbackExecutor) { result ->
+ val page = result as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(20, 25)
+ )
+ onSuccessReceived = true
+ }
+ // let room db complete load
+ countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+ callbackExecutor.executeAll()
+
+ // make sure onSuccess callback was executed
+ assertTrue(onSuccessReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun prepend_onSuccess() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.prepend(key = 40)
+ // ensure future is returned even as its result is still pending
+ assertFalse(listenableFuture.isDone)
+
+ var onSuccessReceived = false
+ val callbackExecutor = TestExecutor()
+ listenableFuture.onSuccess(callbackExecutor) { result ->
+ val page = result as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(35, 40)
+ )
+ onSuccessReceived = true
+ }
+ // let room db complete load
+ countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+ callbackExecutor.executeAll()
+
+ // make sure onSuccess callback was executed
+ assertTrue(onSuccessReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun refresh_awaitThrowsCancellationException() =
+ setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ val listenableFuture = pagingSource.refresh(key = 50)
+ // the initial runInTransaction load
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+ listenableFuture.cancel(true)
+
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+ transactionExecutor.executeNext() // initial load
+ queryExecutor.executeNext() // refreshVersionsAsync from the end runInTransaction
+
+ // await() should throw after cancellation
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // executors should be idle
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+ assertTrue(listenableFuture.isDone)
+ // even though initial refresh load is cancelled, the paging source itself
+ // is NOT invalidated
+ assertFalse(pagingSource.invalid)
+ }
+
+ @Test
+ fun append_awaitThrowsCancellationException() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the append first
+ val listenableFuture = pagingSource.append(key = 20)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ listenableFuture.cancel(true)
+ queryExecutor.executeNext()
+
+ // await() should throw after cancellation
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // although query was executed, it should not complete due to the cancellation signal.
+ // If query was completed, paging source would call refreshVersionsAsync manually
+ // and queuedSize() would be 1 instead of 0 with InvalidationTracker queued up
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ }
+
+ @Test
+ fun prepend_awaitThrowsCancellationException() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the prepend first
+ val listenableFuture = pagingSource.prepend(key = 30)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ listenableFuture.cancel(true)
+ queryExecutor.executeNext()
+
+ // await() should throw after cancellation
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // although query was executed, it should not complete due to the cancellation signal.
+ // If query was completed, paging source would call refreshVersionsAsync manually
+ // and queuedSize() would be 1 instead of 0 with InvalidationTracker queued up
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+ }
+
+ @Test
+ fun refresh_canceledFutureRunsOnFailureCallback() =
+ setupAndRunWithTestExecutor { db, _, transactionExecutor ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+ val listenableFuture = pagingSource.refresh(key = 30)
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+ val callbackExecutor = TestExecutor()
+ var onFailureReceived = false
+ listenableFuture.onFailure(callbackExecutor) { throwable ->
+ assertThat(throwable).isInstanceOf(CancellationException::class.java)
+ onFailureReceived = true
+ }
+
+ // now cancel future and execute the refresh load. The refresh should not complete.
+ listenableFuture.cancel(true)
+ transactionExecutor.executeNext()
+ assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+
+ callbackExecutor.executeAll()
+
+ // make sure onFailure callback was executed
+ assertTrue(onFailureReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun append_canceledFutureRunsOnFailureCallback() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the append first
+ val listenableFuture = pagingSource.append(key = 20)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ val callbackExecutor = TestExecutor()
+ var onFailureReceived = false
+ listenableFuture.onFailure(callbackExecutor) { throwable ->
+ assertThat(throwable).isInstanceOf(CancellationException::class.java)
+ onFailureReceived = true
+ }
+
+ // now cancel future and execute the append load. The append should not complete.
+ listenableFuture.cancel(true)
+ queryExecutor.executeNext()
+ // if load was erroneously completed, InvalidationTracker would be queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+ callbackExecutor.executeAll()
+
+ // make sure onFailure callback was executed
+ assertTrue(onFailureReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun prepend_canceledFutureRunsOnFailureCallback() =
+ setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ // queue up the prepend first
+ val listenableFuture = pagingSource.prepend(key = 30)
+ assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+ val callbackExecutor = TestExecutor()
+ var onFailureReceived = false
+ listenableFuture.onFailure(callbackExecutor) { throwable ->
+ assertThat(throwable).isInstanceOf(CancellationException::class.java)
+ onFailureReceived = true
+ }
+
+ // now cancel future and execute the prepend which should not complete.
+ listenableFuture.cancel(true)
+ queryExecutor.executeNext()
+ // if load was erroneously completed, InvalidationTracker would be queued
+ assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+ callbackExecutor.executeAll()
+
+ // make sure onFailure callback was executed
+ assertTrue(onFailureReceived)
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun refresh_AfterCancellation() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.prepend(key = 50)
+
+ listenableFuture.cancel(true)
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+
+ // new gen after query from previous gen was cancelled
+ val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+ val listenableFuture2 = pagingSource2.refresh()
+ val result = listenableFuture2.await() as LoadResult.Page
+
+ // the new generation should load as usual
+ assertThat(result.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(0, 15)
+ )
+ }
+
+ @Test
+ fun appendAgain_afterFutureCanceled() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.append(key = 30)
+
+ listenableFuture.cancel(true)
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+ assertTrue(listenableFuture.isDone)
+ assertFalse(pagingSource.invalid)
+
+ val listenableFuture2 = pagingSource.append(key = 30)
+
+ val result = listenableFuture2.await() as LoadResult.Page
+ assertThat(result.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(30, 35)
+ )
+ assertTrue(listenableFuture2.isDone)
+ }
+
+ @Test
+ fun prependAgain_afterFutureCanceled() = setupAndRun { db ->
+ db.dao.addAllItems(ITEMS_LIST)
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ pagingSource.itemCount.set(100) // bypass check for initial load
+
+ val listenableFuture = pagingSource.prepend(key = 30)
+
+ listenableFuture.cancel(true)
+ assertFailsWith<CancellationException> {
+ listenableFuture.await()
+ }
+ assertFalse(pagingSource.invalid)
+ assertTrue(listenableFuture.isDone)
+
+ val listenableFuture2 = pagingSource.prepend(key = 30)
+
+ val result = listenableFuture2.await() as LoadResult.Page
+ assertThat(result.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(25, 30)
+ )
+ assertTrue(listenableFuture2.isDone)
+ }
+
+ @Test
+ fun test_jumpSupport() = setupAndRun { db ->
+ val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+ assertTrue(pagingSource.jumpingSupported)
+ }
+
+ @Test
+ fun refresh_secondaryConstructor() = setupAndRun { db ->
+ val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+ db = db,
+ supportSQLiteQuery = SimpleSQLiteQuery(
+ "SELECT * FROM $tableName ORDER BY id ASC"
+ )
+ ) {
+ override fun convertRows(cursor: Cursor): List<TestItem> {
+ return convertRowsHelper(cursor)
+ }
+ }
+
+ db.dao.addAllItems(ITEMS_LIST)
+ val listenableFuture = pagingSource.refresh()
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(0, 15)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun append_secondaryConstructor() = setupAndRun { db ->
+ val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+ db = db,
+ supportSQLiteQuery = SimpleSQLiteQuery(
+ "SELECT * FROM $tableName ORDER BY id ASC"
+ )
+ ) {
+ override fun convertRows(cursor: Cursor): List<TestItem> {
+ return convertRowsHelper(cursor)
+ }
+ }
+
+ db.dao.addAllItems(ITEMS_LIST)
+ pagingSource.itemCount.set(100)
+ val listenableFuture = pagingSource.append(key = 50)
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(50, 55)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ @Test
+ fun prepend_secondaryConstructor() = setupAndRun { db ->
+ val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+ db = db,
+ supportSQLiteQuery = SimpleSQLiteQuery(
+ "SELECT * FROM $tableName ORDER BY id ASC"
+ )
+ ) {
+ override fun convertRows(cursor: Cursor): List<TestItem> {
+ return convertRowsHelper(cursor)
+ }
+ }
+
+ db.dao.addAllItems(ITEMS_LIST)
+ pagingSource.itemCount.set(100)
+ val listenableFuture = pagingSource.prepend(key = 50)
+
+ val page = listenableFuture.await() as LoadResult.Page
+ assertThat(page.data).containsExactlyElementsIn(
+ ITEMS_LIST.subList(45, 50)
+ )
+ assertTrue(listenableFuture.isDone)
+ }
+
+ private fun setupAndRun(
+ test: suspend (LimitOffsetTestDb) -> Unit
+ ) {
+ val db = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(),
+ LimitOffsetTestDb::class.java
+ ).build()
+
+ runTest {
+ test(db)
+ }
+ tearDown(db)
+ }
+
+ private fun setupAndRunWithTestExecutor(
+ test: suspend (LimitOffsetTestDb, TestExecutor, TestExecutor) -> Unit
+ ) {
+ val queryExecutor = TestExecutor()
+ val transactionExecutor = TestExecutor()
+ val db = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(),
+ LimitOffsetTestDb::class.java
+ )
+ .setTransactionExecutor(transactionExecutor)
+ .setQueryExecutor(queryExecutor)
+ .build()
+
+ runTest {
+ db.dao.addAllItems(ITEMS_LIST)
+ queryExecutor.executeNext() // InvalidationTracker from the addAllItems
+ test(db, queryExecutor, transactionExecutor)
+ }
+ tearDown(db)
+ }
+
+ private fun tearDown(db: LimitOffsetTestDb) {
+ if (db.isOpen) db.close()
+ countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+ assertThat(countingTaskExecutorRule.isIdle).isTrue()
+ }
+}
+
+private class LimitOffsetListenableFuturePagingSourceImpl(
+ db: RoomDatabase,
+ queryString: String = "SELECT * FROM $tableName ORDER BY id ASC",
+) : LimitOffsetListenableFuturePagingSource<TestItem>(
+ sourceQuery = RoomSQLiteQuery.acquire(
+ queryString,
+ 0
+ ),
+ db = db,
+ tables = arrayOf(tableName)
+) {
+ override fun convertRows(cursor: Cursor): List<TestItem> {
+ return convertRowsHelper(cursor)
+ }
+}
+
+private fun convertRowsHelper(cursor: Cursor): List<TestItem> {
+ val cursorIndexOfId = getColumnIndexOrThrow(cursor, "id")
+ val data = mutableListOf<TestItem>()
+ while (cursor.moveToNext()) {
+ val tmpId = cursor.getInt(cursorIndexOfId)
+ data.add(TestItem(tmpId))
+ }
+ return data
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun TestExecutor.executeNext() {
+ val tasks = javaClass.getDeclaredField("mTasks").let {
+ it.isAccessible = true
+ it.get(this)
+ } as LinkedList<Runnable>
+
+ if (!tasks.isEmpty()) {
+ val task = tasks.poll()
+ task?.run()
+ }
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun TestExecutor.queuedSize(): Int {
+ val tasks = javaClass.getDeclaredField("mTasks").let {
+ it.isAccessible = true
+ it.get(this)
+ } as LinkedList<Runnable>
+
+ return tasks.size
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.refresh(
+ key: Int? = null,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+ return loadFuture(
+ createLoadParam(
+ loadType = LoadType.REFRESH,
+ key = key,
+ )
+ )
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.append(
+ key: Int? = -1,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+ return loadFuture(
+ createLoadParam(
+ loadType = LoadType.APPEND,
+ key = key,
+ )
+ )
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.prepend(
+ key: Int? = -1,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+ return loadFuture(
+ createLoadParam(
+ loadType = LoadType.PREPEND,
+ key = key,
+ )
+ )
+}
+
+private val CONFIG = PagingConfig(
+ pageSize = 5,
+ enablePlaceholders = true,
+ initialLoadSize = 15
+)
+
+private val ITEMS_LIST = createItemsForDb(0, 100)
+
+private fun createItemsForDb(startId: Int, count: Int): List<TestItem> {
+ return List(count) {
+ TestItem(
+ id = it + startId,
+ )
+ }
+}
+
+private fun createLoadParam(
+ loadType: LoadType,
+ key: Int? = null,
+ initialLoadSize: Int = CONFIG.initialLoadSize,
+ pageSize: Int = CONFIG.pageSize,
+ placeholdersEnabled: Boolean = CONFIG.enablePlaceholders
+): PagingSource.LoadParams<Int> {
+ return when (loadType) {
+ LoadType.REFRESH -> {
+ PagingSource.LoadParams.Refresh(
+ key = key,
+ loadSize = initialLoadSize,
+ placeholdersEnabled = placeholdersEnabled
+ )
+ }
+ LoadType.APPEND -> {
+ PagingSource.LoadParams.Append(
+ key = key ?: -1,
+ loadSize = pageSize,
+ placeholdersEnabled = placeholdersEnabled
+ )
+ }
+ LoadType.PREPEND -> {
+ PagingSource.LoadParams.Prepend(
+ key = key ?: -1,
+ loadSize = pageSize,
+ placeholdersEnabled = placeholdersEnabled
+ )
+ }
+ }
+}
+
+private fun ListenableFuture<LoadResult<Int, TestItem>>.onSuccess(
+ executor: Executor,
+ onSuccessCallback: (LoadResult<Int, TestItem>?) -> Unit,
+) {
+ addCallback(
+ this,
+ object : FutureCallback<LoadResult<Int, TestItem>> {
+ override fun onSuccess(result: LoadResult<Int, TestItem>?) {
+ onSuccessCallback(result)
+ }
+
+ override fun onFailure(t: Throwable) {
+ assertWithMessage("Expected onSuccess callback instead of onFailure, " +
+ "received ${t.localizedMessage}").fail()
+ }
+ },
+ executor
+ )
+}
+
+private fun ListenableFuture<LoadResult<Int, TestItem>>.onFailure(
+ executor: Executor,
+ onFailureCallback: (Throwable) -> Unit,
+) {
+ addCallback(
+ this,
+ object : FutureCallback<LoadResult<Int, TestItem>> {
+ override fun onSuccess(result: LoadResult<Int, TestItem>?) {
+ assertWithMessage("Expected onFailure callback instead of onSuccess, " +
+ "received result $result").fail()
+ }
+
+ override fun onFailure(t: Throwable) {
+ onFailureCallback(t)
+ }
+ },
+ executor
+ )
+}
+
+@Database(entities = [TestItem::class], version = 1, exportSchema = false)
+abstract class LimitOffsetTestDb : RoomDatabase() {
+ abstract val dao: TestItemDao
+}
+
+@Entity(tableName = "TestItem")
+data class TestItem(
+ @PrimaryKey val id: Int,
+ val value: String = "item $id"
+)
+
+@Dao
+interface TestItemDao {
+ @Insert
+ fun addAllItems(testItems: List<TestItem>)
+}
diff --git a/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt b/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
new file mode 100644
index 0000000..fb2d209
--- /dev/null
+++ b/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.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.room.paging.guava
+
+import android.database.Cursor
+import androidx.annotation.NonNull
+import androidx.annotation.RestrictTo
+import androidx.paging.ListenableFuturePagingSource
+import androidx.paging.PagingState
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.guava.GuavaRoom.createListenableFuture
+import androidx.room.paging.util.INITIAL_ITEM_COUNT
+import androidx.room.paging.util.INVALID
+import androidx.room.paging.util.ThreadSafeInvalidationObserver
+import androidx.room.paging.util.getClippedRefreshKey
+import androidx.room.paging.util.queryDatabase
+import androidx.room.paging.util.queryItemCount
+import androidx.room.util.createCancellationSignal
+import androidx.sqlite.db.SupportSQLiteQuery
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.Callable
+import java.util.concurrent.atomic.AtomicInteger
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class LimitOffsetListenableFuturePagingSource<Value : Any>(
+ private val sourceQuery: RoomSQLiteQuery,
+ private val db: RoomDatabase,
+ vararg tables: String
+) : ListenableFuturePagingSource<Int, Value>() {
+
+ constructor(
+ supportSQLiteQuery: SupportSQLiteQuery,
+ db: RoomDatabase,
+ vararg tables: String,
+ ) : this(
+ sourceQuery = RoomSQLiteQuery.copyFrom(supportSQLiteQuery),
+ db = db,
+ tables = tables,
+ )
+
+ // internal for testing visibility
+ internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
+ private val observer = ThreadSafeInvalidationObserver(tables = tables, ::invalidate)
+
+ /**
+ * Returns a [ListenableFuture] immediately before loading from the database completes
+ *
+ * If PagingSource is invalidated while the [ListenableFuture] is still pending, the
+ * invalidation will cancel the load() coroutine that calls await() on this future. The
+ * cancellation of await() will transitively cancel this future as well.
+ */
+ override fun loadFuture(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Value>> {
+ observer.registerIfNecessary(db)
+ val tempCount = itemCount.get()
+ return if (tempCount == INITIAL_ITEM_COUNT) {
+ initialLoad(params)
+ } else {
+ nonInitialLoad(params, tempCount)
+ }
+ }
+
+ /**
+ * For refresh loads
+ *
+ * To guarantee a valid initial load, it is run in transaction so that db writes cannot
+ * happen in between [queryItemCount] and [queryDatabase] to ensure a valid [itemCount].
+ * [itemCount] must be correct in order to calculate correct LIMIT and OFFSET for the query.
+ *
+ *
+ * However, the database load will be canceled via the cancellation signal if the future
+ * it returned has been canceled before it has completed.
+ */
+ private fun initialLoad(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Value>> {
+ val cancellationSignal = createCancellationSignal()
+ val loadCallable = Callable<LoadResult<Int, Value>> {
+ db.runInTransaction(
+ Callable {
+ val tempCount = queryItemCount(sourceQuery, db)
+ itemCount.set(tempCount)
+ queryDatabase(
+ params, sourceQuery, db, tempCount, cancellationSignal, ::convertRows
+ )
+ }
+ )
+ }
+
+ return createListenableFuture(
+ db,
+ true,
+ loadCallable,
+ sourceQuery,
+ false,
+ cancellationSignal,
+ )
+ }
+
+ /**
+ * For append and prepend loads
+ *
+ * The cancellation signal cancels room database operation if its running, or cancels it
+ * the moment it starts. The signal is triggered when the future is canceled.
+ */
+ private fun nonInitialLoad(
+ params: LoadParams<Int>,
+ tempCount: Int
+ ): ListenableFuture<LoadResult<Int, Value>> {
+ val cancellationSignal = createCancellationSignal()
+ val loadCallable = Callable<LoadResult<Int, Value>> {
+ val result = queryDatabase(
+ params, sourceQuery, db, tempCount, cancellationSignal, ::convertRows
+ )
+ db.invalidationTracker.refreshVersionsAsync()
+ @Suppress("UNCHECKED_CAST")
+ if (invalid) INVALID as LoadResult.Invalid<Int, Value> else result
+ }
+
+ return createListenableFuture(
+ db,
+ false,
+ loadCallable,
+ sourceQuery,
+ false,
+ cancellationSignal
+ )
+ }
+
+ @NonNull
+ protected abstract fun convertRows(cursor: Cursor): List<Value>
+
+ override val jumpingSupported: Boolean
+ get() = true
+
+ override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
+ return state.getClippedRefreshKey()
+ }
+}
\ No newline at end of file
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index 8e6600f..84367bf 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -45,7 +45,7 @@
dependencies {
api(libs.kotlinStdlib)
- implementation(project(":room:room-runtime"))
+ api(project(":room:room-runtime"))
implementation(project(":room:room-ktx"))
api("androidx.paging:paging-common:3.1.0")
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index 023e5b6..6634131 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -24,6 +24,8 @@
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
import androidx.room.getQueryDispatcher
+import androidx.room.paging.util.INITIAL_ITEM_COUNT
+import androidx.room.paging.util.INVALID
import androidx.room.paging.util.ThreadSafeInvalidationObserver
import androidx.room.paging.util.getClippedRefreshKey
import androidx.room.paging.util.queryDatabase
@@ -40,9 +42,6 @@
* for Pager's consumption. Registers observers on tables lazily and automatically invalidates
* itself when data changes.
*/
-
-private val INVALID = PagingSource.LoadResult.Invalid<Any, Any>()
-
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class LimitOffsetPagingSource<Value : Any>(
private val sourceQuery: RoomSQLiteQuery,
@@ -60,7 +59,7 @@
tables = tables,
)
- internal val itemCount: AtomicInteger = AtomicInteger(-1)
+ internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
private val observer = ThreadSafeInvalidationObserver(
tables = tables,
@@ -72,7 +71,7 @@
observer.registerIfNecessary(db)
val tempCount = itemCount.get()
// if itemCount is < 0, then it is initial load
- if (tempCount < 0) {
+ if (tempCount == INITIAL_ITEM_COUNT) {
initialLoad(params)
} else {
nonInitialLoad(params, tempCount)
@@ -94,7 +93,13 @@
return db.withTransaction {
val tempCount = queryItemCount(sourceQuery, db)
itemCount.set(tempCount)
- queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
+ queryDatabase(
+ params = params,
+ sourceQuery = sourceQuery,
+ db = db,
+ itemCount = tempCount,
+ convertRows = ::convertRows
+ )
}
}
@@ -102,8 +107,14 @@
params: LoadParams<Int>,
tempCount: Int,
): LoadResult<Int, Value> {
- val loadResult = queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
- // manually check if database has been updated. If so, the observers's
+ val loadResult = queryDatabase(
+ params = params,
+ sourceQuery = sourceQuery,
+ db = db,
+ itemCount = tempCount,
+ convertRows = ::convertRows
+ )
+ // manually check if database has been updated. If so, the observer's
// invalidation callback will invalidate this paging source
db.invalidationTracker.refreshVersionsSync()
@Suppress("UNCHECKED_CAST")
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
index 2511fc6..1722c11 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -18,6 +18,7 @@
package androidx.room.paging.util
import android.database.Cursor
+import android.os.CancellationSignal
import androidx.annotation.RestrictTo
import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadParams
@@ -30,6 +31,18 @@
import androidx.room.RoomSQLiteQuery
/**
+ * A [LoadResult] that can be returned to trigger a new generation of PagingSource
+ *
+ * Any loaded data or queued loads prior to returning INVALID will be discarded
+ */
+val INVALID = LoadResult.Invalid<Any, Any>()
+
+/**
+ * The default itemCount value
+ */
+const val INITIAL_ITEM_COUNT = -1
+
+/**
* Calculates query limit based on LoadType.
*
* Prepend: If requested loadSize is larger than available number of items to prepend, it will
@@ -102,6 +115,8 @@
* @param itemCount the db row count, triggers a new PagingSource generation if itemCount changes,
* i.e. items are added / removed
*
+ * @param cancellationSignal the signal to cancel the query if the query hasn't yet completed
+ *
* @param convertRows the function to iterate data with provided [Cursor] to return List<Value>
*/
fun <Value : Any> queryDatabase(
@@ -109,6 +124,7 @@
sourceQuery: RoomSQLiteQuery,
db: RoomDatabase,
itemCount: Int,
+ cancellationSignal: CancellationSignal? = null,
convertRows: (Cursor) -> List<Value>,
): LoadResult<Int, Value> {
val key = params.key ?: 0
@@ -121,7 +137,7 @@
sourceQuery.argCount
)
sqLiteQuery.copyArgumentsFrom(sourceQuery)
- val cursor = db.query(sqLiteQuery)
+ val cursor = db.query(sqLiteQuery, cancellationSignal)
val data: List<Value>
try {
data = convertRows(cursor)
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -15,18 +15,15 @@
}
@androidx.compose.runtime.Stable public interface ArcPaddingValues {
- method public float calculateEndPadding();
- method public float calculateInnerPadding();
- method public float calculateOuterPadding();
- method public float calculateStartPadding();
+ method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateInnerPadding(int radialDirection);
+ method public float calculateOuterPadding(int radialDirection);
}
public final class BasicCurvedTextKt {
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public interface CurvedAlignment {
@@ -120,6 +117,16 @@
public final class CurvedModifierKt {
}
+ public final class CurvedPaddingKt {
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+ }
+
public final class CurvedParentDataKt {
method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -15,18 +15,15 @@
}
@androidx.compose.runtime.Stable public interface ArcPaddingValues {
- method public float calculateEndPadding();
- method public float calculateInnerPadding();
- method public float calculateOuterPadding();
- method public float calculateStartPadding();
+ method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateInnerPadding(int radialDirection);
+ method public float calculateOuterPadding(int radialDirection);
}
public final class BasicCurvedTextKt {
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public interface CurvedAlignment {
@@ -120,6 +117,16 @@
public final class CurvedModifierKt {
}
+ public final class CurvedPaddingKt {
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+ }
+
public final class CurvedParentDataKt {
method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -15,18 +15,15 @@
}
@androidx.compose.runtime.Stable public interface ArcPaddingValues {
- method public float calculateEndPadding();
- method public float calculateInnerPadding();
- method public float calculateOuterPadding();
- method public float calculateStartPadding();
+ method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+ method public float calculateInnerPadding(int radialDirection);
+ method public float calculateOuterPadding(int radialDirection);
}
public final class BasicCurvedTextKt {
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
- method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
- method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+ method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public interface CurvedAlignment {
@@ -120,6 +117,16 @@
public final class CurvedModifierKt {
}
+ public final class CurvedPaddingKt {
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+ method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+ method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+ }
+
public final class CurvedParentDataKt {
method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
index 0738444..8279fb3 100644
--- a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
@@ -30,7 +30,6 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedAlignment
import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
@@ -43,6 +42,7 @@
import androidx.wear.compose.foundation.curvedColumn
import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
import androidx.wear.compose.foundation.radialGradientBackground
import androidx.wear.compose.foundation.size
import androidx.wear.compose.foundation.weight
@@ -125,14 +125,14 @@
CurvedLayout(modifier = Modifier.fillMaxSize()) {
basicCurvedText(
"Curved Text",
+ CurvedModifier.padding(10.dp),
style = {
CurvedTextStyle(
fontSize = 16.sp,
color = Color.Black,
background = Color.White
)
- },
- contentArcPadding = ArcPaddingValues(10.dp)
+ }
)
curvedComposable {
Box(modifier = Modifier.size(20.dp).background(Color.Gray))
@@ -185,9 +185,9 @@
color = Color.Black
)
},
- contentArcPadding = ArcPaddingValues(5.dp),
modifier = CurvedModifier
.radialGradientBackground(0f to Color.White, 1f to Color.Black)
+ .padding(5.dp)
)
basicCurvedText(
"Angular",
@@ -197,9 +197,9 @@
color = Color.Black
)
},
- contentArcPadding = ArcPaddingValues(5.dp),
modifier = CurvedModifier
.angularGradientBackground(0f to Color.White, 1f to Color.Black)
+ .padding(5.dp)
)
}
}
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
index 712f70c..45360e3 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
@@ -19,7 +19,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import org.junit.Assert
@@ -119,7 +118,7 @@
internal class SpyCurvedChildWrapper(private val capturedInfo: CapturedInfo, wrapped: CurvedChild) :
BaseCurvedChildWrapper(wrapped) {
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int = with(wrapped) {
diff --git a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
index 2b9af94..57a9954 100644
--- a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
+++ b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
@@ -38,7 +38,6 @@
private var text: String = ""
private var clockwise: Boolean = true
private var fontSizePx: Float = 0f
- private var arcPaddingPx: ArcPaddingPx = ArcPaddingPx(0f, 0f, 0f, 0f)
actual var textWidth by mutableStateOf(0f)
actual var textHeight by mutableStateOf(0f)
@@ -53,19 +52,16 @@
actual fun updateIfNeeded(
text: String,
clockwise: Boolean,
- fontSizePx: Float,
- arcPaddingPx: ArcPaddingPx
+ fontSizePx: Float
) {
if (
text != this.text ||
clockwise != this.clockwise ||
- fontSizePx != this.fontSizePx ||
- arcPaddingPx != this.arcPaddingPx
+ fontSizePx != this.fontSizePx
) {
this.text = text
this.clockwise = clockwise
this.fontSizePx = fontSizePx
- this.arcPaddingPx = arcPaddingPx
doUpdate()
lastLayoutInfo = null // Ensure paths are recomputed
}
@@ -77,10 +73,9 @@
val rect = android.graphics.Rect()
paint.getTextBounds(text, 0, text.length, rect)
- textWidth = rect.width() + arcPaddingPx.before + arcPaddingPx.after
- textHeight = -paint.fontMetrics.top + paint.fontMetrics.bottom +
- arcPaddingPx.inner + arcPaddingPx.outer
- baseLinePosition = arcPaddingPx.outer +
+ textWidth = rect.width().toFloat()
+ textHeight = -paint.fontMetrics.top + paint.fontMetrics.bottom
+ baseLinePosition =
if (clockwise) -paint.fontMetrics.top else paint.fontMetrics.bottom
}
@@ -90,9 +85,6 @@
with(layoutInfo) {
val clockwiseFactor = if (clockwise) 1f else -1f
- val paddingBeforeAsAngle = (arcPaddingPx.before / measureRadius)
- .toDegrees()
- .coerceAtMost(360f)
val sweepDegree = sweepRadians.toDegrees().coerceAtMost(360f)
val centerX = centerOffset.x
@@ -125,8 +117,7 @@
centerX + measureRadius,
centerY + measureRadius,
startAngleRadians.toDegrees() +
- (if (clockwise) paddingBeforeAsAngle
- else sweepDegree - paddingBeforeAsAngle),
+ (if (clockwise) 0f else sweepDegree),
clockwiseFactor * sweepDegree
)
}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
index 70a9ac5..ebb4b39 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
@@ -17,97 +17,11 @@
package androidx.wear.compose.foundation
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-/**
- * Apply additional space along each edge of the content in [Dp].
- * See the [ArcPaddingValues] factories for convenient ways to
- * build [ArcPaddingValues].
- */
-@Stable
-public interface ArcPaddingValues {
- /**
- * Padding in the outward direction from the center of the [CurvedLayout]
- */
- fun calculateOuterPadding(): Dp
-
- /**
- * Padding in the inwards direction towards the center of the [CurvedLayout]
- */
- fun calculateInnerPadding(): Dp
-
- /**
- * Padding added at the start of the component.
- */
- fun calculateStartPadding(): Dp
-
- /**
- * Padding added at the end of the component.
- */
- fun calculateEndPadding(): Dp
-}
-
-/**
- * Apply additional space along each edge of the content in [Dp]. Note that the start and end
- * edges will be determined by the direction (clockwise or counterclockwise)
- *
- * @param outer Padding in the outward direction from the center of the
- * [CurvedLayout]
- * @param inner Padding in the inwards direction towards the center of the [CurvedLayout]
- * @param start Padding added at the start of the component.
- * @param end Padding added at the end of the component.
- */
-public fun ArcPaddingValues(
- outer: Dp = 0.dp,
- inner: Dp = 0.dp,
- start: Dp = 0.dp,
- end: Dp = 0.dp
-): ArcPaddingValues =
- ArcPaddingValuesImpl(outer, inner, start, end)
-
-/**
- * Apply [all] dp of additional space along each edge of the content.
- */
-public fun ArcPaddingValues(all: Dp): ArcPaddingValues = ArcPaddingValuesImpl(all, all, all, all)
-
-/**
- * Apply [radial] dp of additional space on the edges towards and away from the center, and
- * [angular] dp before and after the component.
- */
-public fun ArcPaddingValues(radial: Dp = 0.dp, angular: Dp = 0.dp): ArcPaddingValues =
- ArcPaddingValuesImpl(radial, radial, angular, angular)
-
-@Stable
-internal class ArcPaddingValuesImpl(val outer: Dp, val inner: Dp, val start: Dp, val end: Dp) :
- ArcPaddingValues {
- override fun equals(other: Any?): Boolean {
- return other is ArcPaddingValuesImpl &&
- outer == other.outer &&
- inner == other.inner &&
- start == other.start &&
- end == other.end
- }
-
- override fun hashCode() = ((outer.hashCode() * 31 + inner.hashCode()) * 31 + start.hashCode()) *
- 31 + end.hashCode()
-
- override fun toString(): String {
- return "ArcPaddingValuesImpl(outer=$outer, inner=$inner, start=$start, end=$end)"
- }
-
- override fun calculateOuterPadding() = outer
- override fun calculateInnerPadding() = inner
- override fun calculateStartPadding() = start
- override fun calculateEndPadding() = end
-}
/**
* [basicCurvedText] is a component allowing developers to easily write curved text following
@@ -123,8 +37,6 @@
* those needs to be reversed in a Rtl layout.
* If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
* See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
* @param overflow How visual overflow should be handled.
* @param style A @Composable factory to provide the style to use. This composable SHOULDN'T
* generate any compose nodes.
@@ -133,13 +45,11 @@
text: String,
modifier: CurvedModifier = CurvedModifier,
angularDirection: CurvedDirection.Angular? = null,
- contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
overflow: TextOverflow = TextOverflow.Clip,
style: @Composable () -> CurvedTextStyle = { CurvedTextStyle() }
) = add(CurvedTextChild(
text,
curvedLayoutDirection.copy(overrideAngular = angularDirection).clockwise(),
- contentArcPadding,
style,
overflow
), modifier)
@@ -159,8 +69,6 @@
* those needs to be reversed in a Rtl layout.
* If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
* See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
* @param overflow How visual overflow should be handled.
*/
public fun CurvedScope.basicCurvedText(
@@ -168,33 +76,23 @@
style: CurvedTextStyle,
modifier: CurvedModifier = CurvedModifier,
angularDirection: CurvedDirection.Angular? = null,
- // TODO: reimplement as modifiers
- contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
overflow: TextOverflow = TextOverflow.Clip,
-) = basicCurvedText(text, modifier, angularDirection, contentArcPadding, overflow) { style }
+) = basicCurvedText(text, modifier, angularDirection, overflow) { style }
internal class CurvedTextChild(
val text: String,
val clockwise: Boolean = true,
- val contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
val style: @Composable () -> CurvedTextStyle = { CurvedTextStyle() },
val overflow: TextOverflow
) : CurvedChild() {
private val delegate: CurvedTextDelegate = CurvedTextDelegate()
private lateinit var actualStyle: CurvedTextStyle
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int {
- // TODO: move padding into a CurvedModifier
- val arcPaddingPx = ArcPaddingPx(
- contentArcPadding.calculateOuterPadding().toPx(),
- contentArcPadding.calculateInnerPadding().toPx(),
- contentArcPadding.calculateStartPadding().toPx(),
- contentArcPadding.calculateEndPadding().toPx()
- )
- delegate.updateIfNeeded(text, clockwise, actualStyle.fontSize.toPx(), arcPaddingPx)
+ delegate.updateIfNeeded(text, clockwise, actualStyle.fontSize.toPx())
return index // No measurables where mapped.
}
@@ -242,13 +140,6 @@
}
}
-internal data class ArcPaddingPx(
- val outer: Float,
- val inner: Float,
- val before: Float,
- val after: Float
-)
-
internal expect class CurvedTextDelegate() {
var textWidth: Float
var textHeight: Float
@@ -257,8 +148,7 @@
fun updateIfNeeded(
text: String,
clockwise: Boolean,
- fontSizePx: Float,
- arcPaddingPx: ArcPaddingPx
+ fontSizePx: Float
)
fun DrawScope.doDraw(
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
index 5cdb192..d6c4559 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
@@ -21,7 +21,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import kotlin.math.PI
@@ -62,7 +61,7 @@
Box(content = content)
}
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int {
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
index bff6b0a..a71600b 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
@@ -20,7 +20,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
/**
@@ -35,10 +34,6 @@
internal fun add(node: CurvedChild, modifier: CurvedModifier) {
nodes.add(modifier.wrap(node))
}
- internal fun initialize(contentBuilder: CurvedScope.() -> Unit) {
- nodes.clear()
- apply(contentBuilder)
- }
}
/**
@@ -47,9 +42,9 @@
internal abstract class ContainerChild(
curvedLayoutDirection: CurvedLayoutDirection,
internal val reverseLayout: Boolean,
- private val contentBuilder: CurvedScope.() -> Unit
+ contentBuilder: CurvedScope.() -> Unit
) : CurvedChild() {
- private val curvedContainerScope = CurvedScope(curvedLayoutDirection)
+ private val curvedContainerScope = CurvedScope(curvedLayoutDirection).apply(contentBuilder)
internal val children get() = curvedContainerScope.nodes
internal val childrenInLayoutOrder get() = children.indices.map { ix ->
@@ -58,20 +53,22 @@
@Composable
override fun SubComposition() {
- // Ensure we subscribe this composition to any state reads on contentBuilder,
- // and we keep our internal tree in sync with compose's tree.
- curvedContainerScope.initialize(contentBuilder)
children.forEach {
it.SubComposition()
}
}
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
) = children.fold(index) { currentIndex, node ->
- with(node) {
- initializeMeasure(measurables, currentIndex)
+ with(CurvedMeasureScope(
+ subDensity = this,
+ curvedContainerScope.curvedLayoutDirection
+ )) {
+ with(node) {
+ initializeMeasure(measurables, currentIndex)
+ }
}
}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
index 8be4e91..716be35 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
@@ -29,6 +29,23 @@
@Immutable
@kotlin.jvm.JvmInline
public value class Angular internal constructor(internal val value: Int) {
+ /**
+ * Determine if the layout is going clockwise or counter-clockwise given the layout
+ * direction and this angular layout direction.
+ *
+ * @param layoutDirection The [LayoutDirection] to resolve this with.
+ */
+ internal fun resolveClockwise(layoutDirection: LayoutDirection): Boolean {
+ val isLtr = layoutDirection == LayoutDirection.Ltr
+ return when (this) {
+ Reversed -> !isLtr
+ Clockwise -> true
+ CounterClockwise -> false
+ // Normal
+ else -> isLtr
+ }
+ }
+
companion object {
/**
* Go in Clockwise direction for Ltr layout and Counter Clockwise for Rtl.
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
index 199a7da..bb0110c 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
@@ -19,10 +19,8 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
@@ -30,9 +28,9 @@
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.PI
import kotlin.math.cos
@@ -109,11 +107,11 @@
// Note that all angles in the function are in radians, and the anchor parameter is in degrees
val curvedLayoutDirection = initialCurvedLayoutDirection(angularDirection)
- val curvedRowChild by remember(curvedLayoutDirection, radialAlignment, contentBuilder) {
- derivedStateOf {
- CurvedRowChild(curvedLayoutDirection, radialAlignment, contentBuilder)
- }
- }
+
+ // Create the curved tree and subscribe to be recomposed when any part changes, this may not be
+ // optimal but since we have only one measure block (the one here) for all the curved layout,
+ // we still need to do most of the work when content changes.
+ val curvedRowChild = CurvedRowChild(curvedLayoutDirection, radialAlignment, contentBuilder)
Layout(
modifier = modifier.drawWithContent {
@@ -135,9 +133,11 @@
// Give the curved row scope the information needed to measure and map measurables
// to children.
- with(curvedRowChild) {
- val mapped = initializeMeasure(measurables, 0)
- require(mapped == measurables.size)
+ with(CurvedMeasureScope(subDensity = this, curvedLayoutDirection)) {
+ with(curvedRowChild) {
+ val mapped = initializeMeasure(measurables, 0)
+ require(mapped == measurables.size)
+ }
}
curvedRowChild.estimateThickness(radius)
@@ -166,24 +166,14 @@
// TODO() For performance, to avoid class extra creation/destruction, we could implement this class
// as a bit field.
internal class CurvedLayoutDirection(
- private val radial: CurvedDirection.Radial,
- private val angular: CurvedDirection.Angular,
- private val layoutDirection: LayoutDirection
+ internal val radial: CurvedDirection.Radial,
+ internal val angular: CurvedDirection.Angular,
+ internal val layoutDirection: LayoutDirection
) {
- fun clockwise(): Boolean {
- return when (angular) {
- CurvedDirection.Angular.Reversed -> !isLtr()
- CurvedDirection.Angular.Clockwise -> true
- CurvedDirection.Angular.CounterClockwise -> false
- // CurvedDirection.Angular.Clockwise
- else -> isLtr()
- }
- }
+ fun clockwise(): Boolean = angular.resolveClockwise(layoutDirection)
fun outsideIn(): Boolean = radial == CurvedDirection.Radial.OutsideIn
- fun isLtr(): Boolean = layoutDirection == LayoutDirection.Ltr
-
fun copy(
overrideRadial: CurvedDirection.Radial? = null,
overrideAngular: CurvedDirection.Angular? = null
@@ -266,6 +256,12 @@
val measureRadius: Float, // TODO: remove this from here or generalize
)
+// Similar to IntrinsicMeasureScope
+internal class CurvedMeasureScope(
+ val subDensity: Density,
+ val curvedLayoutDirection: CurvedLayoutDirection
+) : Density by subDensity
+
/**
* Base class for children of a [CurvedLayout].
*
@@ -299,6 +295,10 @@
internal val sweepRadians: Float
get() = partialLayoutInfo.sweepRadians
+ // Exposed as is needed in padding modifiers to translate distances to angles.
+ internal val measureRadius: Float
+ get() = partialLayoutInfo.measureRadius
+
/**
* Compose the content. This may generate some compose-ui nodes, but has to match
* initializeMeasure's matching behavior (initializeMeasure should return the index parameter +
@@ -316,7 +316,7 @@
* @return The new index in the measurables array, taking into account how many items we
* mapped.
*/
- open fun MeasureScope.initializeMeasure(
+ open fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int = index
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
index a2a49b4..1a6ebb62 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
@@ -21,7 +21,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
/**
@@ -84,7 +83,7 @@
@Composable
override fun SubComposition() { wrapped.SubComposition() }
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int = with(wrapped) {
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt
new file mode 100644
index 0000000..43be9c2
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+
+/**
+ * Apply additional space along the edges of the content.
+ *
+ * @param paddingValues The [ArcPaddingValues] to use. See that class and factory methods to see
+ * how paddings can be specified.
+ */
+public fun CurvedModifier.padding(paddingValues: ArcPaddingValues) =
+ this.then { child -> PaddingWrapper(child, paddingValues) }
+
+/**
+ * Apply additional space along the edges of the content. Dimmensions are in dp. For before and
+ * after they will be considered as if they are at the midpoint of the content (for conversion
+ * between dimension and angle).
+ *
+ * @param outer The space to add to the outer edge of the content (away from the center of the
+ * containing CurvedLayout)
+ * @param inner The space to add to the inner edge of the content (towards the center of the
+ * containing CurvedLayout)
+ * @param before The space added before the component, if it was draw clockwise. This is the edge of
+ * the component with the "smallest" angle.
+ * @param after The space added after the component, if it was draw clockwise. This is the edge of
+ * the component with the "biggest" angle.
+ */
+public fun CurvedModifier.padding(outer: Dp, inner: Dp, before: Dp, after: Dp) =
+ padding(ArcPaddingValuesImpl(outer, inner, before, after))
+
+/**
+ * Apply [angular] dp space before and after the component, and [radial] dp space to the outer and
+ * inner edges.
+ *
+ * @param radial The space added to the outer and inner edges of the content, in dp.
+ * @param angular The space added before and after the content, in dp.
+ */
+public fun CurvedModifier.padding(radial: Dp = 0.dp, angular: Dp = 0.dp) =
+ padding(radial, radial, angular, angular)
+
+/**
+ * Apply [all] dp space around the component.
+ *
+ * @param all The space added to all edges.
+ */
+public fun CurvedModifier.padding(all: Dp = 0.dp) = padding(all, all, all, all)
+
+/**
+ * Apply additional space along each edge of the content in [Dp].
+ * See the [ArcPaddingValues] factories for convenient ways to
+ * build [ArcPaddingValues].
+ */
+@Stable
+public interface ArcPaddingValues {
+ /**
+ * Padding in the outward direction from the center of the [CurvedLayout]
+ */
+ fun calculateOuterPadding(radialDirection: CurvedDirection.Radial): Dp
+
+ /**
+ * Padding in the inwards direction towards the center of the [CurvedLayout]
+ */
+ fun calculateInnerPadding(radialDirection: CurvedDirection.Radial): Dp
+
+ /**
+ * Padding added before the component, if it was draw clockwise. This is the edge of the
+ * component with the "smallest" angle.
+ */
+ fun calculateAfterPadding(
+ layoutDirection: LayoutDirection,
+ angularDirection: CurvedDirection.Angular
+ ): Dp
+
+ /**
+ * Padding added after the component, if it was draw clockwise. This is the edge of the
+ * component with the "biggest" angle.
+ */
+ fun calculateBeforePadding(
+ layoutDirection: LayoutDirection,
+ angularDirection: CurvedDirection.Angular
+ ): Dp
+}
+
+/**
+ * Apply additional space along each edge of the content in [Dp]. Note that that all dimensions are
+ * applied to a concrete edge, indepenend on layout direction and curved layout direction.
+ *
+ * @param outer Padding in the outward direction from the center of the
+ * [CurvedLayout]
+ * @param inner Padding in the inwards direction towards the center of the [CurvedLayout]
+ * @param before Padding added before the component, if it was draw clockwise.
+ * @param after Padding added after the component, if it was draw clockwise.
+ */
+public fun ArcPaddingValues(
+ outer: Dp = 0.dp,
+ inner: Dp = 0.dp,
+ before: Dp = 0.dp,
+ after: Dp = 0.dp
+): ArcPaddingValues =
+ ArcPaddingValuesImpl(outer, inner, before, after)
+
+/**
+ * Apply [all] dp of additional space along each edge of the content.
+ */
+public fun ArcPaddingValues(all: Dp): ArcPaddingValues = ArcPaddingValuesImpl(all, all, all, all)
+
+/**
+ * Apply [radial] dp of additional space on the edges towards and away from the center, and
+ * [angular] dp before and after the component.
+ */
+public fun ArcPaddingValues(radial: Dp = 0.dp, angular: Dp = 0.dp): ArcPaddingValues =
+ ArcPaddingValuesImpl(radial, radial, angular, angular)
+
+@Immutable
+internal class ArcPaddingValuesImpl(val outer: Dp, val inner: Dp, val before: Dp, val after: Dp) :
+ ArcPaddingValues {
+ override fun equals(other: Any?): Boolean {
+ return other is ArcPaddingValuesImpl &&
+ outer == other.outer &&
+ inner == other.inner &&
+ before == other.before &&
+ after == other.after
+ }
+
+ override fun hashCode() = ((outer.hashCode() * 31 + inner.hashCode()) * 31 +
+ before.hashCode()) * 31 + after.hashCode()
+
+ override fun toString(): String {
+ return "ArcPaddingValuesImpl(outer=$outer, inner=$inner, before=$before, after=$after)"
+ }
+
+ override fun calculateOuterPadding(radialDirection: CurvedDirection.Radial) = outer
+ override fun calculateInnerPadding(radialDirection: CurvedDirection.Radial) = inner
+ override fun calculateBeforePadding(
+ layoutDirection: LayoutDirection,
+ angularDirection: CurvedDirection.Angular
+ ) = before
+ override fun calculateAfterPadding(
+ layoutDirection: LayoutDirection,
+ angularDirection: CurvedDirection.Angular
+ ) = after
+}
+
+internal class PaddingWrapper(
+ child: CurvedChild,
+ val paddingValues: ArcPaddingValues
+) : BaseCurvedChildWrapper(child) {
+ private var outerPx = 0f
+ private var innerPx = 0f
+ private var beforePx = 0f
+ private var afterPx = 0f
+
+ override fun CurvedMeasureScope.initializeMeasure(
+ measurables: List<Measurable>,
+ index: Int
+ ): Int {
+ outerPx = paddingValues.calculateOuterPadding(curvedLayoutDirection.radial).toPx()
+ innerPx = paddingValues.calculateInnerPadding(curvedLayoutDirection.radial).toPx()
+ beforePx = paddingValues.calculateBeforePadding(
+ curvedLayoutDirection.layoutDirection,
+ curvedLayoutDirection.angular
+ ).toPx()
+ afterPx = paddingValues.calculateAfterPadding(
+ curvedLayoutDirection.layoutDirection,
+ curvedLayoutDirection.angular
+ ).toPx()
+ return with(wrapped) {
+ initializeMeasure(measurables, index)
+ }
+ }
+
+ override fun doEstimateThickness(maxRadius: Float) = wrapped.estimateThickness(maxRadius) +
+ outerPx + innerPx
+
+ override fun doRadialPosition(
+ parentOuterRadius: Float,
+ parentThickness: Float
+ ): PartialLayoutInfo {
+ val partialLayoutInfo = wrapped.radialPosition(
+ parentOuterRadius - outerPx,
+ parentThickness - outerPx - innerPx
+ )
+ val angularPadding = (beforePx + afterPx) / partialLayoutInfo.measureRadius
+ return PartialLayoutInfo(
+ partialLayoutInfo.sweepRadians + angularPadding,
+ partialLayoutInfo.outerRadius + outerPx,
+ partialLayoutInfo.thickness + innerPx + outerPx,
+ partialLayoutInfo.measureRadius + outerPx
+ )
+ }
+
+ override fun doAngularPosition(
+ parentStartAngleRadians: Float,
+ parentSweepRadians: Float,
+ centerOffset: Offset
+ ): Float {
+ val startAngularPadding = beforePx / measureRadius
+ val angularPadding = (beforePx + afterPx) / measureRadius
+ return wrapped.angularPosition(
+ parentStartAngleRadians + startAngularPadding,
+ parentSweepRadians - angularPadding,
+ centerOffset
+ ) - startAngularPadding
+ }
+}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
index 1630e35..79c9786 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
@@ -18,7 +18,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -92,7 +91,7 @@
private var minThicknessPx = 0f
private var maxThicknessPx = 0f
- override fun MeasureScope.initializeMeasure(
+ override fun CurvedMeasureScope.initializeMeasure(
measurables: List<Measurable>,
index: Int
): Int {
diff --git a/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt b/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
index 695e7f4..8c4c9c4 100644
--- a/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
+++ b/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
@@ -28,8 +28,7 @@
actual fun updateIfNeeded(
text: String,
clockwise: Boolean,
- fontSizePx: Float,
- arcPaddingPx: ArcPaddingPx
+ fontSizePx: Float
) {
// TODO(b/194653251): Implement
throw java.lang.RuntimeException("Not implemented")
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index b60025f..01ec528 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -1,6 +1,10 @@
// Signature format: 4.0
package androidx.wear.compose.material {
+ @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+ ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+ }
+
@androidx.compose.runtime.Stable public interface ButtonColors {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
}
public final class CurvedTextKt {
- method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public final class DefaultTimeSourceKt {
@@ -328,7 +332,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -515,10 +519,6 @@
property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
property public final boolean isAnimationRunning;
property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
- field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
- }
-
- public static final class SwipeToDismissBoxState.Companion {
}
public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index b285cf4..0d98e8b 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -1,6 +1,10 @@
// Signature format: 4.0
package androidx.wear.compose.material {
+ @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+ ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+ }
+
@androidx.compose.runtime.Stable public interface ButtonColors {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
}
public final class CurvedTextKt {
- method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public final class DefaultTimeSourceKt {
@@ -354,7 +358,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -551,10 +555,6 @@
property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
property public final boolean isAnimationRunning;
property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
- field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
- }
-
- public static final class SwipeToDismissBoxState.Companion {
}
public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index b60025f..01ec528 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -1,6 +1,10 @@
// Signature format: 4.0
package androidx.wear.compose.material {
+ @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+ ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+ }
+
@androidx.compose.runtime.Stable public interface ButtonColors {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
}
public final class CurvedTextKt {
- method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+ method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
public final class DefaultTimeSourceKt {
@@ -328,7 +332,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -515,10 +519,6 @@
property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
property public final boolean isAnimationRunning;
property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
- field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
- }
-
- public static final class SwipeToDismissBoxState.Companion {
}
public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
index f38a020..976c4ce2 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.AutoCenteringParams
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ListHeader
@@ -92,7 +93,8 @@
ScalingLazyColumn(
anchorType = ScalingLazyListAnchorType.ItemStart,
verticalArrangement = Arrangement.spacedBy(itemSpacing),
- state = state
+ state = state,
+ autoCentering = AutoCenteringParams(itemOffset = scrollOffset)
) {
item {
ListHeader {
@@ -119,7 +121,7 @@
fun SimpleScalingLazyColumnWithContentPadding() {
ScalingLazyColumn(
contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp),
- autoCentering = false
+ autoCentering = null
) {
item {
ListHeader {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
index f516fda..63805c3 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
@@ -184,6 +184,110 @@
}
}
+public class AppCardTest {
+ @get:Rule
+ public val rule: ComposeContentTestRule = createComposeRule()
+
+ @Test
+ public fun responds_to_click_when_enabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ AppCard(
+ onClick = { clicked = true },
+ enabled = true,
+ appName = {},
+ time = {},
+ title = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(true, clicked)
+ }
+ }
+
+ @Test
+ public fun does_not_respond_to_click_when_disabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ AppCard(
+ onClick = { clicked = true },
+ appName = {},
+ time = {},
+ title = {},
+ enabled = false,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(false, clicked)
+ }
+ }
+}
+
+public class TitleCardTest {
+ @get:Rule
+ public val rule: ComposeContentTestRule = createComposeRule()
+
+ @Test
+ public fun responds_to_click_when_enabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ TitleCard(
+ onClick = { clicked = true },
+ enabled = true,
+ time = {},
+ title = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(true, clicked)
+ }
+ }
+
+ @Test
+ public fun does_not_respond_to_click_when_disabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ TitleCard(
+ onClick = { clicked = true },
+ enabled = false,
+ time = {},
+ title = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(false, clicked)
+ }
+ }
+}
+
public class CardSizeTest {
@get:Rule
public val rule: ComposeContentTestRule = createComposeRule()
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index dd3eb02..4ea531b 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -118,7 +118,7 @@
modifier = Modifier
.onSizeChanged { viewPortHeight = it.height }
.requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f),
- autoCentering = false
+ autoCentering = null
) {
items(3) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -189,7 +189,7 @@
@Test
fun scrollableScalingLazyColumnGivesCorrectPositionAndSize() {
scrollableScalingLazyColumnPositionAndSize(
- autoCentering = true,
+ enableAutoCentering = true,
contentPadding = PaddingValues(0.dp)
)
}
@@ -197,7 +197,7 @@
@Test
fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPadding() {
scrollableScalingLazyColumnPositionAndSize(
- autoCentering = true,
+ enableAutoCentering = true,
contentPadding = PaddingValues(50.dp)
)
}
@@ -205,13 +205,13 @@
@Test
fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPaddingNoAutoCenter() {
scrollableScalingLazyColumnPositionAndSize(
- autoCentering = false,
+ enableAutoCentering = false,
contentPadding = PaddingValues(50.dp)
)
}
private fun scrollableScalingLazyColumnPositionAndSize(
- autoCentering: Boolean,
+ enableAutoCentering: Boolean,
contentPadding: PaddingValues
) {
lateinit var state: ScalingLazyListState
@@ -230,7 +230,8 @@
itemSizeDp * 3f + itemSpacingDp * 2f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f),
- autoCentering = autoCentering,
+ autoCentering = if (enableAutoCentering)
+ AutoCenteringParams(itemIndex = 0) else null,
contentPadding = contentPadding
) {
items(5) {
@@ -325,7 +326,7 @@
.fillMaxWidth()
.requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
.background(Color.DarkGray),
- autoCentering = false
+ autoCentering = null
) {
items(3) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -371,7 +372,7 @@
)
.background(Color.DarkGray),
scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
index 8d050db..1a3cbe7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
@@ -98,7 +98,7 @@
state = rememberScalingLazyListState(initialCenterItemIndex = 0)
.also { state = it },
modifier = Modifier.height(200.dp),
- autoCentering = false,
+ autoCentering = null,
scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
) {
itemsIndexed(items) { index, item ->
@@ -130,7 +130,7 @@
state = rememberScalingLazyListState(initialCenterItemIndex = 0)
.also { state = it },
modifier = Modifier.height(viewPortHeight),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(itemIndex = 0),
verticalArrangement = Arrangement.spacedBy(gapBetweenItems),
// No scaling as we are doing maths with expected item sizes
scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index 3d17741..850300e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -87,7 +87,7 @@
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
anchorType = ScalingLazyListAnchorType.ItemCenter,
- autoCentering = true,
+ autoCentering = AutoCenteringParams(),
verticalArrangement = Arrangement.spacedBy(0.dp),
scalingParams = ScalingLazyColumnDefaults.scalingParams(
edgeScale = 0f,
@@ -139,7 +139,7 @@
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
anchorType = ScalingLazyListAnchorType.ItemStart,
- autoCentering = true,
+ autoCentering = AutoCenteringParams(),
verticalArrangement = Arrangement.spacedBy(0.dp),
scalingParams = ScalingLazyColumnDefaults.scalingParams(
edgeScale = 0f,
@@ -192,7 +192,7 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -224,7 +224,7 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = false,
+ autoCentering = null,
userScrollEnabled = false
) {
items(5) {
@@ -259,7 +259,7 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -292,7 +292,7 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(itemIndex = 0),
flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state)
) {
items(5) {
@@ -337,7 +337,7 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(itemIndex = 0),
flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(
state = state,
snapOffset = snapOffset
@@ -381,7 +381,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
reverseLayout = true,
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -414,6 +414,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
@@ -492,7 +493,7 @@
modifier = Modifier
.testTag(TEST_TAG)
.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
- autoCentering = false
+ autoCentering = null
) {
items(6) {
Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index a1ae7e9..c39f251 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -77,7 +77,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true
+ autoCentering = AutoCenteringParams()
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -103,7 +103,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(),
contentPadding = PaddingValues(all = 0.dp)
) {
items(5) {
@@ -129,7 +129,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(),
contentPadding = PaddingValues(all = 0.dp)
) {
items(5) {
@@ -155,7 +155,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true,
+ autoCentering = AutoCenteringParams(),
contentPadding = PaddingValues(all = 0.dp),
reverseLayout = true
) {
@@ -183,7 +183,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = true
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -209,7 +209,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -234,7 +234,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
reverseLayout = true,
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -261,7 +261,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
reverseLayout = true,
- autoCentering = true
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -287,7 +287,7 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -314,7 +314,7 @@
modifier = Modifier.requiredSize(
itemSizeDp
),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp * 5))
@@ -404,7 +404,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
reverseLayout = true,
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -569,6 +569,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(
@@ -661,6 +662,7 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ autoCentering = AutoCenteringParams(itemIndex = 0)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -700,7 +702,7 @@
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
reverseLayout = true,
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -736,7 +738,7 @@
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.requiredSize(itemSizeDp * 3.5f + spacing * 2.5f),
verticalArrangement = Arrangement.spacedBy(spacing),
- autoCentering = false
+ autoCentering = null
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -773,7 +775,7 @@
ScalingLazyColumn(
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
- autoCentering = false
+ autoCentering = null
) {
items(6) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -806,7 +808,7 @@
ScalingLazyColumn(
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
- autoCentering = false
+ autoCentering = null
) {
items(6) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -892,7 +894,7 @@
ScalingLazyColumn(
state = rememberScalingLazyListState().also { state = it },
modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
- autoCentering = false
+ autoCentering = null
) {
items(6) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -1026,7 +1028,7 @@
state = rememberScalingLazyListState(
initialCenterItemIndex = 0
).also { state = it },
- autoCentering = true
+ autoCentering = AutoCenteringParams()
) {
items(7) {
Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index ca6aaa8..90eadf9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -16,21 +16,21 @@
package androidx.wear.compose.material
+import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -174,6 +174,8 @@
* @param title A slot for displaying the title of the card, expected to be one or two lines of
* start aligned text of [Typography.button]
* @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When `false`, this card will not
+ * be clickable
* @param appImage A slot for a small ([CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] )
* [Image] associated with the application.
* @param backgroundPainter A painter used to paint the background of the card. A card will
@@ -192,6 +194,7 @@
time: @Composable RowScope.() -> Unit,
title: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
+ enabled: Boolean = true,
appImage: @Composable (RowScope.() -> Unit)? = null,
backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
@@ -204,7 +207,7 @@
onClick = onClick,
modifier = modifier,
backgroundPainter = backgroundPainter,
- enabled = true,
+ enabled = enabled,
) {
Column {
Row(
@@ -283,6 +286,8 @@
* @param title A slot for displaying the title of the card, expected to be one or two lines of text
* of [Typography.button]
* @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When `false`, this card will not
+ * be clickable
* @param time An optional slot for displaying the time relevant to the contents of the card,
* expected to be a short piece of end aligned text.
* @param backgroundPainter A painter used to paint the background of the card. A title card can
@@ -298,6 +303,7 @@
onClick: () -> Unit,
title: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
+ enabled: Boolean = true,
time: @Composable (RowScope.() -> Unit)? = null,
backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
@@ -309,7 +315,7 @@
onClick = onClick,
modifier = modifier,
backgroundPainter = backgroundPainter,
- enabled = true,
+ enabled = enabled,
) {
Column {
Row(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
index eb8da4f..efc3cda 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
@@ -20,16 +20,13 @@
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
-import androidx.wear.compose.foundation.basicCurvedText
import androidx.wear.compose.foundation.CurvedModifier
import androidx.wear.compose.foundation.CurvedScope
import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.basicCurvedText
import androidx.wear.compose.foundation.curvedRow
/**
@@ -74,8 +71,6 @@
* those needs to be reversed in a Rtl layout.
* If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
* See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
* @param overflow How visual overflow should be handled.
*/
public fun CurvedScope.curvedText(
@@ -86,9 +81,8 @@
fontSize: TextUnit = TextUnit.Unspecified,
style: CurvedTextStyle? = null,
angularDirection: CurvedDirection.Angular? = null,
- contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
overflow: TextOverflow = TextOverflow.Clip,
-) = basicCurvedText(text, modifier, angularDirection, contentArcPadding, overflow) {
+) = basicCurvedText(text, modifier, angularDirection, overflow) {
val baseStyle = style ?: CurvedTextStyle(LocalTextStyle.current)
val textColor = color.takeOrElse {
baseStyle.color.takeOrElse {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index 9b737ad..2bcb66a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -200,6 +200,69 @@
}
}
+/**
+ * Parameters to determine which list item and offset to calculate auto-centering spacing for. The
+ * default values are [itemIndex] = 1 and [itemOffset] = 0. This will provide sufficient padding for
+ * the second item (index = 1) in the list being centerable. This is to match the Wear UX
+ * guidelines that a typical list will have a ListHeader item as the first item in the list
+ * (index = 0) and that this should not be scrollable into the middle of the viewport, instead the
+ * first list item that a user can interact with (index = 1) would be the first that would be in the
+ * center.
+ *
+ * If your use case is different and you want all list items to be able to be scrolled to the
+ * viewport middle, including the first item in the list then set [itemIndex] = 0.
+ *
+ * The higher the value for [itemIndex] you provide the less auto centering padding will be
+ * provided as the amount of padding needed to allow that item to be centered will reduce.
+ * Even for a list of short items (such as [CompactChip]) setting [itemIndex] above 3 or 4 is likely
+ * to result in no auto-centering padding being provided as items with index 3 or 4 will probably
+ * already be naturally scrollable to the center of the viewport.
+ *
+ * [itemOffset] allows adjustment of the items position relative the [ScalingLazyColumn]s
+ * [ScalingLazyListAnchorType]. This can be useful if you need fine grained control over item
+ * positioning and spacing, e.g. If you are lining up the gaps between two items on the viewport
+ * center line where you would want to set the offset to half the distance between listItems in
+ * pixels.
+ *
+ * See also [rememberScalingLazyListState] where similar fields are provided to allow control over
+ * the initially selected centered item index and offset. By default these match the auto centering
+ * defaults meaning that the second item (index = 1) will be the item scrolled to the viewport
+ * center.
+ *
+ * @param itemIndex Which list item index to enable auto-centering from. Space (padding) will be
+ * added such that items with index [itemIndex] or greater will be able to be scrolled to the center
+ * of the viewport. If the developer wants to add additional space to allow other list items to also
+ * be scrollable to the center they can use contentPadding on the ScalingLazyColumn. If the
+ * developer wants custom control over position and spacing they can switch off autoCentering
+ * and provide contentPadding.
+ *
+ * @param itemOffset What offset, if any, to apply when calculating space for auto-centering
+ * the [itemIndex] item. E.g. itemOffset can be used if the developer wants to align the viewport
+ * center in the gap between two list items.
+ *
+ * For an example of a [ScalingLazyColumn] with an explicit itemOffset see:
+ * @sample androidx.wear.compose.material.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
+ */
+@Immutable
+public class AutoCenteringParams(
+ // @IntRange(from = 0)
+ internal val itemIndex: Int = 1,
+ internal val itemOffset: Int = 0,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return (other is AutoCenteringParams) &&
+ itemIndex == other.itemIndex &&
+ itemOffset == other.itemOffset
+ }
+
+ override fun hashCode(): Int {
+ var result = itemIndex
+ result = 31 * result + itemOffset
+ return result
+ }
+}
+
internal fun convertToCenterOffset(
anchorType: ScalingLazyListAnchorType,
itemScrollOffset: Int,
@@ -262,14 +325,14 @@
* @param scalingParams The parameters to configure the scaling and transparency effects for the
* component
* @param anchorType How to anchor list items to the center-line of the viewport
- * @param autoCentering Flag to indicate whether space/padding should be automatically added to make
- * sure that list items can be scrolled into the center of the viewport (based on their
- * [anchorType]). If true then space will be added before the first list item, if needed, to ensure
- * that items with indexes greater than or equal to the initialCenterItemIndex passed to
- * [ScalingLazyListState] will be able to be scrolled to the center. Similarly space will be added
- * at the end of the list to ensure that items can be scrolled up to the center. If false no
- * automatic space will be added and instead the developer can use [contentPadding] to manually
- * arrange the items.
+ * @param autoCentering AutoCenteringParams parameter to control whether space/padding should be
+ * automatically added to make sure that list items can be scrolled into the center of the viewport
+ * (based on their [anchorType]). If non-null then space will be added before the first list item,
+ * if needed, to ensure that items with indexes greater than or equal to the itemIndex (offset by
+ * itemOffset pixels) will be able to be scrolled to the center of the viewport. Similarly space
+ * will be added at the end of the list to ensure that items can be scrolled up to the center. If
+ * null no automatic space will be added and instead the developer can use [contentPadding] to
+ * manually arrange the items.
*/
@Composable
public fun ScalingLazyColumn(
@@ -287,7 +350,7 @@
userScrollEnabled: Boolean = true,
scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
- autoCentering: Boolean = true,
+ autoCentering: AutoCenteringParams? = AutoCenteringParams(),
content: ScalingLazyListScope.() -> Unit
) {
var initialized by remember { mutableStateOf(false) }
@@ -360,7 +423,7 @@
)
// Only add spacers if autoCentering == true as we have to consider the impact of
// vertical spacing between items.
- if (autoCentering) {
+ if (autoCentering != null) {
item {
Spacer(
modifier = Modifier.height(state.topAutoCenteringItemSizePx.toDp())
@@ -368,7 +431,7 @@
}
}
scope.content()
- if (autoCentering) {
+ if (autoCentering != null) {
item {
Spacer(
modifier = Modifier.height(state.bottomAutoCenteringItemSizePx.toDp())
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index f5a3607..88e9896 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -346,7 +346,7 @@
scalingParams: ScalingParams,
beforeContentPaddingPx: Int,
anchorType: ScalingLazyListAnchorType,
- autoCentering: Boolean,
+ autoCentering: AutoCenteringParams?,
initialized: Boolean
): ScalingLazyListItemInfo {
val adjustedItemStart = itemStart - verticalAdjustment
@@ -381,7 +381,7 @@
)
return DefaultScalingLazyListItemInfo(
// Adjust index to take into account the Spacer before the first list item
- index = if (autoCentering) item.index - 1 else item.index,
+ index = if (autoCentering != null) item.index - 1 else item.index,
key = item.key,
unadjustedOffset = unadjustedOffset,
offset = offset,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index 04e33f8..6ba18f5 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -39,12 +39,7 @@
*
* @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
* defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
- * the first item (index == 0) before it. Note that if [ScalingLazyColumn] autoCentering property is
- * set to true then space (padding) will be added such that items with index
- * [initialCenterItemIndex] or greater will be able to be scrolled to the center of the viewport. If
- * the developer wants to add additional space to allow other list items to also be scrollable to
- * the center they can use contentPadding on the ScalingLazyColumn. If the developer wants custom
- * control over position and spacing they can switch off autoCentering and provide contentPadding.
+ * the first item (index == 0) before it.
*
* @param initialCenterItemScrollOffset the initial value for
* [ScalingLazyListState.centerItemScrollOffset] in pixels
@@ -69,11 +64,7 @@
*
* @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
* defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
- * the first item (index == 0) before it. Note that if [ScalingLazyColumn] autoCentering property is
- * set to true then space (padding) will be added such that items with index
- * [initialCenterItemIndex] or greater will be able to be scrolled to the center of the viewport. If
- * the developer wants to add additional space to allow other list items to also be scrollable to
- * the center they can use contentPadding on the ScalingLazyColumn.
+ * the first item (index == 0) before it.
*
* If the developer wants custom control over position and spacing they can switch off autoCentering
* and provide contentPadding.
@@ -96,7 +87,7 @@
internal val viewportHeightPx = mutableStateOf<Int?>(null)
internal val reverseLayout = mutableStateOf<Boolean?>(null)
internal val anchorType = mutableStateOf<ScalingLazyListAnchorType?>(null)
- internal val autoCentering = mutableStateOf<Boolean?>(null)
+ internal val autoCentering = mutableStateOf<AutoCenteringParams?>(null)
internal val initialized = mutableStateOf<Boolean>(false)
/**
@@ -112,7 +103,7 @@
gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
anchorType.value == null || reverseLayout.value == null ||
beforeContentPaddingPx.value == null || autoCentering.value == null ||
- !autoCentering.value!! || layoutInfo.visibleItemsInfo.isEmpty()
+ autoCentering.value == null || layoutInfo.visibleItemsInfo.isEmpty()
) {
0
} else {
@@ -150,7 +141,7 @@
if (extraPaddingPx.value == null || scalingParams.value == null ||
gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
anchorType.value == null || reverseLayout.value == null ||
- beforeContentPaddingPx.value == null || autoCentering.value == null
+ beforeContentPaddingPx.value == null
) {
EmptyScalingLazyListLayoutInfo
} else {
@@ -184,7 +175,7 @@
scalingParams.value!!,
beforeContentPaddingPx.value!!,
anchorType.value!!,
- autoCentering.value!!,
+ autoCentering.value,
initialized.value
)
visibleItemsInfo.add(
@@ -227,7 +218,7 @@
scalingParams.value!!,
beforeContentPaddingPx.value!!,
anchorType.value!!,
- autoCentering.value!!,
+ autoCentering.value,
initialized.value
)
visibleItemsInfo.add(0, itemInfo)
@@ -261,7 +252,7 @@
scalingParams.value!!,
beforeContentPaddingPx.value!!,
anchorType.value!!,
- autoCentering.value!!,
+ autoCentering.value,
initialized.value
)
@@ -274,7 +265,7 @@
}
}
val totalItemsCount =
- if (autoCentering.value!!) {
+ if (autoCentering.value != null) {
(lazyListState.layoutInfo.totalItemsCount - 2).coerceAtLeast(0)
} else {
lazyListState.layoutInfo.totalItemsCount
@@ -290,7 +281,7 @@
// Not already initialized
!initialized.value && (
// Not autoCentering
- !autoCentering.value!! || (
+ autoCentering.value == null || (
lazyListState.layoutInfo.visibleItemsInfo.size >= 2 && (
// or Empty list (other than the 2 spacers)
lazyListState.layoutInfo.visibleItemsInfo.size == 2 ||
@@ -411,7 +402,7 @@
initialCenterItemScrollOffset = scrollOffset
return
}
- val lazyListStateIndex = if (autoCentering.value!!) index + 1 else index
+ val lazyListStateIndex = if (autoCentering.value != null) index + 1 else index
val offsetToCenterOfViewport =
beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
@@ -462,7 +453,7 @@
) {
// Convert the index to take into account the Spacer added to the underlying LazyList before
// the first ScalingLazyColumn list item
- val lazyListStateIndex = if (autoCentering.value!!) index + 1 else index
+ val lazyListStateIndex = if (autoCentering.value != null) index + 1 else index
val offsetToCenterOfViewport =
beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
@@ -492,24 +483,24 @@
}
private fun discardAutoCenteringListItem(item: LazyListItemInfo): Boolean =
- autoCentering.value!! &&
+ autoCentering.value != null &&
(item.index == 0 || item.index == lazyListState.layoutInfo.totalItemsCount - 1)
/**
* Calculate the amount of top padding needed (if any) to make sure that the
- * [initialCenterItemIndex] item can be placed in the center of the viewport at
- * [initialCenterItemScrollOffset]
+ * [AutoCenteringParams.itemIndex] item can be placed in the center of the viewport at
+ * [AutoCenteringParams.itemOffset]
*/
private fun calculateTopAutoCenteringPaddingPx(
visibleItems: List<ScalingLazyListItemInfo>,
totalItemCount: Int
): Int {
- if (! autoCentering.value!! || visibleItems.isEmpty() ||
+ if (autoCentering.value == null || visibleItems.isEmpty() ||
visibleItems.first().index != 0) return 0
// Work out the index we want to find - if there are less items in the list than would be
// needed to make initialItemIndex be visible then use the last visible item
- val itemIndexToFind = initialCenterItemIndex.coerceAtMost(totalItemCount - 1)
+ val itemIndexToFind = autoCentering.value!!.itemIndex.coerceAtMost(totalItemCount - 1)
// Find the initialCenterItem, if it is null that means it is not in view - therefore
// we have more than enough content before it to make sure it can be scrolled to the center
@@ -534,22 +525,22 @@
/**
* Calculate the amount of top padding needed (if any) to make sure that the
- * [initialCenterItemIndex] item can be placed in the center of the viewport at
- * [initialCenterItemScrollOffset]
+ * [AutoCenteringParams.itemIndex] item can be placed in the center of the viewport at
+ * [AutoCenteringParams.itemOffset]
*/
private fun calculateTopAutoCenteringPaddingFromLazyListItemInfo(
visibleItems: List<LazyListItemInfo>,
totalItemCount: Int
): Int {
// Check is list is empty or we are not at the start of the visible items
- if (visibleItems.isEmpty() || visibleItems.isEmpty() ||
+ if (autoCentering.value == null || visibleItems.isEmpty() ||
visibleItems[0].index != 0) return 0
// Work out the index we want to find - if there are less items in the list than would be
// needed to make initialItemIndex be visible then use the last visible item. The -3 is to
// allow for the spacers, i.e. an underlying list of size 3 has 2 spacers in index 0 and 2
// and one real item in index 1.
- val itemIndexToFind = (initialCenterItemIndex + 1).coerceAtMost(totalItemCount - 3)
+ val itemIndexToFind = (autoCentering.value!!.itemIndex + 1).coerceAtMost(totalItemCount - 3)
// Find the initialCenterItem, if it is null that means it is not in view - therefore
// we have more than enough content before it to make sure it can be scrolled to the center
@@ -578,12 +569,12 @@
} else {
viewportHeightPx.value!! / 2f -
unadjustedSize / 2f
- } - gapBetweenItemsPx.value!! - initialCenterItemScrollOffset
+ } - gapBetweenItemsPx.value!! - autoCentering.value!!.itemOffset
private fun calculateBottomAutoCenteringPaddingPx(
visibleItemsInfo: List<ScalingLazyListItemInfo>,
totalItemsCount: Int
- ) = if (autoCentering.value!! && visibleItemsInfo.isNotEmpty() &&
+ ) = if (autoCentering.value != null && visibleItemsInfo.isNotEmpty() &&
visibleItemsInfo.last().index == totalItemsCount - 1
) {
if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 22075d5..824eee3 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -324,7 +324,7 @@
*/
public suspend fun snapTo(targetValue: SwipeToDismissValue) = swipeableState.snapTo(targetValue)
- companion object {
+ private companion object {
private fun <T> SwipeableState<T>.edgeNestedScrollConnection(
edgeTouched: State<Boolean>
): NestedScrollConnection =
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index aa03896..2eb89b8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -30,8 +30,10 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
import androidx.wear.compose.foundation.CurvedScope
import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.padding
import androidx.wear.compose.material.TimeTextDefaults.CurvedTextSeparator
import androidx.wear.compose.material.TimeTextDefaults.TextSeparator
import androidx.wear.compose.material.TimeTextDefaults.timeFormat
@@ -209,8 +211,8 @@
) {
curvedText(
text = "·",
- contentArcPadding = contentArcPadding,
- style = curvedTextStyle
+ style = curvedTextStyle,
+ modifier = CurvedModifier.padding(contentArcPadding)
)
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
index 323f922..fac57f8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
@@ -382,7 +382,7 @@
) {
ScalingLazyColumn(
state = scrollState,
- autoCentering = false,
+ autoCentering = null,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = verticalArrangement,
contentPadding = contentPadding,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 550fa41..1aa204d 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
targetSdk 30
- versionCode 5
- versionName "1.4"
+ versionCode 6
+ versionName "1.5"
// Change the APK name to match the *testapp regex we use to pick up APKs for testing as
// part of CI.
archivesBaseName = "wear-compose-demos-testapp"
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
index 42f434d..89a1b35 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
@@ -39,7 +39,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.AnchorType
-import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedAlignment
import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
@@ -50,6 +49,7 @@
import androidx.wear.compose.foundation.curvedColumn
import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
import androidx.wear.compose.foundation.sizeIn
import androidx.wear.compose.foundation.weight
import androidx.wear.compose.material.Text
@@ -229,7 +229,7 @@
fontSize = 24.sp
),
angularDirection = CurvedDirection.Angular.Reversed,
- contentArcPadding = ArcPaddingValues(angular = 5.dp),
+ modifier = CurvedModifier.padding(angular = 5.dp),
// TODO: Re-add when we implement alignment modifiers.
// modifier = Modifier.radialAlignment(RadialAlignment.Inner)
)
@@ -281,7 +281,7 @@
CurvedTextStyle(
fontSize = 24.sp
),
- contentArcPadding = ArcPaddingValues(angular = 5.dp),
+ modifier = CurvedModifier.padding(angular = 5.dp),
)
curvedColumn {
repeat(3) {
@@ -291,11 +291,11 @@
curvedRow {
basicCurvedText(
"after",
- contentArcPadding = ArcPaddingValues(angular = 4.dp)
+ modifier = CurvedModifier.padding(angular = 4.dp),
)
basicCurvedText(
"end",
- contentArcPadding = ArcPaddingValues(angular = 4.dp)
+ modifier = CurvedModifier.padding(angular = 4.dp),
)
}
}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
index 9da029a..76fe1ef 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
@@ -93,7 +93,7 @@
) {
ScalingLazyColumn(
state = listState,
- autoCentering = false
+ autoCentering = null
) {
items(
count = if (smallList) 3 else 10
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
index ea02b8a..756bf60 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
@@ -41,10 +41,11 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
-import androidx.wear.compose.foundation.ArcPaddingValues
import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.foundation.basicCurvedText
+import androidx.wear.compose.foundation.padding
import androidx.wear.compose.material.AppCard
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Card
@@ -238,7 +239,7 @@
color = Color.White,
background = background
),
- contentArcPadding = ArcPaddingValues(2.dp)
+ modifier = CurvedModifier.padding(2.dp)
)
}
CurvedLayout(anchor = 310f) {
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index b03ceb2..a72bfba 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -18,22 +18,43 @@
plugins {
id("AndroidXPlugin")
+ id("kotlin-android")
id("com.android.library")
+ id("com.google.protobuf")
}
dependencies {
api("androidx.annotation:annotation:1.2.0")
- api(project(":wear:tiles:tiles"))
-
+ implementation(project(":wear:tiles:tiles"))
implementation(project(":wear:tiles:tiles-proto"))
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation("androidx.core:core:1.7.0")
+ androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(project(":wear:tiles:tiles-renderer"))
+ androidTestRuntimeOnly(project(path: ":wear:tiles:tiles-proto", configuration: "shadow"))
+ androidTestImplementation("com.google.protobuf:protobuf-java:3.10.0")
+
annotationProcessor(libs.nullaway)
}
android {
defaultConfig {
- minSdkVersion 25
+ minSdkVersion 26
}
+
+ sourceSets {
+ androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-material"
+ }
+
+ defaultConfig {
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
namespace "androidx.wear.tiles.material"
}
diff --git a/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml b/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
index 4d68dc2..55cb77e 100644
--- a/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
+++ b/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
@@ -14,6 +14,21 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.wear.tiles.material">
+ <application
+ android:label="Golden Tests"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:taskAffinity="">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="androidx.wear.tiles.material.testapp.GoldenTestActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
</manifest>
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
new file mode 100644
index 0000000..3022faf
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.tiles.DeviceParametersBuilders;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenTest {
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+
+ public MaterialGoldenTest(LayoutElement layoutElement, String expected) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{1}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ float scale = displayMetrics.density;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // Not important for components.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ HashMap<LayoutElement, String> testCases =
+ generateTestCases(context, deviceParameters, "");
+
+ return testCases.entrySet()
+ .stream()
+ .map(test -> new Object[]{test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
new file mode 100644
index 0000000..25b116e
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.tiles.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.tiles.DeviceParametersBuilders;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenXLTest {
+ /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+ tests together, first all parametrization (data()) methods are called, and then individual
+ tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+ each test. */
+ private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+ private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+ private static final float FONT_SCALE_XXXL = 1.24f;
+
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+
+ public MaterialGoldenXLTest(LayoutElement layoutElement, String expected) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{1}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ currentDisplayMetrics.setTo(displayMetrics);
+ displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+ float scale = displayMetrics.density;
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // Not important for components.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ HashMap<LayoutElement, String> testCases =
+ generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+ // Restore state before this method, so other test have correct context.
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+
+ return testCases.entrySet()
+ .stream()
+ .map(test -> new Object[]{test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @Parameterized.BeforeParam
+ public static void restoreBefore() {
+ OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+ getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+ }
+
+ @Parameterized.AfterParam
+ public static void restoreAfter() {
+ getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+ }
+
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java
new file mode 100644
index 0000000..f707b14
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.test.screenshot.matchers.MSSIMMatcher;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.material.testapp.GoldenTestActivity;
+
+public class RunnerUtils {
+ // This isn't totally ideal right now. The screenshot tests run on a phone, so emulate some
+ // watch dimensions here.
+ public static final int SCREEN_WIDTH = 390;
+ public static final int SCREEN_HEIGHT = 390;
+
+ private RunnerUtils() {}
+
+ static void runSingleScreenshotTest(@NonNull AndroidXScreenshotTestRule rule,
+ @NonNull LayoutElementBuilders.LayoutElement layoutElement,
+ @NonNull String expected) {
+ byte[] layoutElementPayload = layoutElement.toLayoutElementProto().toByteArray();
+
+ Intent startIntent =
+ new Intent(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ GoldenTestActivity.class);
+ startIntent.putExtra("layout", layoutElementPayload);
+
+ ActivityScenario<GoldenTestActivity> scenario = ActivityScenario.launch(startIntent);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ try {
+ // Wait 1s after launching the activity. This allows for the old white layout in the
+ // bootstrap activity to fully go away before proceeding.
+ Thread.sleep(1000);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e("MaterialGoldenTest", "Error sleeping", ex);
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot(),
+ 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT
+ );
+ rule.assertBitmapAgainstGolden(bitmap, expected, new MSSIMMatcher());
+
+ // There's a weird bug (related to b/159805732) where, when calling .close() on
+ // ActivityScenario or calling finish() and immediately exiting the test, the test can hang
+ // on a white screen for 45s. Closing the activity here and waiting for 1s seems to fix
+ // this.
+ scenario.onActivity(Activity::finish);
+
+ try {
+ Thread.sleep(1000);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e("MaterialGoldenTest", "Error sleeping", ex);
+ }
+ }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java
new file mode 100644
index 0000000..60f61f3
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.wear.tiles.ColorBuilders.argb;
+import static androidx.wear.tiles.DimensionBuilders.dp;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+import static androidx.wear.tiles.material.ProgressIndicatorDefaults.GAP_END_ANGLE;
+import static androidx.wear.tiles.material.ProgressIndicatorDefaults.GAP_START_ANGLE;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.wear.tiles.ActionBuilders.LaunchAction;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+import androidx.wear.tiles.ModifiersBuilders.Clickable;
+
+import java.util.HashMap;
+
+public class TestCasesGenerator {
+ private TestCasesGenerator() {}
+
+ private static final String ICON_ID = "tile_icon";
+ private static final String AVATAR = "avatar_image";
+ public static final String NORMAL_SCALE_SUFFIX = "";
+ public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+
+ /**
+ * This function will append goldenSuffix on the name of the golden images that should be
+ * different for different user font sizes. Note that some of the golden will have the same name
+ * as it should point on the same size independent image.
+ */
+ @NonNull
+ static HashMap<LayoutElement, String> generateTestCases(
+ @NonNull Context context,
+ @NonNull DeviceParameters deviceParameters,
+ @NonNull String goldenSuffix) {
+ Clickable clickable =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ String mainText = "Primary label";
+ String labelText = "Secondary label";
+ String largeChipText = "Action";
+ HashMap<LayoutElement, String> testCases = new HashMap<>();
+
+ testCases.put(
+ new Button.Builder(context, clickable).setIconContent(ICON_ID).build(),
+ "default_icon_button_golden" + NORMAL_SCALE_SUFFIX);
+ testCases.put(
+ new Button.Builder(context, clickable)
+ .setButtonColors(ButtonDefaults.SECONDARY_BUTTON_COLORS)
+ .setIconContent(ICON_ID)
+ .setSize(ButtonDefaults.EXTRA_LARGE_BUTTON_SIZE)
+ .build(),
+ "extralarge_secondary_icon_after_button_golden" + NORMAL_SCALE_SUFFIX);
+ testCases.put(
+ new Button.Builder(context, clickable)
+ .setSize(ButtonDefaults.LARGE_BUTTON_SIZE)
+ .setButtonColors(ButtonDefaults.SECONDARY_BUTTON_COLORS)
+ .setIconContent(ICON_ID, dp(40))
+ .build(),
+ "large_secondary_icon_40size_button_golden" + NORMAL_SCALE_SUFFIX);
+ testCases.put(
+ new Button.Builder(context, clickable)
+ .setButtonColors(new ButtonColors(Color.YELLOW, Color.GREEN))
+ .setSize(ButtonDefaults.EXTRA_LARGE_BUTTON_SIZE)
+ .setContent(
+ new Text.Builder(context, "ABC")
+ .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+ .setItalic(true)
+ .setColor(argb(Color.GREEN))
+ .build())
+ .build(),
+ "extralarge_custom_text_custom_sizefont_button_golden" + goldenSuffix);
+ testCases.put(
+ new Button.Builder(context, clickable).setTextContent("ABC").build(),
+ "default_text_button_golden" + goldenSuffix);
+ testCases.put(
+ new Button.Builder(context, clickable).setImageContent(AVATAR).build(),
+ "default_image_button_golden" + NORMAL_SCALE_SUFFIX);
+
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryTextContent(mainText)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .build(),
+ "default_chip_maintext_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryTextLabelContent(mainText, labelText)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .build(),
+ "default_chip_maintextlabeltext_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryTextIconContent(mainText, ICON_ID)
+ .build(),
+ "default_chip_maintexticon_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setChipColors(ChipDefaults.SECONDARY_COLORS)
+ .setPrimaryTextContent(mainText)
+ .build(),
+ "secondary_chip_maintext_centered_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setWidth(130)
+ .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+ .setChipColors(
+ new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+ .build(),
+ "custom_chip_all_overflows_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+ .build(),
+ "default_chip_all_centered_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_END)
+ .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+ .build(),
+ "default_chip_all_rigthalign_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryTextIconContent(mainText, ICON_ID)
+ .setWidth(150)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setChipColors(
+ new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+ .build(),
+ "custom_chip_icon_primary_overflows_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setContent(
+ new LayoutElementBuilders.Box.Builder()
+ .addContent(
+ new Text.Builder(context, "random text")
+ .setTypography(Typography.TYPOGRAPHY_TITLE3)
+ .setItalic(true)
+ .build())
+ .build())
+ .build(),
+ "chip_custom_content_centered_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setChipColors(ChipDefaults.SECONDARY_COLORS)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setContent(
+ new LayoutElementBuilders.Row.Builder()
+ .addContent(
+ new Text.Builder(context, "text1")
+ .setTypography(Typography.TYPOGRAPHY_TITLE3)
+ .setItalic(true)
+ .setColor(argb(Color.WHITE))
+ .build())
+ .addContent(
+ new Text.Builder(context, "text2")
+ .setTypography(Typography.TYPOGRAPHY_TITLE2)
+ .setColor(argb(Color.YELLOW))
+ .build())
+ .build())
+ .setWidth(150)
+ .build(),
+ "chip_custom_content_leftaligned_golden" + goldenSuffix);
+ testCases.put(
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryTextContent("abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde")
+ .build(),
+ "chip_2lines_primary_overflows_golden" + goldenSuffix);
+
+ // Different text lengths to test expanding the width based on the size of text. If it's
+ // more than 9, the rest will be deleted.
+ testCases.put(
+ new CompactChip.Builder(context, "Ab", clickable, deviceParameters).build(),
+ "compactchip_default_len2_golden" + goldenSuffix);
+ testCases.put(
+ new CompactChip.Builder(context, "Abcde", clickable, deviceParameters).build(),
+ "compactchip_default_len5_golden" + goldenSuffix);
+ testCases.put(
+ new CompactChip.Builder(context, "Abcdefghi", clickable, deviceParameters).build(),
+ "compactchip_default_len9_golden" + goldenSuffix);
+ testCases.put(
+ new CompactChip.Builder(context, "AbcdefghiEXTRA", clickable,
+ deviceParameters).build(),
+ "compactchip_default_toolong_golden" + goldenSuffix);
+ testCases.put(
+ new CompactChip.Builder(context, "Action", clickable, deviceParameters)
+ .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
+ .build(),
+ "compactchip_custom_default_golden" + goldenSuffix);
+
+ testCases.put(
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters).build(),
+ "titlechip_default_golden" + goldenSuffix);
+ testCases.put(
+ new TitleChip.Builder(context, "abcdeabcdeabcdeEXTRA", clickable,
+ deviceParameters).build(),
+ "titlechip_default_texttoolong_golden" + goldenSuffix);
+ testCases.put(
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setChipColors(ChipDefaults.TITLE_SECONDARY_COLORS)
+ .build(),
+ "titlechip_leftalign_secondary_default_golden" + goldenSuffix);
+ testCases.put(
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setChipColors(new ChipColors(Color.YELLOW, Color.BLUE))
+ .setWidth(150)
+ .build(),
+ "titlechip_centered_custom_150_secondary_default_golden" + goldenSuffix);
+
+ testCases.put(
+ new CircularProgressIndicator.Builder().build(),
+ "default_full_circularprogressindicator");
+ testCases.put(
+ new CircularProgressIndicator.Builder()
+ .setStartAngle(GAP_START_ANGLE)
+ .setEndAngle(GAP_END_ANGLE)
+ .build(),
+ "default_gap_circularprogressindicator");
+ testCases.put(
+ new CircularProgressIndicator.Builder().setProgress(0.25f).build(),
+ "default_full_90_circularprogressindicator");
+ testCases.put(
+ new CircularProgressIndicator.Builder()
+ .setProgress(0.25f)
+ .setStartAngle(GAP_START_ANGLE)
+ .setEndAngle(GAP_END_ANGLE)
+ .build(),
+ "default_gap_90_circularprogressindicator");
+ testCases.put(
+ new CircularProgressIndicator.Builder()
+ .setStartAngle(45)
+ .setEndAngle(270)
+ .setProgress(0.2f)
+ .setStrokeWidth(12)
+ .setCircularProgressIndicatorColors(
+ new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
+ .build(),
+ "custom_gap_45_circularprogressindicator");
+
+ testCases.put(
+ new Text.Builder(context, "Testing").build(),
+ "default_text_golden" + goldenSuffix);
+ testCases.put(
+ new Text.Builder(context, "Testing text.")
+ .setItalic(true)
+ .setColor(argb(Color.YELLOW))
+ .setTypography(Typography.TYPOGRAPHY_BODY2)
+ .build(),
+ "custom_text_golden" + goldenSuffix);
+ testCases.put(
+ new Text.Builder(context, "abcdeabcdeabcde").build(),
+ "overflow_text_golden" + goldenSuffix);
+
+ return testCases;
+ }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
new file mode 100644
index 0000000..f775a6f
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material.testapp;
+
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.LayoutElementBuilders.Layout;
+import androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId;
+import androidx.wear.tiles.ResourceBuilders.ImageResource;
+import androidx.wear.tiles.ResourceBuilders.Resources;
+import androidx.wear.tiles.material.R;
+import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.tiles.renderer.TileRenderer;
+
+import java.util.concurrent.Executor;
+
+
+public class GoldenTestActivity extends Activity {
+ private static final String ICON_ID = "tile_icon";
+ private static final String AVATAR = "avatar_image";
+
+ @Override
+ @SuppressWarnings("deprecation")
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ byte[] layoutPayload = getIntent().getExtras().getByteArray("layout");
+
+ LayoutElement layoutElementProto;
+ try {
+ layoutElementProto =
+ LayoutElement.parseFrom(layoutPayload);
+ } catch (Exception ex) {
+ // It's a test, just rethrow.
+ throw new IllegalArgumentException("Could not deserialize layout proto", ex);
+ }
+
+ LayoutElementBuilders.LayoutElement rootLayoutElement =
+ LayoutElementBuilders.LayoutElement.fromLayoutElementProto(layoutElementProto);
+
+ Context appContext = getApplicationContext();
+ FrameLayout root = new FrameLayout(appContext);
+ root.setLayoutParams(new LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+
+ Layout layout = new Layout.Builder().setRoot(rootLayoutElement).build();
+
+ Executor mainExecutor = ContextCompat.getMainExecutor(getApplicationContext());
+
+ Resources resources = generateResources();
+ TileRenderer renderer =
+ new TileRenderer(
+ appContext,
+ layout,
+ resources,
+ mainExecutor,
+ i -> {});
+
+ View firstChild = renderer.inflate(root);
+
+ // Simulate what the thing outside the renderer should do. Center the contents.
+ LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
+ layoutParams.gravity = Gravity.CENTER;
+
+ // Set the activity to be full screen so when we crop the Bitmap we don't get time bar etc.
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ setContentView(root, new ViewGroup.LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+ super.onCreate(savedInstanceState);
+ }
+
+ private static Resources generateResources() {
+ return new Resources.Builder()
+ .addIdToImageMapping(
+ ICON_ID,
+ new ImageResource.Builder()
+ .setAndroidResourceByResId(
+ new AndroidImageResourceByResId.Builder()
+ .setResourceId(R.drawable.tile_icon)
+ .build())
+ .build())
+ .addIdToImageMapping(
+ AVATAR,
+ new ImageResource.Builder()
+ .setAndroidResourceByResId(
+ new AndroidImageResourceByResId.Builder()
+ .setResourceId(R.drawable.avatar)
+ .build())
+ .build())
+ .build();
+ }
+}
+
diff --git a/wear/tiles/tiles-material/src/main/res/drawable/avatar.png b/wear/tiles/tiles-material/src/main/res/drawable/avatar.png
new file mode 100644
index 0000000..a6da988
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/res/drawable/avatar.png
Binary files differ
diff --git a/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml b/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml
new file mode 100644
index 0000000..21eb853
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000"
+ android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/wear/watchface/watchface-client/api/1.1.0-beta01.txt b/wear/watchface/watchface-client/api/1.1.0-beta01.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/1.1.0-beta01.txt
@@ -203,19 +203,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -226,8 +213,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -203,19 +203,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -226,8 +213,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt b/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
index 95ff68c..92202fd 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
@@ -102,7 +102,7 @@
method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
- method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
+ method @androidx.wear.watchface.WatchFaceFlavorsExperimental public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
method @AnyThread public boolean isConnectionAlive();
@@ -207,19 +207,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -230,9 +217,9 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method @androidx.wear.watchface.WatchFaceFlavorsExperimental public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index 95ff68c..92202fd 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -102,7 +102,7 @@
method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
- method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
+ method @androidx.wear.watchface.WatchFaceFlavorsExperimental public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
method @AnyThread public boolean isConnectionAlive();
@@ -207,19 +207,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -230,9 +217,9 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method @androidx.wear.watchface.WatchFaceFlavorsExperimental public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt b/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
@@ -203,19 +203,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -226,8 +213,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -203,19 +203,6 @@
ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
}
- public final class WatchFaceException extends java.lang.Exception {
- ctor public WatchFaceException(Exception e, int reason);
- method public int getReason();
- property public final int reason;
- field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
- field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
- field public static final int UNKNOWN = 3; // 0x3
- field public static final int WATCHFACE_DIED = 1; // 0x1
- }
-
- public static final class WatchFaceException.Companion {
- }
-
public final class WatchFaceExceptionKt {
}
@@ -226,8 +213,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
- method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public boolean isUserStyleSchemaStatic();
property public abstract boolean isUserStyleSchemaStatic;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
index a2ae439..4e73052 100644
--- a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
+++ b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,13 @@
<application android:requestLegacyExternalStorage="true">
<service android:name="androidx.wear.watchface.client.test.WatchFaceControlTestService"/>
<service android:name="androidx.wear.watchface.client.test.TestNopCanvasWatchFaceService"/>
+ <service
+ android:name="androidx.wear.watchface.client.test.OutdatedWatchFaceControlTestService">
+ <meta-data android:name="androidx.wear.watchface.xml_version" android:value="99999" />
+ <meta-data
+ android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+ android:resource="@xml/xml_watchface" />
+ </service>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt
new file mode 100644
index 0000000..aa19f94
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client.test
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+/**
+ * Test WatchFaceControlService which has obsolete XML version in manifest.
+ */
+public class OutdatedWatchFaceControlTestService : Service() {
+ override fun onBind(p0: Intent?): IBinder? {
+ // It is not assumed to be called
+ throw NotImplementedError()
+ }
+}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
index 94e1489..ad5fda7 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
@@ -358,4 +358,22 @@
Truth.assertThat(right.systemDataSourceFallbackDefaultType).isEqualTo(
ComplicationType.SHORT_TEXT)
}
+
+ @Test
+ public fun xmlVersionCompatibility() {
+ Truth.assertThat(
+ WatchFaceMetadataClient.isXmlVersionCompatible(context, context.resources)).isTrue()
+ Truth.assertThat(
+ WatchFaceMetadataClient.isXmlVersionCompatible(
+ context,
+ context.resources,
+ ComponentName(context, OutdatedWatchFaceControlTestService::class.java)
+ )).isFalse()
+ Truth.assertThat(
+ WatchFaceMetadataClient.isXmlVersionCompatible(
+ context,
+ context.resources,
+ ComponentName("non.existing.package", "non.existing.package.Service")
+ )).isFalse()
+ }
}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt
new file mode 100644
index 0000000..fac5538
--- /dev/null
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client
+
+import android.os.Build
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+
+internal class Api30Helper {
+ @RequiresApi(Build.VERSION_CODES.R)
+ internal companion object {
+ internal fun toRuntimeExpression(e: RemoteException) = e.rethrowAsRuntimeException()
+ }
+}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index 5b88032..f96a66a2 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -79,9 +79,13 @@
@Throws(RemoteException::class)
public fun getUserStyleSchemaDigestHash(): ByteArray = ByteArray(0)
- /** The watch face's [UserStyleFlavors] if any. */
+ /**
+ * Returns the watch face's [UserStyleFlavors] if any.
+ *
+ * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+ * request or there was a communication problem with watch face process.
+ */
@WatchFaceFlavorsExperimental
- @Throws(WatchFaceException::class)
public fun getUserStyleFlavors(): UserStyleFlavors = UserStyleFlavors()
/**
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
index eb5f023..53d358a 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
@@ -16,62 +16,15 @@
package androidx.wear.watchface.client
-import android.os.DeadObjectException
+import android.os.Build
import android.os.RemoteException
-import android.os.TransactionTooLargeException
-import androidx.annotation.IntDef
-/**
- * Why the remote watch face query failed.
- * @hide
- **/
-@Retention(AnnotationRetention.SOURCE)
-@IntDef(
- WatchFaceException.WATCHFACE_DIED,
- WatchFaceException.TRANSACTION_TOO_LARGE,
- WatchFaceException.UNKNOWN
-)
-annotation class WatchFaceExceptionReason
-
-/**
- * The watch face threw an exception while trying to service the request.
- *
- * @property reason The [WatchFaceExceptionReason] for the exception.
- */
-public class WatchFaceException(
- e: Exception,
- @WatchFaceExceptionReason val reason: Int
-) : Exception(e) {
-
- companion object {
- /**
- * The watchface process died. Connecting again might work, but this isn't guaranteed.
- */
- const val WATCHFACE_DIED = 1
-
- /**
- * The watchface tried to send us too much data. Currently the limit on binder
- * transactions is 1mb. See [TransactionTooLargeException] for more details.
- */
- const val TRANSACTION_TOO_LARGE = 2
-
- /**
- * The watch face threw an exception, typically during initialization. Depending on the
- * nature of the problem this might be a transient issue or it might occur every time
- * for that particular watch face.
- */
- const val UNKNOWN = 3
- }
-}
-
-@Throws(WatchFaceException::class)
internal fun <R> callRemote(task: () -> R): R =
try {
task()
- } catch (e: DeadObjectException) {
- throw WatchFaceException(e, WatchFaceException.WATCHFACE_DIED)
- } catch (e: TransactionTooLargeException) {
- throw WatchFaceException(e, WatchFaceException.TRANSACTION_TOO_LARGE)
} catch (e: RemoteException) {
- throw WatchFaceException(e, WatchFaceException.UNKNOWN)
+ if (Build.VERSION.SDK_INT >= 30)
+ throw Api30Helper.toRuntimeExpression(e)
+ else
+ throw RuntimeException(e)
}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
index c39005c..4c670a1 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
@@ -21,10 +21,12 @@
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
+import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.RectF
import android.os.Bundle
import android.os.IBinder
+import android.util.Log
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
@@ -55,6 +57,9 @@
public interface WatchFaceMetadataClient : AutoCloseable {
public companion object {
+ /** @hide */
+ private const val TAG = "WatchFaceMetadataClient"
+
/**
* Constructs a [WatchFaceMetadataClient] for fetching metadata for the specified watch
* face.
@@ -90,11 +95,50 @@
}
/** @hide */
+ private const val ANDROIDX_WATCHFACE_XML_VERSION = "androidx.wear.watchface.xml_version"
+ /** @hide */
+ private const val ANDROIDX_WATCHFACE_CONTROL_SERVICE =
+ "androidx.wear.watchface.control.WatchFaceControlService"
+
+ @Suppress("DEPRECATION") // getServiceInfo
+ internal fun isXmlVersionCompatible(
+ context: Context,
+ resources: Resources,
+ controlServiceComponentName: ComponentName = ComponentName(
+ context, ANDROIDX_WATCHFACE_CONTROL_SERVICE)
+ ): Boolean {
+ val version = try {
+ context.packageManager.getServiceInfo(
+ controlServiceComponentName,
+ PackageManager.GET_META_DATA or PackageManager.MATCH_DISABLED_COMPONENTS
+ ).metaData.getInt(ANDROIDX_WATCHFACE_XML_VERSION, 0)
+ } catch (exception: PackageManager.NameNotFoundException) {
+ // WatchFaceControlService may be missing in case WF is built with
+ // pre-androidx watchface library.
+ return false
+ }
+
+ val ourVersion = resources.getInteger(
+ androidx.wear.watchface.R.integer.watch_face_xml_version)
+
+ if (version > ourVersion) {
+ Log.w(TAG, "WatchFaceControlService version ($version) " +
+ "of $controlServiceComponentName is higher than $ourVersion")
+ return false
+ }
+
+ return true
+ }
+
+ /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Suppress("DEPRECATION")
open class ParserProvider {
// Open to allow testing without having to install the sample app.
open fun getParser(context: Context, watchFaceName: ComponentName): XmlResourceParser? {
+ if (!isXmlVersionCompatible(context, context.resources))
+ return null
+
return context.packageManager.getServiceInfo(
watchFaceName,
PackageManager.GET_META_DATA
@@ -165,8 +209,10 @@
/**
* Returns the watch face's [UserStyleSchema].
+ *
+ * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+ * request or there was a communication problem with watch face process.
*/
- @Throws(WatchFaceException::class)
public fun getUserStyleSchema(): UserStyleSchema
/**
@@ -179,14 +225,18 @@
/**
* Returns a map of [androidx.wear.watchface.ComplicationSlot] ID to [ComplicationSlotMetadata]
* for each slot in the watch face's [androidx.wear.watchface.ComplicationSlotsManager].
+ *
+ * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+ * request or there was a communication problem with watch face process.
*/
- @Throws(WatchFaceException::class)
public fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata>
/**
* Returns the watch face's [UserStyleFlavors].
+ *
+ * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+ * request or there was a communication problem with watch face process.
*/
- @Throws(WatchFaceException::class)
@WatchFaceFlavorsExperimental
public fun getUserStyleFlavors(): UserStyleFlavors
}
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 47e166d..868b87e 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -137,8 +137,8 @@
complicationData.type == defaultComplicationData.type
) {
"TimelineEntry's complicationData must have the same type as the " +
- "defaultComplicationData. Found ${complicationData.type} expected " +
- "${defaultComplicationData.type}."
+ "defaultComplicationData or be NoDataComplicationData. Found " +
+ "${complicationData.type} expected ${defaultComplicationData.type}."
}
require(!complicationData.hasPlaceholderFields()) {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index 0c31e4b..0dad6db 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -262,6 +262,8 @@
private static final String FIELD_TIMELINE_START_TIME = "TIMELINE_START_TIME";
private static final String FIELD_TIMELINE_END_TIME = "TIMELINE_END_TIME";
private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
+ private static final String FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE";
+ private static final String FIELD_PLACEHOLDER_FIELDS = "PLACEHOLDER_FIELDS";
private static final String FIELD_PLACEHOLDER_TYPE = "PLACEHOLDER_TYPE";
private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
@@ -351,6 +353,7 @@
FIELD_LONG_TITLE,
FIELD_MAX_VALUE,
FIELD_MIN_VALUE,
+ FIELD_PLACEHOLDER_FIELDS,
FIELD_PLACEHOLDER_TYPE,
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
@@ -389,19 +392,9 @@
}
ComplicationData(int type, Bundle fields) {
+ mType = type;
mFields = fields;
mFields.setClassLoader(getClass().getClassLoader());
- // If this is a placeholder, coerce to TYPE_NO_DATA.
- // If this is defined within a timeline, we assume the type of the outer ComplicationData
- // applies to all elements in the timeline and we can just use the passed in type. The only
- // exception is if we get a NO_DATA ComplicationData. In that case, we can check whether
- // the placeholder type is included the serialization to determine if NO_DATA was passed
- // in and coerce the type to NO_DATA.
- if (mFields.containsKey(FIELD_PLACEHOLDER_TYPE)) {
- mType = TYPE_NO_DATA;
- } else {
- mType = type;
- }
}
private ComplicationData(@NonNull Parcel in) {
@@ -411,7 +404,7 @@
@RequiresApi(api = Build.VERSION_CODES.P)
private static class SerializedForm implements Serializable {
- private static final int VERSION_NUMBER = 5;
+ private static final int VERSION_NUMBER = 6;
@NonNull
ComplicationData mComplicationData;
@@ -480,9 +473,6 @@
if (isFieldValidForType(FIELD_END_TIME, type)) {
oos.writeLong(mComplicationData.getEndDateTimeMillis());
}
- if (isFieldValidForType(FIELD_PLACEHOLDER_TYPE, type)) {
- oos.writeInt(mComplicationData.getPlaceholderType());
- }
if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
ComponentName componentName = mComplicationData.getDataSource();
if (componentName == null) {
@@ -500,6 +490,16 @@
long end = mComplicationData.mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
oos.writeLong(end);
+ if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
+ ComplicationData placeholder = mComplicationData.getPlaceholder();
+ if (placeholder == null) {
+ oos.writeBoolean(false);
+ } else {
+ oos.writeBoolean(true);
+ new SerializedForm(placeholder).writeObject(oos);
+ }
+ }
+
// This has to be last, since it's recursive.
List<ComplicationData> timeline = mComplicationData.getTimelineEntries();
int timelineLength = (timeline != null) ? timeline.size() : 0;
@@ -572,12 +572,6 @@
if (isFieldValidForType(FIELD_END_TIME, type)) {
fields.putLong(FIELD_END_TIME, ois.readLong());
}
- if (isFieldValidForType(FIELD_PLACEHOLDER_TYPE, type)) {
- int placeholderType = ois.readInt();
- if (placeholderType != 0) {
- fields.putInt(FIELD_PLACEHOLDER_TYPE, placeholderType);
- }
- }
if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
String componentName = ois.readUTF();
if (componentName.isEmpty()) {
@@ -598,6 +592,18 @@
if (end != -1) {
fields.putLong(FIELD_TIMELINE_END_TIME, end);
}
+
+ if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
+ if (ois.readBoolean()) {
+ SerializedForm serializedPlaceholder = new SerializedForm();
+ serializedPlaceholder.readObject(ois);
+ fields.putInt(FIELD_PLACEHOLDER_TYPE,
+ serializedPlaceholder.mComplicationData.mType);
+ fields.putBundle(FIELD_PLACEHOLDER_FIELDS,
+ serializedPlaceholder.mComplicationData.mFields);
+ }
+ }
+
int timelineLength = ois.readInt();
if (timelineLength != 0) {
Parcelable[] parcels = new Parcelable[timelineLength];
@@ -731,11 +737,12 @@
}
ArrayList<ComplicationData> entries = new ArrayList<>();
for (Parcelable parcelable : bundles) {
- // Pass is the type of the outer complication data to the timeline entries by default.
- // The array should only contain elements of the same type. The only exception is the
- // NO_DATA type, which is allowed, but the code in the constructor is going to coerce
- // the type to NO_DATA if necessary.
- entries.add(new ComplicationData(mType, (Bundle) parcelable));
+ Bundle bundle = (Bundle) parcelable;
+ // Use the serialized FIELD_TIMELINE_ENTRY_TYPE or the outer type if it's not there.
+ // Usually the timeline entry type will be the same as the outer type, unless an entry
+ // contains NoDataComplicationData.
+ int type = bundle.getInt(FIELD_TIMELINE_ENTRY_TYPE, mType);
+ entries.add(new ComplicationData(type, (Bundle) parcelable));
}
return entries;
}
@@ -747,7 +754,13 @@
} else {
mFields.putParcelableArray(
FIELD_TIMELINE_ENTRIES,
- timelineEntries.stream().map(e -> e.mFields).toArray(Parcelable[]::new));
+ timelineEntries.stream().map(
+ e -> {
+ // This supports timeline entry of NoDataComplicationData.
+ e.mFields.putInt(FIELD_TIMELINE_ENTRY_TYPE, e.mType);
+ return e.mFields;
+ }
+ ).toArray(Parcelable[]::new));
}
}
@@ -1215,28 +1228,18 @@
}
/**
- * Returns true if the ComplicationData contains a placeholder type. I.e. if
- * {@link #getPlaceholderType} can succeed.
+ * Returns the placeholder ComplicationData if there is one or `null`.
*/
- public boolean hasPlaceholderType() {
- try {
- return isFieldValidForType(FIELD_PLACEHOLDER_TYPE, mType)
- && mFields.containsKey(FIELD_PLACEHOLDER_TYPE);
- } catch (BadParcelableException e) {
- return false;
- }
- }
-
- /**
- * Returns the type this complication is a placeholder for.
- *
- * <p>Valid only if the type of this complication data is {@link #TYPE_NO_DATA}.
- * Otherwise returns zero.
- */
- @ComplicationType
- public int getPlaceholderType() {
+ @Nullable
+ public ComplicationData getPlaceholder() {
+ checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
checkFieldValidForType(FIELD_PLACEHOLDER_TYPE, mType);
- return mFields.getInt(FIELD_PLACEHOLDER_TYPE);
+ if (!mFields.containsKey(FIELD_PLACEHOLDER_FIELDS)
+ || !mFields.containsKey(FIELD_PLACEHOLDER_TYPE)) {
+ return null;
+ }
+ return new ComplicationData(mFields.getInt(FIELD_PLACEHOLDER_TYPE),
+ mFields.getBundle(FIELD_PLACEHOLDER_FIELDS));
}
/**
@@ -1699,13 +1702,21 @@
}
/**
- * Sets the type this complication is a placeholder for.
+ * Sets the placeholder.
*
* <p>Returns this Builder to allow chaining.
*/
+ @SuppressLint("SyntheticAccessor")
@NonNull
- public Builder setPlaceholderType(@ComplicationType int placeholderType) {
- putIntField(FIELD_PLACEHOLDER_TYPE, placeholderType);
+ public Builder setPlaceholder(@Nullable ComplicationData placeholder) {
+ if (placeholder == null) {
+ mFields.remove(FIELD_PLACEHOLDER_FIELDS);
+ mFields.remove(FIELD_PLACEHOLDER_TYPE);
+ } else {
+ ComplicationData.checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
+ mFields.putBundle(FIELD_PLACEHOLDER_FIELDS, placeholder.mFields);
+ putIntField(FIELD_PLACEHOLDER_TYPE, placeholder.mType);
+ }
return this;
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 8520303..5141d8b 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -86,11 +86,6 @@
internal open fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
}
- internal fun asPlaceholderWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(NoDataComplicationData.TYPE.toWireComplicationType()).apply {
- fillWireComplicationDataBuilder(this)
- }.build()
-
/**
* Returns `true` if any of the fields of this ComplicationData are placeholders. I.e. if any
* fields are equal to: [ComplicationText.PLACEHOLDER], [SmallImage.PLACEHOLDER],
@@ -135,7 +130,7 @@
TYPE,
placeholder?.tapAction,
cachedWireComplicationData,
- dataSource = placeholder?.dataSource
+ dataSource = null
) {
/** Constructs a NoDataComplicationData without a [placeholder]. */
@@ -170,12 +165,12 @@
}
return createWireComplicationDataBuilder().apply {
if (placeholder == null) {
- setPlaceholderType(TYPE.toWireComplicationType())
+ setPlaceholder(null)
} else {
- setPlaceholderType(placeholder.type.toWireComplicationType())
- placeholder.fillWireComplicationDataBuilder(this)
+ val builder = placeholder.createWireComplicationDataBuilder()
+ placeholder.fillWireComplicationDataBuilder(builder)
+ setPlaceholder(builder.build())
}
- setDataSource(dataSource)
}.build().also { cachedWireComplicationData = it }
}
@@ -1611,6 +1606,84 @@
}
}
+internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? = when (type) {
+ NoDataComplicationData.TYPE.toWireComplicationType() -> null
+
+ ShortTextComplicationData.TYPE.toWireComplicationType() -> {
+ ShortTextComplicationData.Builder(
+ shortText!!.toApiComplicationTextPlaceholderAware(),
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+ ).apply {
+ setTapAction(tapAction)
+ setValidTimeRange(parseTimeRange())
+ setMonochromaticImage(parseIconPlaceholderAware())
+ setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
+ setDataSource(dataSource)
+ }.build()
+ }
+
+ LongTextComplicationData.TYPE.toWireComplicationType() -> {
+ LongTextComplicationData.Builder(
+ longText!!.toApiComplicationTextPlaceholderAware(),
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+ ).apply {
+ setTapAction(tapAction)
+ setValidTimeRange(parseTimeRange())
+ setMonochromaticImage(parseIconPlaceholderAware())
+ setSmallImage(parseSmallImagePlaceholderAware())
+ setTitle(longTitle?.toApiComplicationTextPlaceholderAware())
+ setDataSource(dataSource)
+ }.build()
+ }
+
+ RangedValueComplicationData.TYPE.toWireComplicationType() ->
+ RangedValueComplicationData.Builder(
+ value = rangedValue,
+ min = rangedMinValue,
+ max = rangedMaxValue,
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+ ).apply {
+ setTapAction(tapAction)
+ setValidTimeRange(parseTimeRange())
+ setMonochromaticImage(parseIconPlaceholderAware())
+ setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
+ setText(shortText?.toApiComplicationTextPlaceholderAware())
+ setDataSource(dataSource)
+ }.build()
+
+ MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
+ MonochromaticImageComplicationData(
+ parseIconPlaceholderAware()!!,
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+ tapAction,
+ parseTimeRange(),
+ this,
+ dataSource
+ )
+
+ SmallImageComplicationData.TYPE.toWireComplicationType() ->
+ SmallImageComplicationData(
+ parseSmallImagePlaceholderAware()!!,
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+ tapAction,
+ parseTimeRange(),
+ this,
+ dataSource
+ )
+
+ PhotoImageComplicationData.TYPE.toWireComplicationType() ->
+ PhotoImageComplicationData(
+ parseLargeImagePlaceholderAware()!!,
+ contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+ tapAction,
+ parseTimeRange(),
+ this,
+ dataSource
+ )
+
+ else -> null
+}
+
/**
* @hide
*/
@@ -1619,95 +1692,9 @@
val wireComplicationData = this
return when (type) {
NoDataComplicationData.TYPE.toWireComplicationType() -> {
- if (hasPlaceholderType()) {
- val placeholder = when (placeholderType) {
- NoDataComplicationData.TYPE.toWireComplicationType() -> null
-
- ShortTextComplicationData.TYPE.toWireComplicationType() -> {
- ShortTextComplicationData.Builder(
- shortText!!.toApiComplicationTextPlaceholderAware(),
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY
- ).apply {
- setMonochromaticImage(parseIconPlaceholderAware())
- setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
- setDataSource(dataSource)
- }.build()
- }
-
- LongTextComplicationData.TYPE.toWireComplicationType() -> {
- LongTextComplicationData.Builder(
- longText!!.toApiComplicationTextPlaceholderAware(),
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY
- ).apply {
- setMonochromaticImage(parseIconPlaceholderAware())
- setSmallImage(parseSmallImagePlaceholderAware())
- setTitle(longTitle?.toApiComplicationTextPlaceholderAware())
- setDataSource(dataSource)
- }.build()
- }
-
- RangedValueComplicationData.TYPE.toWireComplicationType() ->
- RangedValueComplicationData.Builder(
- value = rangedValue,
- min = rangedMinValue,
- max = rangedMaxValue,
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY
- ).apply {
- setMonochromaticImage(parseIconPlaceholderAware())
- setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
- setText(shortText?.toApiComplicationTextPlaceholderAware())
- setDataSource(dataSource)
- }.build()
-
- MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
- MonochromaticImageComplicationData(
- parseIconPlaceholderAware()!!,
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY,
- tapAction,
- parseTimeRange(),
- wireComplicationData,
- dataSource
- )
-
- SmallImageComplicationData.TYPE.toWireComplicationType() ->
- SmallImageComplicationData(
- parseSmallImagePlaceholderAware()!!,
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY,
- tapAction,
- parseTimeRange(),
- wireComplicationData,
- dataSource
- )
-
- PhotoImageComplicationData.TYPE.toWireComplicationType() ->
- PhotoImageComplicationData(
- parseLargeImagePlaceholderAware()!!,
- contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY,
- tapAction,
- parseTimeRange(),
- wireComplicationData,
- dataSource
- )
-
- else -> throw IllegalStateException(
- "Unrecognized placeholderType $placeholderType"
- )
- }
-
- if (placeholder != null) {
- NoDataComplicationData(placeholder)
- } else {
- NoDataComplicationData()
- }
- } else {
- NoDataComplicationData()
- }
+ placeholder?.toPlaceholderComplicationData() ?.let {
+ NoDataComplicationData(it)
+ } ?: NoDataComplicationData()
}
EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index e583843..2b79589 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -1095,16 +1095,19 @@
@Test
public fun timelineEntryCollectionWithPlaceholder() {
+ val placeholderString =
+ androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING
val data =
ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText(
- androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING))
+ .setLongText(ComplicationText.plainText(placeholderString))
.build()
val timelineEntry =
ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText(
- androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING))
+ .setPlaceholder(
+ ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
+ .setLongText(ComplicationText.plainText(placeholderString))
+ .build()
+ )
.build()
timelineEntry.timelineStartEpochSecond = 100
timelineEntry.timelineEndEpochSecond = 1000
@@ -1112,7 +1115,7 @@
val entry = data.timelineEntries!!.first()
Truth.assertThat(entry.type).isEqualTo(ComplicationData.TYPE_NO_DATA)
- Truth.assertThat(entry.placeholderType).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+ Truth.assertThat(entry.placeholder!!.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
}
private companion object {
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 19a5e93..f1945a4 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -45,7 +45,7 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_NO_DATA)
+ .setPlaceholder(null)
.build()
)
testRoundTripConversions(data)
@@ -470,12 +470,17 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
- .setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
- .setIcon(createPlaceholderIcon())
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+ .setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
+ .setIcon(createPlaceholderIcon())
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -533,10 +538,15 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
- .setLongText(WireComplicationText.plainText("text"))
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("text"))
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -592,13 +602,18 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
+ .setRangedMinValue(0f)
+ .setRangedMaxValue(100f)
+ .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -656,10 +671,15 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_ICON)
- .setIcon(createPlaceholderIcon())
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ .setIcon(createPlaceholderIcon())
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -713,11 +733,16 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
- .setSmallImage(createPlaceholderIcon())
- .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ .setSmallImage(createPlaceholderIcon())
+ .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -771,10 +796,15 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_LARGE_IMAGE)
- .setLargeImage(createPlaceholderIcon())
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ .setLargeImage(createPlaceholderIcon())
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ )
.build()
)
testRoundTripConversions(data)
@@ -842,7 +872,7 @@
public fun noDataComplicationData() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_NO_DATA).build(),
+ .setPlaceholder(null).build(),
ComplicationType.NO_DATA
)
}
@@ -953,11 +983,16 @@
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setShortText(WireComplicationText.plainText("text"))
- .setShortTitle(WireComplicationText.plainText("title"))
- .setIcon(icon)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setShortText(WireComplicationText.plainText("text"))
+ .setShortTitle(WireComplicationText.plainText("title"))
+ .setIcon(icon)
+ .build()
+ )
.build(),
ComplicationType.NO_DATA
)
@@ -968,11 +1003,16 @@
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setLongText(WireComplicationText.plainText("text"))
- .setLongTitle(WireComplicationText.plainText("title"))
- .setIcon(icon)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setLongText(WireComplicationText.plainText("text"))
+ .setLongTitle(WireComplicationText.plainText("title"))
+ .setIcon(icon)
+ .build()
+ )
.build(),
ComplicationType.NO_DATA
)
@@ -983,13 +1023,18 @@
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setRangedValue(75f)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setIcon(icon)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .setRangedValue(75f)
+ .setRangedMinValue(0f)
+ .setRangedMaxValue(100f)
+ .setShortTitle(WireComplicationText.plainText("battery"))
+ .setIcon(icon)
+ .build()
+ )
.build(),
ComplicationType.NO_DATA
)
@@ -1000,10 +1045,15 @@
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
- .setSmallImage(icon)
- .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
- .setContentDescription(WireComplicationText.plainText("content description"))
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ .setSmallImage(icon)
+ .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .build()
+ )
.build(),
ComplicationType.NO_DATA
)
@@ -1014,9 +1064,14 @@
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_ICON)
- .setIcon(icon)
- .setContentDescription(WireComplicationText.plainText("content description"))
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ .setIcon(icon)
+ .setContentDescription(
+ WireComplicationText.plainText("content description")
+ )
+ .build()
+ )
.build(),
ComplicationType.NO_DATA
)
@@ -1105,14 +1160,14 @@
}
@Test
- public fun NoDataComplicationData() {
+ public fun noDataComplicationData() {
assertThat(
NoDataComplicationData(
ShortTextComplicationData.Builder(
ComplicationText.PLACEHOLDER,
ComplicationText.EMPTY
).setTapAction(mPendingIntent).build()
- ).asWireComplicationData().tapAction
+ ).asWireComplicationData().placeholder?.tapAction
).isEqualTo(mPendingIntent)
}
}
@@ -1325,8 +1380,11 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("text"))
+ .build()
+ )
.build()
)
}
@@ -1340,8 +1398,11 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
- .setLongText(WireComplicationText.plainText("text"))
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("text"))
+ .build()
+ )
.build()
)
}
@@ -1359,10 +1420,13 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setRangedValue(95f)
+ .setRangedMinValue(0f)
+ .setRangedMaxValue(100f)
+ .build()
+ )
.build()
)
}
@@ -1377,8 +1441,11 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_ICON)
- .setIcon(icon)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ .setIcon(icon)
+ .build()
+ )
.build()
)
}
@@ -1393,9 +1460,12 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
- .setSmallImage(icon)
- .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ .setSmallImage(icon)
+ .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+ .build()
+ )
.build()
)
}
@@ -1409,8 +1479,11 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(WireComplicationData.TYPE_LARGE_IMAGE)
- .setLargeImage(icon)
+ .setPlaceholder(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ .setLargeImage(icon)
+ .build()
+ )
.build()
)
}
diff --git a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
index 8ede3a8..2f790f8 100644
--- a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
+++ b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
@@ -170,6 +170,7 @@
boolean mIsPlaceholderTitle;
@VisibleForTesting
boolean mIsPlaceholderText;
+ boolean mIsPlaceholder;
// Drawables for rendering rounded images
private RoundedDrawable mRoundedBackgroundDrawable = null;
@@ -284,9 +285,12 @@
mIsPlaceholderRangedValue = false;
mIsPlaceholderTitle = false;
mIsPlaceholderText = false;
+ mIsPlaceholder = false;
if (data.getType() == ComplicationData.TYPE_NO_DATA) {
- if (data.hasPlaceholderType()) {
+ ComplicationData placeholder = data.getPlaceholder();
+ if (placeholder != null) {
+ data = placeholder;
mIsPlaceholderIcon = data.hasIcon() && ImageKt.isPlaceholder(data.getIcon());
mIsPlaceholderSmallImage =
data.hasSmallImage() && ImageKt.isPlaceholder(data.getSmallImage());
@@ -295,7 +299,7 @@
mIsPlaceholderRangedValue = data.hasRangedValue()
&& data.getRangedValue()
== RangedValueComplicationData.PLACEHOLDER;
- if (data.getPlaceholderType() == ComplicationData.TYPE_LONG_TEXT) {
+ if (data.getType() == ComplicationData.TYPE_LONG_TEXT) {
mIsPlaceholderTitle =
data.hasLongTitle() && data.getLongTitle().isPlaceholder();
mIsPlaceholderText =
@@ -308,6 +312,7 @@
}
mComplicationData = data;
mHasNoData = false;
+ mIsPlaceholder = true;
} else {
if (!mHasNoData) {
// Render TYPE_NO_DATA as a short text complication with a predefined string
@@ -547,8 +552,8 @@
float height;
// Avoid drawing two placeholder text fields of the same length.
if (!mSubTextBounds.isEmpty()
- && (mComplicationData.getPlaceholderType() == ComplicationData.TYPE_SHORT_TEXT
- || mComplicationData.getPlaceholderType() == ComplicationData.TYPE_LONG_TEXT)) {
+ && (mComplicationData.getType() == ComplicationData.TYPE_SHORT_TEXT
+ || mComplicationData.getType() == ComplicationData.TYPE_LONG_TEXT)) {
width = mMainTextBounds.width() * 0.4f;
height = mMainTextBounds.height() * 0.9f;
} else {
@@ -659,7 +664,7 @@
if (paintSet.isInBurnInProtectionMode() && mBurnInProtectionIcon != null) {
icon = mBurnInProtectionIcon;
}
- icon.setColorFilter(mComplicationData.hasPlaceholderType() ? PLACEHOLDER_COLOR_FILTER :
+ icon.setColorFilter(mIsPlaceholder ? PLACEHOLDER_COLOR_FILTER :
paintSet.mIconColorFilter);
drawIconOnCanvas(canvas, mIconBounds, icon);
} else if (isPlaceholder) {
@@ -768,9 +773,6 @@
mBackgroundBoundsF.set(0, 0, mBounds.width(), mBounds.height());
LayoutHelper currentLayoutHelper;
int type = mComplicationData.getType();
- if (type == ComplicationData.TYPE_NO_DATA && mComplicationData.hasPlaceholderType()) {
- type = mComplicationData.getPlaceholderType();
- }
switch (type) {
case ComplicationData.TYPE_ICON:
currentLayoutHelper = new IconLayoutHelper();
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
index d3dd469..f3b4ae2 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
@@ -608,8 +608,10 @@
public void placeholderLongTextIsDrawnAsPlaceholder() {
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(PLACEHOLDER_COMPLICATION_TEXT)
+ .setPlaceholder(new ComplicationData.Builder(
+ ComplicationData.TYPE_LONG_TEXT)
+ .setLongText(PLACEHOLDER_COMPLICATION_TEXT)
+ .build())
.build(),
true);
@@ -623,8 +625,10 @@
public void placeholderShortTextIsDrawnAsPlaceholder() {
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(PLACEHOLDER_COMPLICATION_TEXT)
+ .setPlaceholder(new ComplicationData.Builder(
+ ComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(PLACEHOLDER_COMPLICATION_TEXT)
+ .build())
.build(),
true);
@@ -638,9 +642,11 @@
public void placeholderLongTitleIsDrawnAsPlaceholder() {
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText("Hi"))
- .setLongTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .setPlaceholder(new ComplicationData.Builder(
+ ComplicationData.TYPE_LONG_TEXT)
+ .setLongText(ComplicationText.plainText("Hi"))
+ .setLongTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .build())
.build(),
true);
@@ -654,9 +660,11 @@
public void placeholderShortTitleIsDrawnAsPlaceholder() {
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Hi"))
- .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .setPlaceholder(new ComplicationData.Builder(
+ ComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(ComplicationText.plainText("Hi"))
+ .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .build())
.build(),
true);
@@ -670,11 +678,13 @@
public void iconIsTintedWithPlaceholderTintForPlaceholderComplication() {
mComplicationRenderer.setComplicationData(
new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
- .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Hi"))
- .setIcon(Icon.createWithBitmap(
- Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)))
- .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .setPlaceholder(new ComplicationData.Builder(
+ ComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(ComplicationText.plainText("Hi"))
+ .setIcon(Icon.createWithBitmap(
+ Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)))
+ .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+ .build())
.build(),
false);
diff --git a/wear/watchface/watchface/src/main/AndroidManifest.xml b/wear/watchface/watchface/src/main/AndroidManifest.xml
index 52231e3..546fd68 100644
--- a/wear/watchface/watchface/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface/src/main/AndroidManifest.xml
@@ -35,6 +35,8 @@
android:exported="true"
android:permission="com.google.android.wearable.permission.BIND_WATCH_FACE_CONTROL">
<meta-data android:name="androidx.wear.watchface.api_version" android:value="5" />
+ <meta-data android:name="androidx.wear.watchface.xml_version"
+ android:value="@integer/watch_face_xml_version" />
<intent-filter>
<action android:name="com.google.android.wearable.action.WATCH_FACE_CONTROL"/>
</intent-filter>
diff --git a/wear/watchface/watchface/src/main/res/values/config.xml b/wear/watchface/watchface/src/main/res/values/config.xml
index 2325b7c2..a0e0eec 100644
--- a/wear/watchface/watchface/src/main/res/values/config.xml
+++ b/wear/watchface/watchface/src/main/res/values/config.xml
@@ -17,4 +17,5 @@
<resources>
<bool name="watch_face_instance_service_enabled">false</bool>
+ <integer name="watch_face_xml_version">0</integer>
</resources>