Merge "Use WindowMetrics.density instead of Context density" into androidx-platform-dev
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
index 9235137..7892f10 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
@@ -129,6 +129,9 @@
}
private fun getSupportedProfiles(cameraSelector: CameraSelector): List<CamcorderProfileProxy> {
+ if (!CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!)) {
+ return emptyList()
+ }
val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
val videoCapabilities = VideoCapabilities.from(cameraInfo)
return videoCapabilities.supportedQualities
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 380f24b..29c53eb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -20,6 +20,7 @@
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
+import android.app.AppOpsManager.OnOpNotedCallback
import android.app.AsyncNotedAppOp
import android.app.SyncNotedAppOp
import android.content.ContentResolver
@@ -41,8 +42,9 @@
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.ImageFormatConstants
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.Observable.Observer
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.testing.AudioUtil
import androidx.camera.testing.CameraPipeConfigTestRule
@@ -55,18 +57,27 @@
import androidx.camera.testing.mocks.MockConsumer
import androidx.camera.testing.mocks.helpers.CallTimes
import androidx.camera.testing.mocks.helpers.CallTimesAtLeast
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_NON_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.INACTIVE
+import androidx.camera.video.VideoRecordEvent.Finalize
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INVALID_OUTPUT_OPTIONS
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_RECORDER_ERROR
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.camera.video.VideoRecordEvent.Pause
+import androidx.camera.video.VideoRecordEvent.Resume
+import androidx.camera.video.VideoRecordEvent.Start
+import androidx.camera.video.VideoRecordEvent.Status
import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk
import androidx.camera.video.internal.compat.quirk.DeviceQuirks
import androidx.camera.video.internal.compat.quirk.ExtraSupportedResolutionQuirk
import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
+import androidx.camera.video.internal.encoder.EncoderFactory
import androidx.camera.video.internal.encoder.InvalidConfigException
-import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -77,9 +88,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.io.File
-import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
-import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CompletableDeferred
@@ -104,10 +113,14 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.timeout
+private const val DEFAULT_STATUS_COUNT = 5
private const val GENERAL_TIMEOUT = 5000L
private const val STATUS_TIMEOUT = 15000L
private const val TEST_ATTRIBUTION_TAG = "testAttribution"
-private const val BITRATE_AUTO = 0
+// For the file size is small, the final file length possibly exceeds the file size limit
+// after adding the file header. We still add the buffer for the tolerance of comparing the
+// file length and file size limit.
+private const val FILE_SIZE_LIMIT_BUFFER = 50 * 1024 // 50k threshold buffer
@LargeTest
@RunWith(Parameterized::class)
@@ -156,12 +169,11 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context: Context = ApplicationProvider.getApplicationContext()
private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+ private val recordingsToStop = mutableListOf<RecordingProcess>()
private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
- private lateinit var recorder: Recorder
private lateinit var preview: Preview
private lateinit var surfaceTexturePreview: Preview
- private lateinit var mockVideoRecordEventConsumer: MockConsumer<VideoRecordEvent>
@Before
fun setUp() {
@@ -249,19 +261,18 @@
surfaceTexturePreview,
preview
)
-
- mockVideoRecordEventConsumer = MockConsumer<VideoRecordEvent>()
}
@After
fun tearDown() {
+ for (recording in recordingsToStop) {
+ recording.stop()
+ }
+
if (this::cameraUseCaseAdapter.isInitialized) {
instrumentation.runOnMainSync {
cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
}
- if (this::recorder.isInitialized) {
- recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
- }
}
CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
@@ -269,316 +280,207 @@
@Test
fun canRecordToFile() {
- testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(BITRATE_AUTO, enableAudio = true)
+ // Arrange.
+ val outputOptions = createFileOutputOptions()
+ val recording = createRecordingProcess(outputOptions = outputOptions)
+
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ val uri = finalize.outputResults.outputUri
+ assertThat(uri).isEqualTo(Uri.fromFile(outputOptions.file))
+ checkFileHasAudioAndVideo(uri)
+ }
}
@Test
fun recordingWithSetTargetVideoEncodingBitRate() {
testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(6_000_000)
- verifyConfiguredVideoBitrate()
}
@Test
fun recordingWithSetTargetVideoEncodingBitRateOutOfRange() {
testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(1000_000_000)
- verifyConfiguredVideoBitrate()
}
@Test
fun recordingWithNegativeBitRate() {
- initializeRecorder()
assertThrows(IllegalArgumentException::class.java) {
- Recorder.Builder().setTargetVideoEncodingBitRate(-5).build()
+ createRecorder(targetBitrate = -5)
}
}
@Test
fun canRecordToMediaStore() {
- initializeRecorder()
assumeTrue(
"Ignore the test since the MediaStore.Video has compatibility issues.",
DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
)
- invokeSurfaceRequest()
- val statusSemaphore = Semaphore(0)
- val finalizeSemaphore = Semaphore(0)
- val context: Context = ApplicationProvider.getApplicationContext()
+
+ // Arrange.
val contentResolver: ContentResolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
}
-
val outputOptions = MediaStoreOutputOptions.Builder(
contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
).setContentValues(contentValues).build()
+ val recording = createRecordingProcess(outputOptions = outputOptions)
- var uri: Uri = Uri.EMPTY
- val recording = recorder.prepareRecording(context, outputOptions)
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor()) {
- if (it is VideoRecordEvent.Status) {
- statusSemaphore.release()
- }
- if (it is VideoRecordEvent.Finalize) {
- uri = it.outputResults.outputUri
- finalizeSemaphore.release()
- }
- }
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ val uri = finalize.outputResults.outputUri
+ checkFileHasAudioAndVideo(uri)
- assertThat(statusSemaphore.tryAcquire(5, 15000L, TimeUnit.MILLISECONDS)).isTrue()
-
- recording.stopSafely()
-
- // Wait for the recording to complete.
- assertThat(finalizeSemaphore.tryAcquire(GENERAL_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
- assertThat(uri).isNotEqualTo(Uri.EMPTY)
-
- checkFileHasAudioAndVideo(uri)
-
- contentResolver.delete(uri, null, null)
+ // Clean-up.
+ contentResolver.delete(uri, null, null)
+ }
}
@Test
@SdkSuppress(minSdkVersion = 26)
fun canRecordToFileDescriptor() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val pfd = ParcelFileDescriptor.open(
- file,
- ParcelFileDescriptor.MODE_READ_WRITE
- )
- val recording = recorder
- .prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+ // Arrange.
+ val file = createTempFile()
+ val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
+ val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+ val recording = createRecordingProcess(outputOptions = outputOptions)
+ // Act.
+ recording.startAndVerify()
// ParcelFileDescriptor should be safe to close after PendingRecording#start.
pfd.close()
+ recording.stopAndVerify()
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true,
- GENERAL_TIMEOUT
- )
-
+ // Assert.
checkFileHasAudioAndVideo(Uri.fromFile(file))
-
- file.delete()
}
@Test
@SdkSuppress(minSdkVersion = 26)
fun recordToFileDescriptor_withClosedFileDescriptor_receiveError() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val file = createTempFile()
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
-
pfd.close()
+ val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+ val recording = createRecordingProcess(outputOptions = outputOptions)
- recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- // Check the output Uri from the finalize event match the Uri from the given file.
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
+ // Act.
+ recording.start()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
}
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
-
- file.delete()
}
@Test
@SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
@SuppressLint("NewApi") // Intentionally testing behavior of calling from invalid API level
fun prepareRecordingWithFileDescriptor_throwsExceptionBeforeApi26() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recorder = createRecorder()
+ val file = createTempFile()
ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_WRITE
).use { pfd ->
+ // Assert.
assertThrows(UnsupportedOperationException::class.java) {
+ // Act.
recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
}
}
-
- file.delete()
}
@Test
fun canPauseResume() {
- initializeRecorder()
- invokeSurfaceRequest()
+ // Arrange.
+ val recording = createRecordingProcess()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- pause()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Pause::class.java,
- true, GENERAL_TIMEOUT
- )
-
- resume()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Resume::class.java,
- true, GENERAL_TIMEOUT
- )
- // Check there are data being encoded after resuming.
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Status::class.java,
- true, STATUS_TIMEOUT, CallTimesAtLeast(5)
- )
-
- stopSafely()
- }
-
- // Wait for the recording to be finalized.
- mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT)
-
- checkFileHasAudioAndVideo(Uri.fromFile(file))
-
- file.delete()
+ // Act.
+ recording.startAndVerify()
+ recording.pauseAndVerify()
+ recording.resumeAndVerify()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_NONE)
+ }
}
@Test
fun canStartRecordingPaused_whenRecorderInitializing() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recorder = createRecorder(sendSurfaceRequest = false)
+ val recording = createRecordingProcess(recorder = recorder)
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- pause()
+ // Act.
+ recording.start()
+ recording.pause()
+ // Only invoke surface request after pause() has been called
+ recorder.sendSurfaceRequest()
- // Only invoke surface request after pause() has been called
- invokeSurfaceRequest()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Start::class.java,
- true, GENERAL_TIMEOUT
- )
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Pause::class.java,
- true, GENERAL_TIMEOUT
- )
-
- stopSafely()
- }
-
- file.delete()
+ // Assert.
+ recording.verifyStart()
+ recording.verifyPause()
}
@Test
fun canReceiveRecordingStats() {
- initializeRecorder()
- invokeSurfaceRequest()
+ // Arrange.
+ val recording = createRecordingProcess()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Act.
+ recording.startAndVerify()
+ recording.pauseAndVerify()
+ recording.resumeAndVerify()
+ recording.stopAndVerify()
- // Start
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- pause()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Pause::class.java,
- true, GENERAL_TIMEOUT
- )
-
- resume()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Resume::class.java,
- true, GENERAL_TIMEOUT
- )
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Status::class.java,
- true, STATUS_TIMEOUT, CallTimesAtLeast(5)
- )
-
- stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
- }
-
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
- }
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
+ // Assert.
+ val events = recording.getAllEvents()
+ assertThat(events.size).isAtLeast(
+ 1 /* Start */ +
+ 5 /* Status */ +
+ 1 /* Pause */ +
+ 1 /* Resume */ +
+ 5 /* Status */ +
+ 1 /* Stop */
)
- captor.allValues.run {
- assertThat(size).isAtLeast(
- (
- 1 /* Start */ +
- 5 /* Status */ +
- 1 /* Pause */ +
- 1 /* Resume */ +
- 5 /* Status */ +
- 1 /* Stop */
- )
- )
-
- // Ensure duration and bytes are increasing
- take(size - 1).mapIndexed { index, _ ->
- Pair(get(index).recordingStats, get(index + 1).recordingStats)
- }.forEach { (former: RecordingStats, latter: RecordingStats) ->
- assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
- assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
- }
-
- // Ensure they are not all zero by checking last stats
- last().recordingStats.also {
- assertThat(it.numBytesRecorded).isGreaterThan(0L)
- assertThat(it.recordedDurationNanos).isGreaterThan(0L)
- }
+ // Assert: Ensure duration and bytes are increasing.
+ List(events.size - 1) { index ->
+ Pair(events[index].recordingStats, events[index + 1].recordingStats)
+ }.forEach { (former: RecordingStats, latter: RecordingStats) ->
+ assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
+ assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
}
- file.delete()
+ // Assert: Ensure they are not all zero by checking the last stats.
+ events.last().recordingStats.also {
+ assertThat(it.numBytesRecorded).isGreaterThan(0L)
+ assertThat(it.recordedDurationNanos).isGreaterThan(0L)
+ }
}
@Test
fun setFileSizeLimit() {
- initializeRecorder()
+ // Arrange.
val fileSizeLimit = 500L * 1024L // 500 KB
- runFileSizeLimitTest(fileSizeLimit)
+ val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+ val recording = createRecordingProcess(outputOptions = outputOptions)
+
+ // Act.
+ recording.startAndVerify()
+ recording.verifyFinalize(timeoutMs = 60_000L) { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+ assertThat(outputOptions.file.length())
+ .isLessThan(fileSizeLimit + FILE_SIZE_LIMIT_BUFFER)
+ }
}
// Sets the file size limit to 1 byte, which will be lower than the initial data sent from
@@ -586,800 +488,429 @@
// written to it.
@Test
fun setFileSizeLimitLowerThanInitialDataSize() {
- initializeRecorder()
+ // Arrange.
val fileSizeLimit = 1L // 1 byte
- runFileSizeLimitTest(fileSizeLimit)
+ val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+ val recording = createRecordingProcess(outputOptions = outputOptions)
+
+ // Act.
+ recording.start()
+ recording.verifyFinalize { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+ }
}
@Test
fun setLocation() {
- initializeRecorder()
runLocationTest(createLocation(25.033267462243586, 121.56454121737946))
}
@Test
fun setNegativeLocation() {
- initializeRecorder()
runLocationTest(createLocation(-27.14394722411734, -109.33053675296067))
}
@Test
fun stop_withErrorWhenDurationLimitReached() {
- initializeRecorder()
- val videoRecordEventListener = MockConsumer<VideoRecordEvent>()
- invokeSurfaceRequest()
+ // Arrange.
val durationLimitMs = 3000L
- val durationTolerance = 50L
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputOptions = FileOutputOptions.Builder(file)
- .setDurationLimitMillis(durationLimitMs)
- .build()
+ val durationToleranceMs = 50L
+ val outputOptions = createFileOutputOptions(durationLimitMillis = durationLimitMs)
+ val recording = createRecordingProcess(outputOptions = outputOptions)
- val recording = recorder
- .prepareRecording(context, outputOptions)
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
+ // Act.
+ recording.start()
- // The recording should be finalized after the specified duration limit plus some time
- // for processing it.
- videoRecordEventListener.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- false,
- durationLimitMs + 2000L
- )
-
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
- argument -> VideoRecordEvent::class.java.isInstance(argument)
+ // Assert.
+ recording.verifyFinalize(timeoutMs = durationLimitMs + 2000L) { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
+ assertThat(finalize.recordingStats.recordedDurationNanos)
+ .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationToleranceMs))
+ checkDurationAtMost(
+ Uri.fromFile(outputOptions.file),
+ durationLimitMs + durationToleranceMs
+ )
}
- videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
- false, CallTimesAtLeast(1), captor)
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
- assertThat(finalize.recordingStats.recordedDurationNanos)
- .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationTolerance))
- checkDurationAtMost(Uri.fromFile(file), durationLimitMs)
-
- recording.stopSafely()
- file.delete()
}
@Test
fun checkStreamState() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+ // Arrange.
+ val recorder = createRecorder()
@Suppress("UNCHECKED_CAST")
- val streamInfoObserver =
- mock(Observable.Observer::class.java) as Observable.Observer<StreamInfo>
+ val streamInfoObserver = mock(Observer::class.java) as Observer<StreamInfo>
val inOrder = inOrder(streamInfoObserver)
- recorder.streamInfo.addObserver(CameraXExecutors.directExecutor(), streamInfoObserver)
+ recorder.streamInfo.addObserver(directExecutor(), streamInfoObserver)
- // Recorder should start in INACTIVE stream state before any recordings
- inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
- argThat {
- it!!.streamState == StreamInfo.StreamState.INACTIVE
- }
- )
-
- // Start
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
- // Starting recording should move Recorder to ACTIVE stream state
- inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
- argThat {
- it!!.streamState == StreamInfo.StreamState.ACTIVE
- }
- )
-
- recording.stopSafely()
-
- // Stopping recording should eventually move to INACTIVE stream state
+ // Assert: Recorder should start in INACTIVE stream state before any recordings
inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
argThat {
it!!.streamState == StreamInfo.StreamState.INACTIVE
}
)
+ val recording = createRecordingProcess(recorder = recorder)
- file.delete()
+ // Act.
+ recording.start()
+
+ // Assert: Starting recording should move Recorder to ACTIVE stream state
+ inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
+ argThat { it!!.streamState == StreamInfo.StreamState.ACTIVE }
+ )
+
+ // Act.
+ recording.stop()
+
+ // Assert: Stopping recording should eventually move to INACTIVE stream state
+ inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
+ argThat {
+ it!!.streamState == StreamInfo.StreamState.INACTIVE
+ }
+ )
}
@Test
fun start_throwsExceptionWhenActive() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputOptions = FileOutputOptions.Builder(file).build()
+ // Arrange.
+ val recorder = createRecorder()
+ val recording = createRecordingProcess(recorder = recorder)
- val recording = recorder.prepareRecording(context, outputOptions).start(
- CameraXExecutors.directExecutor()
- ) {}
+ // Act: 1st start.
+ recording.start()
- val pendingRecording = recorder.prepareRecording(context, outputOptions)
+ // Assert.
assertThrows(java.lang.IllegalStateException::class.java) {
- pendingRecording.start(CameraXExecutors.directExecutor()) {}
+ // Act: 2nd start.
+ val recording2 = createRecordingProcess(recorder = recorder)
+ recording2.start()
}
-
- recording.close()
- file.delete()
}
@Test
fun start_whenSourceActiveNonStreaming() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recorder = createRecorder(initSourceState = ACTIVE_NON_STREAMING)
+ val recording = createRecordingProcess(recorder = recorder)
- recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- invokeSurfaceRequest()
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
- // Wait for the recording to be finalized.
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- file.delete()
+ // Act.
+ recording.start()
+ recorder.onSourceStateChanged(ACTIVE_STREAMING)
+ recording.verifyStart()
+ recording.verifyStatus()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_NONE)
+ }
}
@Test
fun start_finalizeImmediatelyWhenSourceInactive() {
- initializeRecorder()
- invokeSurfaceRequest()
- val videoCaptureMonitor = VideoCaptureMonitor()
- recorder.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
- // Ensure the Recorder is initialized before start test.
- videoCaptureMonitor.waitForVideoCaptureStatus()
+ // Arrange.
+ val recorder = createRecorder(initSourceState = INACTIVE)
+ val recording = createRecordingProcess(recorder = recorder)
+
+ // Act.
+ recording.start()
+
+ // Assert.
+ recording.verifyFinalize { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
}
-
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
-
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- false, GENERAL_TIMEOUT
- )
- mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(false)
-
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
- }
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
-
- recording.stopSafely()
-
- file.delete()
}
@Test
fun pause_whenSourceActiveNonStreaming() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- pause()
-
- invokeSurfaceRequest()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Start::class.java,
- true, GENERAL_TIMEOUT
- )
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Pause::class.java,
- true, GENERAL_TIMEOUT
- )
-
- stopSafely()
- }
-
- // Wait for the recording to be finalized.
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
+ // Arrange.
+ val recorder = createRecorder(
+ sendSurfaceRequest = false,
+ initSourceState = ACTIVE_NON_STREAMING
)
+ val recording = createRecordingProcess(recorder = recorder)
- // If the recording is paused immediately after being started, the recording should be
- // finalized with ERROR_NO_VALID_DATA.
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
+ // Act.
+ recording.start()
+ recording.pause()
+ recorder.sendSurfaceRequest()
+
+ // Assert.
+ recording.verifyStart()
+ recording.verifyPause()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
}
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
- file.delete()
}
@Test
fun pause_noOpWhenAlreadyPaused() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+ // Act.
+ recording.startAndVerify()
+ recording.pauseAndVerify()
+ recording.pause()
- pause()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Pause::class.java,
- true, GENERAL_TIMEOUT
- )
-
- pause()
-
- stopSafely()
- }
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- // As described in b/197416199, there might be encoded data in flight which will trigger
- // Status event after pausing. So here it checks there's only one Pause event.
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
- }
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- assertThat(captor.allValues.count { it is VideoRecordEvent.Pause }).isAtMost(1)
-
- file.delete()
+ // Assert: One Pause event.
+ val events = recording.getAllEvents()
+ val pauseEvents = events.filterIsInstance<Pause>()
+ assertThat(pauseEvents.size).isAtMost(1)
}
@Test
fun pause_throwsExceptionWhenStopping() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify()
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
-
+ // Assert.
assertThrows(IllegalStateException::class.java) {
recording.pause()
}
-
- file.delete()
}
@Test
fun resume_noOpWhenNotPaused() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- // Calling resume shouldn't affect the stream of status events finally followed
- // by a finalize event. There shouldn't be another resume event generated.
+ // Act.
+ recording.startAndVerify()
recording.resume()
+ recording.stopAndVerify()
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Status::class.java,
- true,
- STATUS_TIMEOUT,
- CallTimesAtLeast(5)
- )
-
- recording.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- // Ensure no resume events were ever sent.
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Resume::class.java,
- false,
- GENERAL_TIMEOUT,
- CallTimes(0)
- )
-
- file.delete()
+ // Assert: No Resume event.
+ val events = recording.getAllEvents()
+ val resumeEvents = events.filterIsInstance<Resume>()
+ assertThat(resumeEvents).isEmpty()
}
@Test
fun resume_throwsExceptionWhenStopping() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+ // Act.
+ recording.startAndVerify()
+ recording.stop()
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
-
+ // Assert.
assertThrows(IllegalStateException::class.java) {
- recording.resume()
+ recording.resumeAndVerify()
}
-
- file.delete()
}
@Test
fun stop_beforeSurfaceRequested() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recorder = createRecorder(sendSurfaceRequest = false)
+ val recording = createRecordingProcess(recorder = recorder)
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+ // Act.
+ recording.start()
+ recording.stop()
+ recorder.sendSurfaceRequest()
- recording.pause()
-
- recording.stopSafely()
-
- invokeSurfaceRequest()
-
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
+ // Assert.
+ recording.verifyFinalize { finalize ->
+ assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
}
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
- file.delete()
- }
-
- @Test
- fun stop_fromAutoCloseable() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- // Recording will be stopped by AutoCloseable.close() upon exiting use{} block
- val pendingRecording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- pendingRecording.start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
- .use {
- invokeSurfaceRequest()
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
- }
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- file.delete()
}
@Test
fun stop_WhenUseCaseDetached() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+ // Act.
+ recording.startAndVerify()
instrumentation.runOnMainSync {
cameraUseCaseAdapter.removeUseCases(listOf(preview))
}
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- recording.stopSafely()
- file.delete()
+ // Assert.
+ recording.verifyFinalize { finalize ->
+ assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+ }
}
- @Suppress("UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
@Test
fun stop_whenRecordingIsGarbageCollected() {
- initializeRecorder()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ var recording: RecordingProcess? = createRecordingProcess()
+ val listener = recording!!.listener
- var recording: Recording? = recorder
- .prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- // First ensure the recording gets some status events
- invokeSurfaceRequest()
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+ // Act.
+ recording.startAndVerify()
// Remove reference to recording and run GC. The recording should be stopped once
// the Recording's finalizer runs.
+ recordingsToStop.remove(recording)
+ @Suppress("UNUSED_VALUE")
recording = null
GarbageCollectionUtil.runFinalization()
- // Ensure the event listener gets a finalize event. Note: the word "finalize" is very
- // overloaded here. This event means the recording has finished, but does not relate to the
- // finalizer that runs during garbage collection. However, that is what causes the
+ // Assert: Ensure the event listener gets a finalize event. Note: the word "finalize" is
+ // very overloaded here. This event means the recording has finished, but does not relate
+ // to the finalizer that runs during garbage collection. However, that is what causes the
// recording to finish.
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- file.delete()
+ listener.verifyFinalize()
}
@Test
fun stop_noOpWhenStopping() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess()
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify()
+ recording.stop()
- stopSafely()
- stopSafely()
- }
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
- mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(true)
-
- file.delete()
+ // Assert.
+ recording.verifyNoMoreEvent()
}
@Test
fun optionsOverridesDefaults() {
- initializeRecorder()
val qualitySelector = QualitySelector.from(Quality.HIGHEST)
- val recorder = Recorder.Builder()
- .setQualitySelector(qualitySelector)
- .build()
+ val recorder = createRecorder(qualitySelector = qualitySelector)
assertThat(recorder.qualitySelector).isEqualTo(qualitySelector)
}
@Test
fun canRetrieveProvidedExecutorFromRecorder() {
- initializeRecorder()
val myExecutor = Executor { command -> command?.run() }
- val recorder = Recorder.Builder()
- .setExecutor(myExecutor)
- .build()
+ val recorder = createRecorder(executor = myExecutor)
assertThat(recorder.executor).isSameInstanceAs(myExecutor)
}
@Test
fun cannotRetrieveExecutorWhenExecutorNotProvided() {
- initializeRecorder()
- val recorder = Recorder.Builder().build()
+ val recorder = createRecorder()
assertThat(recorder.executor).isNull()
}
@Test
fun canRecordWithoutAudio() {
- initializeRecorder()
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ // Arrange.
+ val recording = createRecordingProcess(withAudio = false)
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- // Check the audio information reports state as disabled.
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ val uri = finalize.outputResults.outputUri
+ checkFileHasAudioAndVideo(uri, hasAudio = false)
}
-
- mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
- false, CallTimesAtLeast(1), captor)
-
- assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
- val status = captor.value as VideoRecordEvent.Status
- assertThat(status.recordingStats.audioStats.audioState)
- .isEqualTo(AudioStats.AUDIO_STATE_DISABLED)
-
- recording.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
- false, GENERAL_TIMEOUT)
-
- checkFileAudio(Uri.fromFile(file), false)
- checkFileVideo(Uri.fromFile(file), true)
-
- file.delete()
}
@Test
fun cannotStartMultiplePendingRecordingsWhileInitializing() {
- initializeRecorder()
- val file1 = File.createTempFile("CameraX1", ".tmp").apply { deleteOnExit() }
- val file2 = File.createTempFile("CameraX2", ".tmp").apply { deleteOnExit() }
- try {
- // We explicitly do not invoke the surface request so the recorder is initializing.
- recorder.prepareRecording(context, FileOutputOptions.Builder(file1).build())
- .start(CameraXExecutors.directExecutor()) {}
- .apply {
- assertThrows<IllegalStateException> {
- recorder.prepareRecording(context, FileOutputOptions.Builder(file2).build())
- .start(CameraXExecutors.directExecutor()) {}
- }
- stopSafely()
- }
- } finally {
- file1.delete()
- file2.delete()
+ // Arrange: Prepare 1st recording and start.
+ val recorder = createRecorder(sendSurfaceRequest = false)
+ val recording = createRecordingProcess(recorder = recorder)
+ recording.start()
+
+ // Assert.
+ assertThrows<IllegalStateException> {
+ // Act: Prepare 2nd recording and start.
+ createRecordingProcess(recorder = recorder).start()
}
}
@Test
fun canRecoverFromErrorState(): Unit = runBlocking {
- initializeRecorder()
+ // Arrange.
// Create a video encoder factory that will fail on first 2 create encoder requests.
- // Recorder initialization should fail by 1st encoder creation fail.
- // 1st recording request should fail by 2nd encoder creation fail.
- // 2nd recording request should be successful.
var createEncoderRequestCount = 0
- val recorder = Recorder.Builder()
- .setVideoEncoderFactory { executor, config ->
- if (createEncoderRequestCount < 2) {
- createEncoderRequestCount++
- throw InvalidConfigException("Create video encoder fail on purpose.")
- } else {
- Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
- }
- }.build().apply { onSourceStateChanged(VideoOutput.SourceState.INACTIVE) }
-
- invokeSurfaceRequest(recorder)
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+ val recorder = createRecorder(
+ videoEncoderFactory = { executor, config ->
+ if (createEncoderRequestCount < 2) {
+ createEncoderRequestCount++
+ throw InvalidConfigException("Create video encoder fail on purpose.")
+ } else {
+ Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
+ }
+ })
+ // Recorder initialization should fail by 1st encoder creation fail.
// Wait STREAM_ID_ERROR which indicates Recorder enter the error state.
withTimeoutOrNull(3000) {
recorder.streamInfo.asFlow().dropWhile { it!!.id != StreamInfo.STREAM_ID_ERROR }.first()
} ?: fail("Do not observe STREAM_ID_ERROR from StreamInfo observer.")
- // 1st recording request
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
- }
+ // Act: 1st recording request should fail by 2nd encoder creation fail.
+ var recording = createRecordingProcess(recorder = recorder)
+ recording.start()
+ recording.verifyFinalize { finalize ->
+ assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
+ }
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- 3000L,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
- }
-
- // 2nd recording request
- mockVideoRecordEventConsumer.clearAcceptCalls()
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- it.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
- }
-
- file.delete()
+ // Act: 2nd recording request should be successful.
+ recording = createRecordingProcess(recorder = recorder)
+ recording.startAndVerify()
+ recording.stopAndVerify()
}
@Test
@SdkSuppress(minSdkVersion = 31)
fun audioRecordIsAttributed() = runBlocking {
- initializeRecorder()
+ // Arrange.
val notedTag = CompletableDeferred<String>()
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
- appOps.setOnOpNotedCallback(
- Dispatchers.Main.asExecutor(),
- object : AppOpsManager.OnOpNotedCallback() {
- override fun onNoted(p0: SyncNotedAppOp) {
- // no-op. record_audio should be async.
- }
+ appOps.setOnOpNotedCallback(Dispatchers.Main.asExecutor(), object : OnOpNotedCallback() {
+ override fun onNoted(p0: SyncNotedAppOp) {
+ // no-op. record_audio should be async.
+ }
- override fun onSelfNoted(p0: SyncNotedAppOp) {
- // no-op. record_audio should be async.
- }
+ override fun onSelfNoted(p0: SyncNotedAppOp) {
+ // no-op. record_audio should be async.
+ }
- override fun onAsyncNoted(noted: AsyncNotedAppOp) {
- if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
- TEST_ATTRIBUTION_TAG == noted.attributionTag
- ) {
- notedTag.complete(noted.attributionTag!!)
- }
+ override fun onAsyncNoted(noted: AsyncNotedAppOp) {
+ if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
+ TEST_ATTRIBUTION_TAG == noted.attributionTag
+ ) {
+ notedTag.complete(noted.attributionTag!!)
}
- })
+ }
+ })
+ val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+ val recording = createRecordingProcess(context = attributionContext)
- var recording: Recording? = null
+ // Act.
+ recording.start()
try {
- val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- recording =
- recorder.prepareRecording(
- attributionContext, FileOutputOptions.Builder(file).build()
- )
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
val timeoutDuration = 5.seconds
withTimeoutOrNull(timeoutDuration) {
+ // Assert.
assertThat(notedTag.await()).isEqualTo(TEST_ATTRIBUTION_TAG)
} ?: fail("Timed out waiting for attribution tag. Waited $timeoutDuration.")
} finally {
appOps.setOnOpNotedCallback(null, null)
- recording?.stopSafely()
}
}
- private fun invokeSurfaceRequest() {
- invokeSurfaceRequest(recorder)
- }
+ private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(targetBitrate: Int) {
+ // Arrange.
+ val recorder = createRecorder(targetBitrate = targetBitrate)
+ val recording = createRecordingProcess(recorder = recorder, withAudio = false)
- private fun invokeSurfaceRequest(recorder: Recorder) {
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider { request: SurfaceRequest ->
- recorder.onSurfaceRequested(request)
- }
- recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_STREAMING)
- }
- }
-
- private fun initializeRecorder(bitrate: Int = BITRATE_AUTO) {
- recorder = Recorder.Builder().apply {
- if (bitrate != BITRATE_AUTO) {
- setTargetVideoEncodingBitRate(bitrate)
- }
- }.build()
- recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
- }
-
- private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(
- bitrate: Int,
- enableAudio: Boolean = false
- ) {
- initializeRecorder(bitrate)
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- val recording =
- recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
- .apply { if (enableAudio) withAudioEnabled() }
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true,
- GENERAL_TIMEOUT
- )
-
- val uri = Uri.fromFile(file)
- if (enableAudio) {
- checkFileHasAudioAndVideo(uri)
- } else {
- checkFileVideo(uri, true)
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify { finalize ->
+ assertThat(finalize.error).isEqualTo(ERROR_NONE)
}
- // Check the output Uri from the finalize event match the Uri from the given file.
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
- VideoRecordEvent::class.java.isInstance(
- argument
- )
- }
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent::class.java,
- false,
- CallTimesAtLeast(1),
- captor
- )
-
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
-
- file.delete()
- }
-
- private fun verifyConfiguredVideoBitrate() {
+ // Assert.
assertThat(recorder.mFirstRecordingVideoBitrate).isIn(
com.google.common.collect.Range.closed(
recorder.mVideoEncoderBitrateRange.lower,
@@ -1388,96 +919,224 @@
)
}
- private fun checkFileHasAudioAndVideo(uri: Uri) {
- checkFileAudio(uri, true)
- checkFileVideo(uri, true)
- }
-
- private fun checkFileAudio(uri: Uri, hasAudio: Boolean) {
- MediaMetadataRetriever().apply {
- try {
- setDataSource(context, uri)
- val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
-
- assertThat(value).isEqualTo(
- if (hasAudio) {
- "yes"
- } else {
- null
- }
- )
- } finally {
- release()
+ private fun Recorder.sendSurfaceRequest() {
+ instrumentation.runOnMainSync {
+ preview.setSurfaceProvider { request: SurfaceRequest ->
+ onSurfaceRequested(request)
}
}
}
- private fun checkFileVideo(uri: Uri, hasVideo: Boolean) {
- MediaMetadataRetriever().apply {
- try {
- setDataSource(context, uri)
- val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+ private fun createTempFile() = temporaryFolder.newFile()
- assertThat(value).isEqualTo(
- if (hasVideo) {
- "yes"
- } else {
- null
- }
- )
- } finally {
- release()
+ private fun createRecorder(
+ sendSurfaceRequest: Boolean = true,
+ initSourceState: VideoOutput.SourceState = ACTIVE_STREAMING,
+ qualitySelector: QualitySelector? = null,
+ executor: Executor? = null,
+ videoEncoderFactory: EncoderFactory? = null,
+ audioEncoderFactory: EncoderFactory? = null,
+ targetBitrate: Int? = null,
+ ): Recorder {
+ val recorder = Recorder.Builder().apply {
+ qualitySelector?.let { setQualitySelector(it) }
+ executor?.let { setExecutor(it) }
+ videoEncoderFactory?.let { setVideoEncoderFactory(it) }
+ audioEncoderFactory?.let { setAudioEncoderFactory(it) }
+ targetBitrate?.let { setTargetVideoEncodingBitRate(targetBitrate) }
+ }.build()
+ if (sendSurfaceRequest) {
+ recorder.sendSurfaceRequest()
+ }
+ recorder.onSourceStateChanged(initSourceState)
+ return recorder
+ }
+
+ private fun createFileOutputOptions(
+ file: File = createTempFile(),
+ fileSizeLimit: Long? = null,
+ durationLimitMillis: Long? = null,
+ location: Location? = null,
+ ): FileOutputOptions = FileOutputOptions.Builder(file).apply {
+ fileSizeLimit?.let { setFileSizeLimit(it) }
+ durationLimitMillis?.let { setDurationLimitMillis(it) }
+ location?.let { setLocation(it) }
+ }.build()
+
+ private fun createRecordingProcess(
+ recorder: Recorder = createRecorder(),
+ context: Context = ApplicationProvider.getApplicationContext(),
+ outputOptions: OutputOptions = createFileOutputOptions(),
+ withAudio: Boolean = true
+ ) = RecordingProcess(
+ recorder,
+ context,
+ outputOptions,
+ withAudio
+ )
+
+ inner class RecordingProcess(
+ private val recorder: Recorder,
+ context: Context,
+ outputOptions: OutputOptions,
+ withAudio: Boolean
+ ) {
+ private val pendingRecording: PendingRecording =
+ PendingRecording(context, recorder, outputOptions).apply {
+ if (withAudio) {
+ withAudioEnabled()
+ }
}
+ val listener = MockConsumer<VideoRecordEvent>()
+ private lateinit var recording: Recording
+
+ fun startAndVerify(
+ statusCount: Int = DEFAULT_STATUS_COUNT,
+ onStatus: ((List<Status>) -> Unit)? = null,
+ ) = startInternal(verify = true, statusCount = statusCount, onStatus = onStatus)
+
+ fun start() = startInternal(verify = false)
+
+ private fun startInternal(
+ verify: Boolean = false,
+ statusCount: Int = DEFAULT_STATUS_COUNT,
+ onStatus: ((List<Status>) -> Unit)? = null
+ ) {
+ recording = pendingRecording.start(mainThreadExecutor(), listener)
+ recordingsToStop.add(this)
+ if (verify) {
+ verifyStart()
+ verifyStatus(statusCount = statusCount, onStatus = onStatus)
+ }
+ }
+
+ fun verifyStart() {
+ listener.verifyStart()
+ }
+
+ fun verifyStatus(
+ statusCount: Int = DEFAULT_STATUS_COUNT,
+ onStatus: ((List<Status>) -> Unit)? = null,
+ ) {
+ listener.verifyStatus(eventCount = statusCount, onEvent = onStatus)
+ }
+
+ fun stopAndVerify(onFinalize: ((Finalize) -> Unit)? = null) =
+ stopInternal(verify = true, onFinalize)
+
+ fun stop() = stopInternal(verify = false)
+
+ private fun stopInternal(
+ verify: Boolean = false,
+ onFinalize: ((Finalize) -> Unit)? = null
+ ) {
+ recording.stopSafely(recorder)
+ if (verify) {
+ verifyFinalize(onFinalize = onFinalize)
+ }
+ }
+
+ fun verifyFinalize(
+ timeoutMs: Long = GENERAL_TIMEOUT,
+ onFinalize: ((Finalize) -> Unit)? = null
+ ) = listener.verifyFinalize(timeoutMs = timeoutMs, onFinalize = onFinalize)
+
+ fun pauseAndVerify() = pauseInternal(verify = true)
+
+ fun pause() = pauseInternal(verify = false)
+
+ private fun pauseInternal(verify: Boolean = false) {
+ recording.pause()
+ if (verify) {
+ verifyPause()
+ }
+ }
+
+ fun verifyPause() = listener.verifyPause()
+
+ fun resumeAndVerify() = resumeInternal(verify = true)
+
+ fun resume() = resumeInternal(verify = false)
+
+ private fun resumeInternal(verify: Boolean = false) {
+ recording.resume()
+ if (verify) {
+ verifyResume()
+ }
+ }
+
+ private fun verifyResume() {
+ listener.verifyResume()
+ listener.verifyStatus()
+ }
+
+ fun getAllEvents(): List<VideoRecordEvent> {
+ lateinit var events: List<VideoRecordEvent>
+ listener.verifyEvent(
+ VideoRecordEvent::class.java,
+ CallTimesAtLeast(1),
+ onEvent = {
+ events = it
+ }
+ )
+ return events
+ }
+
+ fun verifyNoMoreEvent() = listener.verifyNoMoreAcceptCalls(/*inOrder=*/true)
+ }
+
+ private fun checkFileHasAudioAndVideo(
+ uri: Uri,
+ hasAudio: Boolean = true,
+ ) {
+ MediaMetadataRetriever().useAndRelease {
+ it.setDataSource(context, uri)
+ assertThat(it.hasVideo()).isEqualTo(true)
+ assertThat(it.hasAudio()).isEqualTo(hasAudio)
}
}
+ @Suppress("SameParameterValue")
private fun checkLocation(uri: Uri, location: Location) {
- MediaMetadataRetriever().apply {
- try {
- setDataSource(context, uri)
- // Only test on mp4 output format, others will be ignored.
- val mime = extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
- assumeTrue("Unsupported mime = $mime",
- "video/mp4".equals(mime, ignoreCase = true))
- val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
- assertThat(value).isNotNull()
- // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
- val matchGroup =
- "([\\+-]?[0-9]+(\\.[0-9]+)?)([\\+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
- .find(value!!) ?: fail("Fail on checking location metadata: $value")
- val lat = matchGroup.groupValues[1].toDouble()
- val lon = matchGroup.groupValues[3].toDouble()
+ MediaMetadataRetriever().useAndRelease {
+ it.setDataSource(context, uri)
+ // Only test on mp4 output format, others will be ignored.
+ val mime = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
+ assumeTrue("Unsupported mime = $mime",
+ "video/mp4".equals(mime, ignoreCase = true))
+ val value = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
+ assertThat(value).isNotNull()
+ // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
+ val matchGroup =
+ "([+-]?[0-9]+(\\.[0-9]+)?)([+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
+ .find(value!!) ?: fail("Fail on checking location metadata: $value")
+ val lat = matchGroup.groupValues[1].toDouble()
+ val lon = matchGroup.groupValues[3].toDouble()
- // MediaMuxer.setLocation rounds the value to 4 decimal places
- val tolerance = 0.0001
- assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
- .that(lat).isWithin(tolerance).of(location.latitude)
- assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
- .that(lon).isWithin(tolerance).of(location.longitude)
- } finally {
- release()
- }
+ // MediaMuxer.setLocation rounds the value to 4 decimal places
+ val tolerance = 0.0001
+ assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
+ .that(lat).isWithin(tolerance).of(location.latitude)
+ assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
+ .that(lon).isWithin(tolerance).of(location.longitude)
}
}
+ @Suppress("SameParameterValue")
private fun checkDurationAtMost(uri: Uri, duration: Long) {
- MediaMetadataRetriever().apply {
- try {
- setDataSource(context, uri)
- val durationFromFile = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
+ MediaMetadataRetriever().useAndRelease {
+ it.setDataSource(context, uri)
+ val durationFromFile = it.getDuration()
- assertThat(durationFromFile).isNotNull()
- assertThat(durationFromFile!!.toLong()).isAtMost(duration)
- } finally {
- release()
- }
+ assertThat(durationFromFile).isNotNull()
+ assertThat(durationFromFile!!).isAtMost(duration)
}
}
// It fails on devices with certain chipset if the codec is stopped when the camera is still
// producing frames to the provided surface. This method first stop the camera from
// producing frames then stops the recording safely on the problematic devices.
- private fun Recording.stopSafely() {
+ private fun Recording.stopSafely(recorder: Recorder) {
val deactivateSurfaceBeforeStop =
DeviceQuirks.get(DeactivateEncoderSurfaceBeforeStopEncoderQuirk::class.java) != null
if (deactivateSurfaceBeforeStop) {
@@ -1487,70 +1146,21 @@
}
stop()
if (deactivateSurfaceBeforeStop && Build.VERSION.SDK_INT >= 23) {
- invokeSurfaceRequest()
+ recorder.sendSurfaceRequest()
}
}
- private fun runFileSizeLimitTest(fileSizeLimit: Long) {
- // For the file size is small, the final file length possibly exceeds the file size limit
- // after adding the file header. We still add the buffer for the tolerance of comparing the
- // file length and file size limit.
- val sizeLimitBuffer = 50 * 1024 // 50k threshold buffer
- invokeSurfaceRequest()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputOptions = FileOutputOptions.Builder(file)
- .setFileSizeLimit(fileSizeLimit)
- .build()
-
- val recording = recorder
- .prepareRecording(context, outputOptions)
- .withAudioEnabled()
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- false, 60000L
- )
-
- val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
- argument -> VideoRecordEvent::class.java.isInstance(argument)
- }
- mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
- false, CallTimesAtLeast(1), captor)
-
- assertThat(captor.value).isInstanceOf(VideoRecordEvent.Finalize::class.java)
- val finalize = captor.value as VideoRecordEvent.Finalize
- assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
- assertThat(file.length()).isLessThan(fileSizeLimit + sizeLimitBuffer)
-
- recording.stopSafely()
-
- file.delete()
- }
-
private fun runLocationTest(location: Location) {
- invokeSurfaceRequest(recorder)
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputOptions = FileOutputOptions.Builder(file)
- .setLocation(location)
- .build()
+ // Arrange.
+ val outputOptions = createFileOutputOptions(location = location)
+ val recording = createRecordingProcess(outputOptions = outputOptions)
- val recording = recorder
- .prepareRecording(context, outputOptions)
- .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
- mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
- recording.stopSafely()
-
- mockVideoRecordEventConsumer.verifyAcceptCall(
- VideoRecordEvent.Finalize::class.java,
- true, GENERAL_TIMEOUT
- )
-
- checkLocation(Uri.fromFile(file), location)
-
- file.delete()
+ // Act.
+ recording.startAndVerify()
+ recording.stopAndVerify { finalize ->
+ // Assert.
+ checkLocation(finalize.outputResults.outputUri, location)
+ }
}
private fun createLocation(
@@ -1563,50 +1173,85 @@
this.longitude = longitude
}
- private fun MockConsumer<VideoRecordEvent>.verifyRecordingStartSuccessfully() {
- verifyAcceptCall(
- VideoRecordEvent.Start::class.java,
- true,
- GENERAL_TIMEOUT
- )
- verifyAcceptCall(
- VideoRecordEvent.Status::class.java,
- true,
- STATUS_TIMEOUT,
- CallTimesAtLeast(5)
+ private fun MockConsumer<VideoRecordEvent>.verifyStart(
+ inOrder: Boolean = true,
+ onEvent: ((Start) -> Unit)? = null
+ ) {
+ verifyEvent(Start::class.java, inOrder = inOrder, onEvent = onEvent)
+ }
+
+ private fun MockConsumer<VideoRecordEvent>.verifyFinalize(
+ inOrder: Boolean = true,
+ timeoutMs: Long = GENERAL_TIMEOUT,
+ onFinalize: ((Finalize) -> Unit)? = null
+ ) {
+ verifyEvent(
+ Finalize::class.java,
+ inOrder = inOrder,
+ timeoutMs = timeoutMs,
+ onEvent = onFinalize
)
}
- class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
- private var countDown: CountDownLatch? = null
- fun waitForVideoCaptureStatus(
- count: Int = 10,
- timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
- ) {
- assertWithMessage("Video recording doesn't start").that(synchronized(this) {
- countDown = CountDownLatch(count)
- countDown
- }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+ private fun MockConsumer<VideoRecordEvent>.verifyStatus(
+ eventCount: Int = DEFAULT_STATUS_COUNT,
+ inOrder: Boolean = true,
+ onEvent: ((List<Status>) -> Unit)? = null,
+ ) {
+ verifyEvent(
+ Status::class.java,
+ CallTimesAtLeast(eventCount),
+ inOrder = inOrder,
+ timeoutMs = STATUS_TIMEOUT,
+ onEvent = onEvent
+ )
+ }
+
+ private fun MockConsumer<VideoRecordEvent>.verifyPause(
+ inOrder: Boolean = true,
+ onEvent: ((Pause) -> Unit)? = null
+ ) {
+ verifyEvent(Pause::class.java, inOrder = inOrder, onEvent = onEvent)
+ }
+
+ private fun MockConsumer<VideoRecordEvent>.verifyResume(
+ inOrder: Boolean = true,
+ onEvent: ((Resume) -> Unit)? = null,
+ ) {
+ verifyEvent(Resume::class.java, inOrder = inOrder, onEvent = onEvent)
+ }
+
+ private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+ eventType: Class<T>,
+ inOrder: Boolean = false,
+ timeoutMs: Long = GENERAL_TIMEOUT,
+ onEvent: ((T) -> Unit)? = null,
+ ) {
+ verifyEvent(
+ eventType,
+ callTimes = CallTimes(1),
+ inOrder = inOrder,
+ timeoutMs = timeoutMs
+ ) { events ->
+ onEvent?.invoke(events.last())
}
+ }
- override fun accept(event: VideoRecordEvent?) {
- when (event) {
- is VideoRecordEvent.Status -> {
- synchronized(this) {
- countDown?.countDown()
- }
- }
- else -> {
- // Ignore other events.
- }
+ private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+ eventType: Class<T>,
+ callTimes: CallTimes,
+ inOrder: Boolean = false,
+ timeoutMs: Long = GENERAL_TIMEOUT,
+ onEvent: ((List<T>) -> Unit)? = null,
+ ) {
+ verifyAcceptCall(eventType, inOrder, timeoutMs, callTimes)
+ if (onEvent != null) {
+ val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+ eventType.isInstance(argument)
}
+ verifyAcceptCall(eventType, false, callTimes, captor)
+ @Suppress("UNCHECKED_CAST")
+ onEvent.invoke(captor.allValues as List<T>)
}
}
-
- private fun Recorder.startVideoRecording(
- file: File,
- eventListener: Consumer<VideoRecordEvent>
- ): Recording = prepareRecording(
- context, FileOutputOptions.Builder(file).build()
- ).start(Dispatchers.Main.asExecutor(), eventListener)
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 3b00ca9..912c57a 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -448,7 +448,7 @@
instrumentation.runOnMainSync {
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
}
- val videoCaptureMonitor = RecorderTest.VideoCaptureMonitor()
+ val videoCaptureMonitor = VideoCaptureMonitor()
videoCapture.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
// Ensure the Recorder is initialized before start test.
videoCaptureMonitor.waitForVideoCaptureStatus()
@@ -1080,10 +1080,46 @@
)
}
-private fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
+private class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
+ private var countDown: CountDownLatch? = null
+
+ fun waitForVideoCaptureStatus(
+ count: Int = 10,
+ timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
+ ) {
+ assertWithMessage("Video recording doesn't start").that(synchronized(this) {
+ countDown = CountDownLatch(count)
+ countDown
+ }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ override fun accept(event: VideoRecordEvent?) {
+ when (event) {
+ is VideoRecordEvent.Status -> {
+ synchronized(this) {
+ countDown?.countDown()
+ }
+ }
+ else -> {
+ // Ignore other events.
+ }
+ }
+ }
+}
+
+internal fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
try {
block(this)
} finally {
release()
}
}
+
+internal fun MediaMetadataRetriever.hasAudio(): Boolean =
+ extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == "yes"
+
+internal fun MediaMetadataRetriever.hasVideo(): Boolean =
+ extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) == "yes"
+
+internal fun MediaMetadataRetriever.getDuration(): Long? =
+ extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.ignore b/compose/ui/ui-graphics/api/current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+ Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+ Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index f1c4192..8f387eca 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -115,7 +115,9 @@
public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public float length;
}
@@ -666,7 +668,9 @@
@kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public abstract float length;
}
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 90bb660..f5a56d4 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -115,7 +115,9 @@
public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public float length;
}
@@ -669,7 +671,9 @@
@kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public abstract float length;
}
diff --git a/compose/ui/ui-graphics/api/restricted_current.ignore b/compose/ui/ui-graphics/api/restricted_current.ignore
new file mode 100644
index 0000000..ac8aa37
--- /dev/null
+++ b/compose/ui/ui-graphics/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getPosition(float):
+ Added method androidx.compose.ui.graphics.PathMeasure.getPosition(float)
+AddedAbstractMethod: androidx.compose.ui.graphics.PathMeasure#getTangent(float):
+ Added method androidx.compose.ui.graphics.PathMeasure.getTangent(float)
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index adad1a5..a7f88e5 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -145,7 +145,9 @@
public final class AndroidPathMeasure implements androidx.compose.ui.graphics.PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public float length;
}
@@ -698,7 +700,9 @@
@kotlin.jvm.JvmDefaultWithCompatibility public interface PathMeasure {
method public float getLength();
+ method public long getPosition(float distance);
method public boolean getSegment(float startDistance, float stopDistance, androidx.compose.ui.graphics.Path destination, optional boolean startWithMoveTo);
+ method public long getTangent(float distance);
method public void setPath(androidx.compose.ui.graphics.Path? path, boolean forceClosed);
property public abstract float length;
}
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
new file mode 100644
index 0000000..afacfe3
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PathMeasureTest {
+
+ @Test
+ fun testGetPositionAndTangent() {
+ val width = 100f
+ val height = 100f
+ val path = Path().apply {
+ lineTo(width, height)
+ }
+ val pathMeasure = PathMeasure()
+
+ pathMeasure.setPath(path, false)
+ val distance = pathMeasure.length
+ val position = pathMeasure.getPosition(distance * 0.5f)
+
+ val tangent = pathMeasure.getTangent(distance * 0.5f)
+
+ Assert.assertEquals(50f, position.x)
+ Assert.assertEquals(50f, position.y)
+ Assert.assertEquals(0.707106f, tangent.x, 0.00001f)
+ Assert.assertEquals(0.707106f, tangent.y, 0.00001f)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
index d9996fa..358b3af 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathMeasure.android.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.graphics
+import androidx.compose.ui.geometry.Offset
+
actual fun PathMeasure(): PathMeasure = AndroidPathMeasure(android.graphics.PathMeasure())
class AndroidPathMeasure internal constructor(
@@ -25,6 +27,10 @@
override val length: Float
get() = internalPathMeasure.length
+ private var positionArray: FloatArray? = null
+
+ private var tangentArray: FloatArray? = null
+
override fun getSegment(
startDistance: Float,
stopDistance: Float,
@@ -42,4 +48,38 @@
override fun setPath(path: Path?, forceClosed: Boolean) {
internalPathMeasure.setPath(path?.asAndroidPath(), forceClosed)
}
+
+ override fun getPosition(
+ distance: Float
+ ): Offset {
+ if (positionArray == null) {
+ positionArray = FloatArray(2)
+ }
+ if (tangentArray == null) {
+ tangentArray = FloatArray(2)
+ }
+ val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+ return if (result) {
+ Offset(positionArray!![0], positionArray!![1])
+ } else {
+ Offset.Unspecified
+ }
+ }
+
+ override fun getTangent(
+ distance: Float
+ ): Offset {
+ if (positionArray == null) {
+ positionArray = FloatArray(2)
+ }
+ if (tangentArray == null) {
+ tangentArray = FloatArray(2)
+ }
+ val result = internalPathMeasure.getPosTan(distance, positionArray, tangentArray)
+ return if (result) {
+ Offset(tangentArray!![0], tangentArray!![1])
+ } else {
+ Offset.Unspecified
+ }
+ }
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
index 9e168cb..d2ee71f 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathMeasure.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.graphics
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.internal.JvmDefaultWithCompatibility
/**
@@ -53,4 +54,22 @@
* Assign a new path, or null to have none.
*/
fun setPath(path: Path?, forceClosed: Boolean)
+
+ /**
+ * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding position
+ *
+ * @param distance The distance along the current contour to sample
+ *
+ * @return [Offset.Unspecified] if there is no path set
+ */
+ fun getPosition(distance: Float): Offset
+
+ /**
+ * Pins distance to 0 <= distance <= getLength(), and then computes the corresponding tangent
+ *
+ * @param distance The distance along the current contour to sample
+ *
+ * @return [Offset.Unspecified] if there is no path set
+ */
+ fun getTangent(distance: Float): Offset
}
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
index 7658c68..810a409 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.graphics
+import androidx.compose.ui.geometry.Offset
/**
* Convert the [org.jetbrains.skia.PathMeasure] instance into a Compose-compatible PathMeasure
*/
@@ -49,6 +50,28 @@
override val length: Float
get() = skia.length
+
+ override fun getPosition(
+ distance: Float
+ ): Offset {
+ val result = skia.getPosition(distance)
+ return if (result != null) {
+ Offset(result.x, result.y)
+ } else {
+ Offset.Unspecified
+ }
+ }
+
+ override fun getTangent(
+ distance: Float
+ ): Offset {
+ val result = skia.getTangent(distance)
+ return if (result != null) {
+ Offset(result.x, result.y)
+ } else {
+ Offset.Unspecified
+ }
+ }
}
actual fun PathMeasure(): PathMeasure =
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
index ca445ce..071f8bd 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
@@ -21,7 +21,6 @@
import android.os.OutcomeReceiver
import android.service.credentials.BeginCreateCredentialRequest
import android.service.credentials.BeginCreateCredentialResponse
-import android.service.credentials.BeginGetCredentialOption
import android.service.credentials.BeginGetCredentialRequest
import android.service.credentials.BeginGetCredentialResponse
import android.service.credentials.CredentialProviderService
@@ -48,21 +47,7 @@
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) {
- val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> =
- mutableListOf()
- request.beginGetCredentialOptions.forEach {
- val structuredOption = BeginGetCredentialUtil
- .convertRequestOption(
- it.type,
- it.candidateQueryData)
- if (structuredOption != null) {
- beginGetCredentialOptions.add(structuredOption)
- }
- }
- val structuredRequest =
- BeginGetCredentialRequest.Builder(request.callingAppInfo)
- .setBeginGetCredentialOptions(beginGetCredentialOptions)
- .build()
+ val structuredRequest = BeginGetCredentialUtil.convertToStructuredRequest(request)
val outcome = object : OutcomeReceiver<BeginGetCredentialResponse,
androidx.credentials.exceptions.GetCredentialException> {
override fun onResult(response: BeginGetCredentialResponse?) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
index c2ae7cb..64277fc 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -26,6 +26,7 @@
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.GetCredentialResponse
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
/**
* PendingIntentHandler to be used by credential providers to extract requests from
@@ -79,10 +80,11 @@
*/
@JvmStatic
fun getBeginGetCredentialRequest(intent: Intent): BeginGetCredentialRequest? {
- return intent.getParcelableExtra(
+ val request = intent.getParcelableExtra(
"android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST",
BeginGetCredentialRequest::class.java
)
+ return request?.let { BeginGetCredentialUtil.convertToStructuredRequest(it) }
}
/**
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
index 8e85125..fef103b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
@@ -35,7 +35,7 @@
class BeginCreateCredentialUtil {
companion object {
@JvmStatic
- fun convertToStructuredRequest(request: BeginCreateCredentialRequest):
+ internal fun convertToStructuredRequest(request: BeginCreateCredentialRequest):
BeginCreateCredentialRequest {
return try {
when (request.type) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
index bed5c11..c743c49 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
@@ -18,12 +18,12 @@
import android.os.Bundle
import android.service.credentials.BeginGetCredentialOption
+import android.service.credentials.BeginGetCredentialRequest
import androidx.annotation.RequiresApi
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.GetPublicKeyCredentialOptionPrivileged
import androidx.credentials.PasswordCredential
import androidx.credentials.PublicKeyCredential
-import androidx.credentials.internal.FrameworkClassParsingException
import androidx.credentials.provider.BeginGetCustomCredentialOption
import androidx.credentials.provider.BeginGetPasswordOption
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
@@ -36,37 +36,49 @@
class BeginGetCredentialUtil {
companion object {
@JvmStatic
- fun convertRequestOption(type: String, candidateQueryData: Bundle):
- BeginGetCredentialOption? {
- return try {
- when (type) {
- PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
- BeginGetPasswordOption(candidateQueryData)
- }
- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
- when (candidateQueryData.getString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
- GetPublicKeyCredentialOption
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION -> {
- BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData)
- }
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
- -> {
- BeginGetPublicKeyCredentialOptionPrivileged
- .createFrom(candidateQueryData)
- }
- else -> {
- throw FrameworkClassParsingException()
- }
+ internal fun convertToStructuredRequest(request: BeginGetCredentialRequest):
+ BeginGetCredentialRequest {
+ val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> =
+ mutableListOf()
+ request.beginGetCredentialOptions.forEach {
+ beginGetCredentialOptions.add(convertRequestOption(
+ it.type,
+ it.candidateQueryData)
+ )
+ }
+ return BeginGetCredentialRequest.Builder(request.callingAppInfo)
+ .setBeginGetCredentialOptions(beginGetCredentialOptions)
+ .build()
+ }
+ @JvmStatic
+ internal fun convertRequestOption(type: String, candidateQueryData: Bundle):
+ BeginGetCredentialOption {
+ return when (type) {
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
+ BeginGetPasswordOption(candidateQueryData)
+ }
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
+ when (candidateQueryData.getString(
+ PublicKeyCredential.BUNDLE_KEY_SUBTYPE
+ )) {
+ GetPublicKeyCredentialOption
+ .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION -> {
+ BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData)
+ }
+ GetPublicKeyCredentialOptionPrivileged
+ .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
+ -> {
+ BeginGetPublicKeyCredentialOptionPrivileged
+ .createFrom(candidateQueryData)
+ }
+ else -> {
+ BeginGetCustomCredentialOption(type, candidateQueryData)
}
}
- else -> {
- BeginGetCustomCredentialOption(type, candidateQueryData)
- }
}
- } catch (e: FrameworkClassParsingException) {
- null
+ else -> {
+ BeginGetCustomCredentialOption(type, candidateQueryData)
+ }
}
}
}
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
index 141611d..a155641 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -15,12 +15,11 @@
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21"/>
<application>
<activity android:name=".MainActivity" android:exported="true"
- android:theme="@style/Theme.AppCompat.DayNight">
+ android:theme="@style/MyPinkTheme">
<!-- Handle Google app icon launch. -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
new file mode 100644
index 0000000..35b4a46
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+
+ <style name="MyPinkTheme" parent="Theme.AppCompat.DayNight" >
+ <!-- The color for selected headers -->
+ <item name="colorAccent">#ffffff</item>
+ <!-- The color for unselected headers -->
+ <item name="colorControlNormal">#FF69B4</item>
+ <!-- The color for all text -->
+ <item name="android:textColorPrimary">#ffffff</item>
+ <!-- The color for variant popup background -->
+ <item name="colorButtonNormal">#FFC0CB</item>
+ </style>
+
+</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
index e27fe2e..a471ae6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -19,7 +19,7 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
- android:tint="?attr/colorControlNormal">
+ android:tint="?attr/colorButtonNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,22h20V2L2,22z"/>
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index d5a9334..675a185 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -18,6 +18,8 @@
import android.content.Context
import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.room.Room
@@ -33,6 +35,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.io.IOException
@@ -153,6 +156,31 @@
}
@Test
+ fun allBookSuspend_autoClose() {
+ val context: Context = ApplicationProvider.getApplicationContext()
+ context.deleteDatabase("autoClose.db")
+ val db = Room.databaseBuilder(
+ context = context,
+ klass = TestDatabase::class.java,
+ name = "test.db"
+ ).setAutoCloseTimeout(10, TimeUnit.MILLISECONDS).build()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ StrictMode.setThreadPolicy(
+ ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .penaltyDeath()
+ .build()
+ )
+ runBlocking {
+ db.booksDao().getBooksSuspend()
+ delay(100) // let db auto-close
+ db.booksDao().getBooksSuspend()
+ }
+ }
+ }
+
+ @Test
@Suppress("DEPRECATION")
fun suspendingBlock_beginEndTransaction() {
runBlocking {
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index 1495051e..69c458d 100644
--- a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -53,7 +53,7 @@
inTransaction: Boolean,
callable: Callable<R>
): R {
- if (db.isOpen && db.inTransaction()) {
+ if (db.isOpenInternal && db.inTransaction()) {
return callable.call()
}
@@ -74,7 +74,7 @@
cancellationSignal: CancellationSignal?,
callable: Callable<R>
): R {
- if (db.isOpen && db.inTransaction()) {
+ if (db.isOpenInternal && db.inTransaction()) {
return callable.call()
}
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index 251b82a..2088242 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -162,11 +162,11 @@
}
}
- // TODO: Close CleanupStatement
- internal fun onAutoCloseCallback() {
+ private fun onAutoCloseCallback() {
synchronized(trackerLock) {
initialized = false
observedTableTracker.resetTriggerState()
+ cleanupStatement?.close()
}
}
@@ -329,7 +329,7 @@
}
internal fun ensureInitialization(): Boolean {
- if (!database.isOpen) {
+ if (!database.isOpenInternal) {
return false
}
if (!initialized) {
@@ -526,7 +526,7 @@
* This api should eventually be public.
*/
internal fun syncTriggers() {
- if (!database.isOpen) {
+ if (!database.isOpenInternal) {
return
}
syncTriggers(database.openHelper.writableDatabase)
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
index 599d9d3..a455992 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
@@ -394,13 +394,22 @@
/**
* True if database connection is open and initialized.
*
+ * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
+ * is considered open even if internally the connection has been closed, unless manually closed.
+ *
* @return true if the database connection is open, false otherwise.
*/
- @Suppress("Deprecation")
+ @Suppress("Deprecation") // Due to usage of `mDatabase`
open val isOpen: Boolean
- get() {
- return (autoCloser?.isActive ?: mDatabase?.isOpen) == true
- }
+ get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true
+
+ /**
+ * True if the actual database connection is open, regardless of auto-close.
+ */
+ @Suppress("Deprecation") // Due to usage of `mDatabase`
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val isOpenInternal: Boolean
+ get() = mDatabase?.isOpen == true
/**
* Closes the database if it is already open.
@@ -1193,7 +1202,7 @@
/**
* Enables auto-closing for the database to free up unused resources. The underlying
- * database will be closed after it's last use after the specified `autoCloseTimeout` has
+ * database will be closed after it's last use after the specified [autoCloseTimeout] has
* elapsed since its last usage. The database will be automatically
* re-opened the next time it is accessed.
*
@@ -1210,7 +1219,7 @@
*
* The auto-closing database operation runs on the query executor.
*
- * The database will not be reopened if the RoomDatabase or the
+ * The database will not be re-opened if the RoomDatabase or the
* SupportSqliteOpenHelper is closed manually (by calling
* [RoomDatabase.close] or [SupportSQLiteOpenHelper.close]. If the
* database is closed manually, you must create a new database using
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
index 5f2c9aa..060110e 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
@@ -77,7 +77,7 @@
doReturn(statement).whenever(mSqliteDb)
.compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
- doReturn(true).whenever(mRoomDatabase).isOpen
+ doReturn(true).whenever(mRoomDatabase).isOpenInternal
doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
val closeLock = ReentrantLock()
doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
@@ -247,7 +247,7 @@
@Test
fun closedDb() {
- doReturn(false).whenever(mRoomDatabase).isOpen
+ doReturn(false).whenever(mRoomDatabase).isOpenInternal
doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
mTracker.addObserver(LatchObserver(1, "a", "b"))
mTracker.refreshRunnable.run()
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
index 1ae07c2..1569ff6 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
@@ -18,6 +18,7 @@
import android.os.Bundle;
import android.webkit.WebView;
+import android.webkit.WebViewClient;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@@ -33,6 +34,7 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_drag);
WebView demoWebview = findViewById(R.id.image_webview);
+ demoWebview.setWebViewClient(new WebViewClient()); // Open links in this WebView.
demoWebview.loadUrl("www.google.com");
}
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -5,6 +5,16 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
}
+ public final class DropDataContentProvider extends android.content.ContentProvider {
+ ctor public DropDataContentProvider();
+ method public int delete(android.net.Uri, String?, String![]?);
+ method public String? getType(android.net.Uri);
+ method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public boolean onCreate();
+ method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public abstract class JavaScriptReplyProxy {
method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
}
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -5,6 +5,16 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
}
+ public final class DropDataContentProvider extends android.content.ContentProvider {
+ ctor public DropDataContentProvider();
+ method public int delete(android.net.Uri, String?, String![]?);
+ method public String? getType(android.net.Uri);
+ method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public boolean onCreate();
+ method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public abstract class JavaScriptReplyProxy {
method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
}
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index faf13cb..0c9d5ee 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -5,6 +5,16 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
}
+ public final class DropDataContentProvider extends android.content.ContentProvider {
+ ctor public DropDataContentProvider();
+ method public int delete(android.net.Uri, String?, String![]?);
+ method public String? getType(android.net.Uri);
+ method public android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public boolean onCreate();
+ method public android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public abstract class JavaScriptReplyProxy {
method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
index 6c38760..6af699c 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.webkit.internal.WebViewGlueCommunicator;
import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
@@ -33,13 +32,23 @@
import java.io.FileNotFoundException;
/**
- * TODO(1353048): Un-hide this after finishing the feature.
+ * WebView provides partial support for Android
+ * <a href="https://developer.android.com/develop/ui/views/touch-and-input/drag-drop">
+ * Drag and Drop</a> allowing images, text and links to be dragged out of a WebView.
*
- * @hide
- * This should be added to the manifest in order to enable dragging images out.
+ * The content provider is required to make the images drag work, to enable, you should add this
+ * class to your manifest, for example:
+ *
+ * <pre class="prettyprint">
+ * <provider
+ * android:authorities="{{your package}}.DropDataProvider"
+ * android:name="androidx.webkit.DropDataContentProvider"
+ * android:exported="false"
+ * android:grantUriPermissions="true"/>
+ * </pre>
+ *
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class DropDataContentProvider extends ContentProvider {
+public final class DropDataContentProvider extends ContentProvider {
DropDataContentProviderBoundaryInterface mImpl;
@Override
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -37,7 +37,7 @@
method public void clearSplitInfoCallback();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
- method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+ method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
}
@@ -106,17 +106,13 @@
method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
}
- public interface SplitAttributesCalculator {
- method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
- }
-
- public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public class SplitAttributesCalculatorParams {
+ method public boolean areDefaultConstraintsSatisfied();
method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public android.view.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
}
public class SplitInfo {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -37,7 +37,7 @@
method public void clearSplitInfoCallback();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
- method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+ method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
}
@@ -106,17 +106,13 @@
method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
}
- public interface SplitAttributesCalculator {
- method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
- }
-
- public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public class SplitAttributesCalculatorParams {
+ method public boolean areDefaultConstraintsSatisfied();
method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public android.view.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
}
public class SplitInfo {
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index d008345..ead684f 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -37,7 +37,7 @@
method public void clearSplitInfoCallback();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
- method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
+ method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
}
@@ -106,17 +106,13 @@
method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
}
- public interface SplitAttributesCalculator {
- method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
- }
-
- public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public class SplitAttributesCalculatorParams {
+ method public boolean areDefaultConstraintsSatisfied();
method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public android.view.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
}
public class SplitInfo {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index f8707a9..df1fa32 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -73,7 +73,8 @@
* <ul>
* <li>{@link SplitPlaceholderRule.Builder#setFinishPrimaryWithPlaceholder(int)}</li>
* <li>{@link androidx.window.extensions.embedding.SplitAttributes} APIs</li>
- * <li>{@link androidx.window.extensions.embedding.SplitAttributesCalculator} APIs</li>
+ * <li>{@link ActivityEmbeddingComponent#setSplitAttributesCalculator(
+ * androidx.window.extensions.core.util.function.Function)}</li>
* </ul>
* </p>
* @hide
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index fd99cab..9e5ef39 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -17,10 +17,12 @@
package androidx.window.extensions.embedding;
import android.app.Activity;
+import android.view.WindowMetrics;
import androidx.annotation.NonNull;
import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.core.util.function.Function;
import java.util.List;
import java.util.Set;
@@ -80,18 +82,24 @@
boolean isActivityEmbedded(@NonNull Activity activity);
/**
- * Sets a {@link SplitAttributesCalculator}.
+ * Sets a callback to compute the {@link SplitAttributes} for the {@link SplitRule} and current
+ * window state provided in {@link SplitAttributesCalculatorParams}. This method can be used
+ * to dynamically configure the split layout properties when new activities are launched or
+ * window properties change. If set, {@link SplitRule#getDefaultSplitAttributes() the default
+ * split properties} and {@link SplitRule#checkParentMetrics(WindowMetrics) restrictions}
+ * will be ignored, and the callback will be invoked for every change.
*
- * @param calculator the calculator to set. It will replace the previously set
- * {@link SplitAttributesCalculator} if it exists.
+ * @param calculator the callback to set. It will replace the previously set callback if it
+ * exists.
* @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
*/
- void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator);
+ void setSplitAttributesCalculator(
+ @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator);
/**
- * Clears the previously set {@link SplitAttributesCalculator}.
+ * Clears the previously callback set in {@link #setSplitAttributesCalculator(Function)}.
*
- * @see #setSplitAttributesCalculator(SplitAttributesCalculator)
+ * @see #setSplitAttributesCalculator(Function)
* @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
*/
void clearSplitAttributesCalculator();
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index 179afb4..6c9577d 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -17,6 +17,7 @@
package androidx.window.extensions.embedding;
import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Function;
import java.util.Objects;
@@ -32,13 +33,13 @@
mTag = tag;
}
- // TODO(b/240912390): refer to the real API in later CLs.
/**
* A unique string to identify this {@link EmbeddingRule}.
* The suggested usage is to set the tag in the corresponding rule builder to be able to
- * differentiate between different rules in the callbacks. For example, it can be used to
- * compute the right {@link SplitAttributes} for the right split rule in
- * {@code SplitAttributesCalculator#computeSplitAttributesForState}.
+ * differentiate between different rules in {@link SplitAttributes} calculator function. For
+ * example, it can be used to compute the {@link SplitAttributes} for the specific
+ * {@link SplitRule} in the {@link Function} set with
+ * {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(Function)}.
*
* @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
*/
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
index 992d934..d724e1b 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -29,7 +29,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.window.extensions.layout.FoldingFeature;
+import androidx.window.extensions.core.util.function.Function;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -56,10 +56,9 @@
* <li>Setting the default {@code SplitAttributes} using
* {@link SplitPairRule.Builder#setDefaultSplitAttributes} or
* {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes}.</li>
- * <li>Using
- * {@link SplitAttributesCalculator#computeSplitAttributesForParams}
- * to customize the {@code SplitAttributes} for a given device and
- * window state.</li>
+ * <li>Using {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(Function)} to set
+ * the callback to customize the {@code SplitAttributes} for a given device and window
+ * state.</li>
* </ul>
*
* @see SplitAttributes.SplitType
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
deleted file mode 100644
index ef69323..0000000
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
+++ /dev/null
@@ -1,156 +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.window.extensions.embedding;
-
-import android.content.res.Configuration;
-import android.os.Build;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.window.extensions.layout.WindowLayoutInfo;
-
-/**
- * A developer-defined {@link SplitAttributes} calculator to compute the current split layout with
- * the current device and window state.
- *
- * @see ActivityEmbeddingComponent#setSplitAttributesCalculator(SplitAttributesCalculator)
- * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
- * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
- */
-public interface SplitAttributesCalculator {
- /**
- * Computes the {@link SplitAttributes} with the current device state.
- *
- * @param params See {@link SplitAttributesCalculatorParams}
- */
- @NonNull
- SplitAttributes computeSplitAttributesForParams(
- @NonNull SplitAttributesCalculatorParams params
- );
-
- /** The container of {@link SplitAttributesCalculator} parameters */
- class SplitAttributesCalculatorParams {
- @NonNull
- private final WindowMetrics mParentWindowMetrics;
- @NonNull
- private final Configuration mParentConfiguration;
- @NonNull
- private final SplitAttributes mDefaultSplitAttributes;
- private final boolean mIsDefaultMinSizeSatisfied;
- @NonNull
- private final WindowLayoutInfo mParentWindowLayoutInfo;
- @Nullable
- private final String mSplitRuleTag;
-
- /** Returns the parent container's {@link WindowMetrics} */
- @NonNull
- public WindowMetrics getParentWindowMetrics() {
- return mParentWindowMetrics;
- }
-
- /** Returns the parent container's {@link Configuration} */
- @NonNull
- public Configuration getParentConfiguration() {
- return new Configuration(mParentConfiguration);
- }
-
- /**
- * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
- * {@link SplitRule} Builder APIs
- * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
- * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
- * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
- * definitions.
- */
- @NonNull
- public SplitAttributes getDefaultSplitAttributes() {
- return mDefaultSplitAttributes;
- }
-
- /**
- * Returns whether the {@link #getParentWindowMetrics()} satisfies
- * {@link SplitRule#checkParentMetrics(WindowMetrics)} with the minimal size requirement
- * specified in the {@link SplitRule} Builder constructors.
- *
- * @see SplitPairRule.Builder
- * @see SplitPlaceholderRule.Builder
- */
- public boolean isDefaultMinSizeSatisfied() {
- return mIsDefaultMinSizeSatisfied;
- }
-
- /** Returns the parent container's {@link WindowLayoutInfo} */
- @NonNull
- public WindowLayoutInfo getParentWindowLayoutInfo() {
- return mParentWindowLayoutInfo;
- }
-
- /** Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result. */
- @Nullable
- public String getSplitRuleTag() {
- return mSplitRuleTag;
- }
-
- SplitAttributesCalculatorParams(
- @NonNull WindowMetrics parentWindowMetrics,
- @NonNull Configuration parentConfiguration,
- @NonNull SplitAttributes defaultSplitAttributes,
- boolean isDefaultMinSizeSatisfied,
- @NonNull WindowLayoutInfo parentWindowLayoutInfo,
- @Nullable String splitRuleTag
- ) {
- mParentWindowMetrics = parentWindowMetrics;
- mParentConfiguration = parentConfiguration;
- mDefaultSplitAttributes = defaultSplitAttributes;
- mIsDefaultMinSizeSatisfied = isDefaultMinSizeSatisfied;
- mParentWindowLayoutInfo = parentWindowLayoutInfo;
- mSplitRuleTag = splitRuleTag;
- }
-
- @NonNull
- @Override
- public String toString() {
- return getClass().getSimpleName() + ":{"
- + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
- + ", configuration=" + mParentConfiguration
- + ", windowLayoutInfo=" + mParentWindowLayoutInfo
- + ", defaultSplitAttributes=" + mDefaultSplitAttributes
- + ", isDefaultMinSizeSatisfied=" + mIsDefaultMinSizeSatisfied
- + ", tag=" + mSplitRuleTag + "}";
- }
-
- private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
- // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- return Api30Impl.windowMetricsToString(windowMetrics);
- }
- throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
- }
-
- @RequiresApi(30)
- private static final class Api30Impl {
- static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
- return WindowMetrics.class.getSimpleName() + ":{"
- + "bounds=" + windowMetrics.getBounds()
- + ", windowInsets=" + windowMetrics.getWindowInsets()
- + "}";
- }
- }
- }
-}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
new file mode 100644
index 0000000..e26e496
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+/**
+ * The parameter container used to report the current device and window state in
+ * {@link ActivityEmbeddingComponent#setSplitAttributesCalculator(
+ * androidx.window.extensions.core.util.function.Function)} and references the corresponding
+ * {@link SplitRule} by {@link #getSplitRuleTag()} if {@link SplitRule#getTag()} is specified.
+ *
+ * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
+ * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+ */
+public class SplitAttributesCalculatorParams {
+ @NonNull
+ private final WindowMetrics mParentWindowMetrics;
+ @NonNull
+ private final Configuration mParentConfiguration;
+ @NonNull
+ private final WindowLayoutInfo mParentWindowLayoutInfo;
+ @NonNull
+ private final SplitAttributes mDefaultSplitAttributes;
+ private final boolean mAreDefaultConstraintsSatisfied;
+ @Nullable
+ private final String mSplitRuleTag;
+
+ /** Returns the parent container's {@link WindowMetrics} */
+ @NonNull
+ public WindowMetrics getParentWindowMetrics() {
+ return mParentWindowMetrics;
+ }
+
+ /** Returns the parent container's {@link Configuration} */
+ @NonNull
+ public Configuration getParentConfiguration() {
+ return new Configuration(mParentConfiguration);
+ }
+
+ /**
+ * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
+ * {@link SplitRule} Builder APIs
+ * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
+ * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
+ * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
+ * definitions.
+ */
+ @NonNull
+ public SplitAttributes getDefaultSplitAttributes() {
+ return mDefaultSplitAttributes;
+ }
+
+ /**
+ * Returns whether the {@link #getParentWindowMetrics()} satisfies the dimensions and aspect
+ * ratios requirements specified in the {@link androidx.window.embedding.SplitRule}, which
+ * are:
+ * - {@link androidx.window.embedding.SplitRule#minWidthDp}
+ * - {@link androidx.window.embedding.SplitRule#minHeightDp}
+ * - {@link androidx.window.embedding.SplitRule#minSmallestWidthDp}
+ * - {@link androidx.window.embedding.SplitRule#maxAspectRatioInPortrait}
+ * - {@link androidx.window.embedding.SplitRule#maxAspectRatioInLandscape}
+ */
+ public boolean areDefaultConstraintsSatisfied() {
+ return mAreDefaultConstraintsSatisfied;
+ }
+
+ /** Returns the parent container's {@link WindowLayoutInfo} */
+ @NonNull
+ public WindowLayoutInfo getParentWindowLayoutInfo() {
+ return mParentWindowLayoutInfo;
+ }
+
+ /**
+ * Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result if it was
+ * set.
+ */
+ @Nullable
+ public String getSplitRuleTag() {
+ return mSplitRuleTag;
+ }
+
+ SplitAttributesCalculatorParams(
+ @NonNull WindowMetrics parentWindowMetrics,
+ @NonNull Configuration parentConfiguration,
+ @NonNull WindowLayoutInfo parentWindowLayoutInfo,
+ @NonNull SplitAttributes defaultSplitAttributes,
+ boolean areDefaultConstraintsSatisfied,
+ @Nullable String splitRuleTag
+ ) {
+ mParentWindowMetrics = parentWindowMetrics;
+ mParentConfiguration = parentConfiguration;
+ mParentWindowLayoutInfo = parentWindowLayoutInfo;
+ mDefaultSplitAttributes = defaultSplitAttributes;
+ mAreDefaultConstraintsSatisfied = areDefaultConstraintsSatisfied;
+ mSplitRuleTag = splitRuleTag;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":{"
+ + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
+ + ", configuration=" + mParentConfiguration
+ + ", windowLayoutInfo=" + mParentWindowLayoutInfo
+ + ", defaultSplitAttributes=" + mDefaultSplitAttributes
+ + ", areDefaultConstraintsSatisfied=" + mAreDefaultConstraintsSatisfied
+ + ", tag=" + mSplitRuleTag + "}";
+ }
+
+ private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+ // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Api30Impl.windowMetricsToString(windowMetrics);
+ }
+ throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
+ }
+
+ @RequiresApi(30)
+ private static final class Api30Impl {
+ static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+ return WindowMetrics.class.getSimpleName() + ":{"
+ + "bounds=" + windowMetrics.getBounds()
+ + ", windowInsets=" + windowMetrics.getWindowInsets()
+ + "}";
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index b975fe1..10dac95 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -163,8 +163,8 @@
* @param activityIntentPredicate the {@link Predicate} to verify if an ({@link Activity},
* {@link Intent}) pair matches this rule
* @param parentWindowMetricsPredicate the {@link Predicate} to verify if the matched split
- * pair is allowed to show side-by-side with the given
- * parent {@link WindowMetrics}
+ * pair is allowed to show adjacent to each other with the
+ * given parent {@link WindowMetrics}
* @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
public Builder(@NonNull Predicate<Pair<Activity, Activity>> activityPairPredicate,
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 1e6d044..a1be65d 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -35,7 +35,7 @@
/**
* Split configuration rules for activities that are launched to side in a split. Define when an
* activity that was launched in a side container from another activity should be shown
- * side-by-side or on top of it, as well as the visual properties of the split. Can be applied to
+ * adjacent or on top of it, as well as the visual properties of the split. Can be applied to
* new activities started from the same process automatically by the embedding implementation on
* the device.
*/
@@ -57,9 +57,8 @@
*/
public static final int FINISH_ALWAYS = 1;
/**
- * Only finish the associated container when displayed side-by-side/adjacent to the one
- * being finished. Does not finish the associated one when containers are stacked on top of
- * each other.
+ * Only finish the associated container when displayed adjacent to the one being finished. Does
+ * not finish the associated one when containers are stacked on top of each other.
* @see SplitFinishBehavior
*/
public static final int FINISH_ADJACENT = 2;
@@ -70,7 +69,7 @@
* <p>
* For example, given that {@link SplitPairRule#getFinishPrimaryWithSecondary()} is
* {@link #FINISH_ADJACENT} and secondary container finishes. The primary associated
- * container is finished if it's side-by-side with secondary container. The primary
+ * container is finished if it's shown adjacent to the secondary container. The primary
* associated container is not finished if it occupies entire task bounds.</p>
*
* @see SplitPairRule#getFinishPrimaryWithSecondary()
@@ -92,6 +91,18 @@
mDefaultSplitAttributes = defaultSplitAttributes;
}
+ /**
+ * Checks whether the parent window satisfied the dimensions and aspect ratios requirements
+ * specified in the {@link androidx.window.embedding.SplitRule}, which are
+ * {@link androidx.window.embedding.SplitRule#minWidthDp},
+ * {@link androidx.window.embedding.SplitRule#minHeightDp},
+ * {@link androidx.window.embedding.SplitRule#minSmallestWidthDp},
+ * {@link androidx.window.embedding.SplitRule#maxAspectRatioInPortrait} and
+ * {@link androidx.window.embedding.SplitRule#maxAspectRatioInLandscape}.
+ *
+ * @param parentMetrics the {@link WindowMetrics} of the parent window.
+ * @return whether the parent window satisfied the {@link SplitRule} requirements.
+ */
@SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean checkParentMetrics(@NonNull WindowMetrics parentMetrics) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index d7d4b1f..8665740 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -33,7 +33,7 @@
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
-import androidx.window.embedding.SplitAttributesCalculator
+import androidx.window.embedding.SplitAttributesCalculatorParams
import androidx.window.embedding.SplitController
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
@@ -43,10 +43,12 @@
* Initializes SplitController with a set of statically defined rules.
*/
class ExampleWindowInitializer : Initializer<RuleController> {
+ private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
override fun create(context: Context): RuleController {
SplitController.getInstance(context).apply {
if (isSplitAttributesCalculatorSupported()) {
- setSplitAttributesCalculator(SampleSplitAttributesCalculator())
+ setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
}
}
return RuleController.getInstance(context).apply {
@@ -57,86 +59,132 @@
}
/**
- * A sample [SplitAttributesCalculator] to demonstrate how to change the [SplitAttributes] with
- * the current device and window state and
- * [SplitAttributesCalculator.SplitAttributesCalculatorParams.splitRuleTag].
+ * A sample callback set in [SplitController.setSplitAttributesCalculator] to demonstrate how to
+ * change the [SplitAttributes] with the current device and window state and
+ * [SplitAttributesCalculatorParams.splitRuleTag].
*/
- private class SampleSplitAttributesCalculator : SplitAttributesCalculator {
-
- private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
-
- override fun computeSplitAttributesForParams(
- params: SplitAttributesCalculator.SplitAttributesCalculatorParams
- ): SplitAttributes {
- val isPortrait = params.parentWindowMetrics.isPortrait()
- val windowLayoutInfo = params.parentWindowLayoutInfo
- val isTabletop = windowLayoutInfo.isTabletop()
- val isBookMode = windowLayoutInfo.isBookMode()
- val config = params.parentConfiguration
- // The SplitAttributes to occupy the whole task bounds
- val expandContainersAttrs = SplitAttributes.Builder()
- .setSplitType(SplitAttributes.SplitType.expandContainers())
- .build()
- val tag = params.splitRuleTag
- val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
- // Make a copy of the default splitAttributes, but replace the animation background
- // color to what is configured in the Demo app.
- val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
- val defaultSplitAttributes = SplitAttributes.Builder()
- .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
- .setSplitType(params.defaultSplitAttributes.splitType)
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- when (tag?.substringBefore(SUFFIX_REVERSED)) {
- TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
- return if (params.isDefaultMinSizeSatisfied) {
- defaultSplitAttributes
- } else {
- expandContainersAttrs
- }
+ private fun sampleSplitAttributesCalculator(
+ params: SplitAttributesCalculatorParams
+ ): SplitAttributes {
+ val isPortrait = params.parentWindowMetrics.isPortrait()
+ val windowLayoutInfo = params.parentWindowLayoutInfo
+ val isTabletop = windowLayoutInfo.isTabletop()
+ val isBookMode = windowLayoutInfo.isBookMode()
+ val config = params.parentConfiguration
+ // The SplitAttributes to occupy the whole task bounds
+ val expandContainersAttrs = SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.expandContainers())
+ .build()
+ val tag = params.splitRuleTag
+ val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
+ // Make a copy of the default splitAttributes, but replace the animation background
+ // color to what is configured in the Demo app.
+ val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
+ val defaultSplitAttributes = SplitAttributes.Builder()
+ .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
+ .setSplitType(params.defaultSplitAttributes.splitType)
+ .setAnimationBackgroundColor(backgroundColor)
+ .build()
+ when (tag?.substringBefore(SUFFIX_REVERSED)) {
+ TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
+ return if (params.areDefaultConstraintsSatisfied) {
+ defaultSplitAttributes
+ } else {
+ expandContainersAttrs
}
- TAG_SHOW_FULLSCREEN_IN_PORTRAIT -> {
- if (isPortrait) {
- return expandContainersAttrs
- }
+ }
+ TAG_SHOW_FULLSCREEN_IN_PORTRAIT -> {
+ if (isPortrait) {
+ return expandContainersAttrs
}
- TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
- if (isTabletop) {
- return SplitAttributes.Builder()
- .setSplitType(SplitAttributes.SplitType.splitByHinge())
- .setLayoutDirection(
- if (shouldReversed) {
- BOTTOM_TO_TOP
- } else {
- TOP_TO_BOTTOM
- }
- )
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- } else if (isPortrait) {
- return expandContainersAttrs
- }
- }
- TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
- if (isTabletop) {
- return SplitAttributes.Builder()
- .setSplitType(SplitAttributes.SplitType.splitByHinge())
- .setLayoutDirection(
- if (shouldReversed) {
- BOTTOM_TO_TOP
- } else {
- TOP_TO_BOTTOM
- }
- )
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- }
- }
- TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE -> {
+ }
+ TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+ if (isTabletop) {
return SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.splitByHinge())
+ .setLayoutDirection(
+ if (shouldReversed) {
+ BOTTOM_TO_TOP
+ } else {
+ TOP_TO_BOTTOM
+ }
+ )
+ .setAnimationBackgroundColor(backgroundColor)
+ .build()
+ } else if (isPortrait) {
+ return expandContainersAttrs
+ }
+ }
+ TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+ if (isTabletop) {
+ return SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.splitByHinge())
+ .setLayoutDirection(
+ if (shouldReversed) {
+ BOTTOM_TO_TOP
+ } else {
+ TOP_TO_BOTTOM
+ }
+ )
+ .setAnimationBackgroundColor(backgroundColor)
+ .build()
+ }
+ }
+ TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE -> {
+ return SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.splitByHinge())
+ .setLayoutDirection(
+ if (shouldReversed) {
+ BOTTOM_TO_TOP
+ } else {
+ TOP_TO_BOTTOM
+ }
+ ).build()
+ }
+ TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE -> {
+ return if (isBookMode) {
+ expandContainersAttrs
+ } else if (config.screenWidthDp <= 600) {
+ SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.splitEqually())
.setLayoutDirection(
- if (config.screenWidthDp <= 600) {
+ if (shouldReversed) {
+ BOTTOM_TO_TOP
+ } else {
+ TOP_TO_BOTTOM
+ }
+ )
+ .setAnimationBackgroundColor(backgroundColor)
+ .build()
+ } else {
+ SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.splitEqually())
+ .setLayoutDirection(
+ if (shouldReversed) {
+ RIGHT_TO_LEFT
+ } else {
+ LEFT_TO_RIGHT
+ }
+ )
+ .setAnimationBackgroundColor(backgroundColor)
+ .build()
+ }
+ }
+ TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING -> {
+ val foldingState = windowLayoutInfo.getFoldingFeature()
+ if (foldingState != null) {
+ return SplitAttributes.Builder()
+ .setSplitType(
+ if (foldingState.isSeparating) {
+ SplitAttributes.SplitType.splitByHinge()
+ } else {
+ SplitAttributes.SplitType.ratio(0.3f)
+ }
+ ).setLayoutDirection(
+ if (
+ foldingState.orientation
+ == FoldingFeature.Orientation.HORIZONTAL
+ ) {
if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
} else {
if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
@@ -145,86 +193,33 @@
.setAnimationBackgroundColor(backgroundColor)
.build()
}
- TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE -> {
- return if (isBookMode) {
- expandContainersAttrs
- } else if (config.screenWidthDp <= 600) {
- SplitAttributes.Builder()
- .setSplitType(SplitAttributes.SplitType.splitEqually())
- .setLayoutDirection(
- if (shouldReversed) {
- BOTTOM_TO_TOP
- } else {
- TOP_TO_BOTTOM
- }
- )
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- } else {
- SplitAttributes.Builder()
- .setSplitType(SplitAttributes.SplitType.splitEqually())
- .setLayoutDirection(
- if (shouldReversed) {
- RIGHT_TO_LEFT
- } else {
- LEFT_TO_RIGHT
- }
- )
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- }
- }
- TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING -> {
- val foldingState = windowLayoutInfo.getFoldingFeature()
- if (foldingState != null) {
- return SplitAttributes.Builder()
- .setSplitType(
- if (foldingState.isSeparating) {
- SplitAttributes.SplitType.splitByHinge()
- } else {
- SplitAttributes.SplitType.ratio(0.3f)
- }
- ).setLayoutDirection(
- if (
- foldingState.orientation
- == FoldingFeature.Orientation.HORIZONTAL
- ) {
- if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
- } else {
- if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
- }
- )
- .setAnimationBackgroundColor(backgroundColor)
- .build()
- }
- }
}
- return defaultSplitAttributes
}
+ return defaultSplitAttributes
+ }
- private fun WindowMetrics.isPortrait(): Boolean =
- bounds.height() > bounds.width()
+ private fun WindowMetrics.isPortrait(): Boolean =
+ bounds.height() > bounds.width()
- private fun WindowLayoutInfo.isTabletop(): Boolean {
- val foldingFeature = getFoldingFeature()
- return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
- foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
- }
+ private fun WindowLayoutInfo.isTabletop(): Boolean {
+ val foldingFeature = getFoldingFeature()
+ return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+ foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+ }
- private fun WindowLayoutInfo.isBookMode(): Boolean {
- val foldingFeature = getFoldingFeature()
- return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
- foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
- }
+ private fun WindowLayoutInfo.isBookMode(): Boolean {
+ val foldingFeature = getFoldingFeature()
+ return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+ foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
+ }
- /**
- * Returns the [FoldingFeature] if it is exactly the only [FoldingFeature] in
- * [WindowLayoutInfo]. Otherwise, returns `null`.
- */
- private fun WindowLayoutInfo.getFoldingFeature(): FoldingFeature? {
- val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
- return if (foldingFeatures.size == 1) foldingFeatures[0] else null
- }
+ /**
+ * Returns the [FoldingFeature] if it is exactly the only [FoldingFeature] in
+ * [WindowLayoutInfo]. Otherwise, returns `null`.
+ */
+ private fun WindowLayoutInfo.getFoldingFeature(): FoldingFeature? {
+ val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
+ return if (foldingFeatures.size == 1) foldingFeatures[0] else null
}
override fun dependencies(): List<Class<out Initializer<*>>> {
diff --git a/window/window-rxjava2/api/api_lint.ignore b/window/window-rxjava2/api/api_lint.ignore
new file mode 100644
index 0000000..636cfcc
--- /dev/null
+++ b/window/window-rxjava2/api/api_lint.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+ContextFirst: androidx.window.rxjava2.layout.WindowInfoTrackerRx#windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+ Context is distinct, so it must be the first argument (method `windowLayoutInfoFlowable`)
+ContextFirst: androidx.window.rxjava2.layout.WindowInfoTrackerRx#windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+ Context is distinct, so it must be the first argument (method `windowLayoutInfoObservable`)
diff --git a/window/window-rxjava2/api/current.txt b/window/window-rxjava2/api/current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/current.txt
+++ b/window/window-rxjava2/api/current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava2/api/public_plus_experimental_current.txt b/window/window-rxjava2/api/public_plus_experimental_current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/public_plus_experimental_current.txt
+++ b/window/window-rxjava2/api/public_plus_experimental_current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava2/api/restricted_current.txt b/window/window-rxjava2/api/restricted_current.txt
index 8135cee..5250696 100644
--- a/window/window-rxjava2/api/restricted_current.txt
+++ b/window/window-rxjava2/api/restricted_current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava2/build.gradle b/window/window-rxjava2/build.gradle
index 389a75c..dee2b16 100644
--- a/window/window-rxjava2/build.gradle
+++ b/window/window-rxjava2/build.gradle
@@ -37,6 +37,7 @@
api(libs.kotlinCoroutinesRx2)
api(libs.rxjava2)
api(project(":window:window"))
+ implementation 'androidx.annotation:annotation:1.5.0'
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
index a22df89..418725d 100644
--- a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
+++ b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/layout/WindowInfoTrackerRxTest.kt
@@ -17,6 +17,7 @@
package androidx.window.rxjava2.layout
import android.app.Activity
+import android.content.Context
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
@@ -28,10 +29,10 @@
/**
* Tests for the RxJava 2 adapters.
*/
-public class WindowInfoTrackerRxTest {
+class WindowInfoTrackerRxTest {
@Test
- public fun testWindowLayoutInfoObservable() {
+ fun testWindowLayoutInfoObservable() {
val activity = mock<Activity>()
val feature = mock<FoldingFeature>()
val expected = WindowLayoutInfo(listOf(feature))
@@ -44,7 +45,7 @@
}
@Test
- public fun testWindowLayoutInfoFlowable() {
+ fun testWindowLayoutInfoFlowable() {
val activity = mock<Activity>()
val feature = mock<FoldingFeature>()
val expected = WindowLayoutInfo(listOf(feature))
@@ -55,4 +56,29 @@
testSubscriber.assertValue(expected)
}
+ @Test
+ fun testWindowLayoutInfoObservable_context() {
+ val activity = mock<Context>()
+ val feature = mock<FoldingFeature>()
+ val expected = WindowLayoutInfo(listOf(feature))
+ val mockTracker = mock<WindowInfoTracker>()
+ whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+ val testSubscriber = mockTracker.windowLayoutInfoObservable(activity).test()
+
+ testSubscriber.assertValue(expected)
+ }
+
+ @Test
+ fun testWindowLayoutInfoFlowable_context() {
+ val activity = mock<Context>()
+ val feature = mock<FoldingFeature>()
+ val expected = WindowLayoutInfo(listOf(feature))
+ val mockTracker = mock<WindowInfoTracker>()
+ whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+ val testSubscriber = mockTracker.windowLayoutInfoFlowable(activity).test()
+
+ testSubscriber.assertValue(expected)
+ }
}
diff --git a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
index 2a82d8c..d41cc80 100644
--- a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
+++ b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/layout/WindowInfoTrackerRx.kt
@@ -18,6 +18,8 @@
package androidx.window.rxjava2.layout
import android.app.Activity
+import android.content.Context
+import androidx.annotation.UiContext
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import io.reactivex.Flowable
@@ -29,7 +31,7 @@
* Return an [Observable] stream of [WindowLayoutInfo].
* @see WindowInfoTracker.windowLayoutInfo
*/
-public fun WindowInfoTracker.windowLayoutInfoObservable(
+fun WindowInfoTracker.windowLayoutInfoObservable(
activity: Activity
): Observable<WindowLayoutInfo> {
return windowLayoutInfo(activity).asObservable()
@@ -39,8 +41,28 @@
* Return a [Flowable] stream of [WindowLayoutInfo].
* @see WindowInfoTracker.windowLayoutInfo
*/
-public fun WindowInfoTracker.windowLayoutInfoFlowable(
+fun WindowInfoTracker.windowLayoutInfoFlowable(
activity: Activity
): Flowable<WindowLayoutInfo> {
return windowLayoutInfo(activity).asFlowable()
}
+
+/**
+ * Return an [Observable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoObservable(
+ @UiContext context: Context
+): Observable<WindowLayoutInfo> {
+ return windowLayoutInfo(context).asObservable()
+}
+
+/**
+ * Return a [Flowable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoFlowable(
+ @UiContext context: Context
+): Flowable<WindowLayoutInfo> {
+ return windowLayoutInfo(context).asFlowable()
+}
diff --git a/window/window-rxjava3/api/api_lint.ignore b/window/window-rxjava3/api/api_lint.ignore
new file mode 100644
index 0000000..8c55c33
--- /dev/null
+++ b/window/window-rxjava3/api/api_lint.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+ContextFirst: androidx.window.rxjava3.layout.WindowInfoTrackerRx#windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+ Context is distinct, so it must be the first argument (method `windowLayoutInfoFlowable`)
+ContextFirst: androidx.window.rxjava3.layout.WindowInfoTrackerRx#windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.content.Context) parameter #1:
+ Context is distinct, so it must be the first argument (method `windowLayoutInfoObservable`)
diff --git a/window/window-rxjava3/api/current.txt b/window/window-rxjava3/api/current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/current.txt
+++ b/window/window-rxjava3/api/current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava3/api/public_plus_experimental_current.txt b/window/window-rxjava3/api/public_plus_experimental_current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/public_plus_experimental_current.txt
+++ b/window/window-rxjava3/api/public_plus_experimental_current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava3/api/restricted_current.txt b/window/window-rxjava3/api/restricted_current.txt
index 5700dd3..23510cc 100644
--- a/window/window-rxjava3/api/restricted_current.txt
+++ b/window/window-rxjava3/api/restricted_current.txt
@@ -3,7 +3,9 @@
public final class WindowInfoTrackerRx {
method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Flowable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoFlowable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, android.app.Activity activity);
+ method public static io.reactivex.rxjava3.core.Observable<androidx.window.layout.WindowLayoutInfo> windowLayoutInfoObservable(androidx.window.layout.WindowInfoTracker, @UiContext android.content.Context context);
}
}
diff --git a/window/window-rxjava3/build.gradle b/window/window-rxjava3/build.gradle
index 6874507..32804c4 100644
--- a/window/window-rxjava3/build.gradle
+++ b/window/window-rxjava3/build.gradle
@@ -36,6 +36,7 @@
api(libs.kotlinCoroutinesRx3)
api(libs.rxjava3)
api(project(":window:window"))
+ implementation 'androidx.annotation:annotation:1.5.0'
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
index 04c68fc..f4a4c4c 100644
--- a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
+++ b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/layout/WindowInfoTrackerRxTest.kt
@@ -17,6 +17,7 @@
package androidx.window.rxjava3.layout
import android.app.Activity
+import android.content.Context
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
@@ -30,10 +31,10 @@
* [io.reactivex.rxjava3.core.Flowable] and ensure that data is forwarded appropriately.
* @see WindowInfoTracker
*/
-public class WindowInfoTrackerRxTest {
+class WindowInfoTrackerRxTest {
@Test
- public fun testWindowLayoutInfoObservable() {
+ fun testWindowLayoutInfoObservable() {
val activity = mock<Activity>()
val feature = mock<FoldingFeature>()
val expected = WindowLayoutInfo(listOf(feature))
@@ -46,7 +47,7 @@
}
@Test
- public fun testWindowLayoutInfoFlowable() {
+ fun testWindowLayoutInfoFlowable() {
val activity = mock<Activity>()
val feature = mock<FoldingFeature>()
val expected = WindowLayoutInfo(listOf(feature))
@@ -57,4 +58,30 @@
testSubscriber.assertValue(expected)
}
+
+ @Test
+ fun testWindowLayoutInfoObservable_context() {
+ val activity = mock<Context>()
+ val feature = mock<FoldingFeature>()
+ val expected = WindowLayoutInfo(listOf(feature))
+ val mockTracker = mock<WindowInfoTracker>()
+ whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+ val testSubscriber = mockTracker.windowLayoutInfoObservable(activity).test()
+
+ testSubscriber.assertValue(expected)
+ }
+
+ @Test
+ fun testWindowLayoutInfoFlowable_context() {
+ val activity = mock<Context>()
+ val feature = mock<FoldingFeature>()
+ val expected = WindowLayoutInfo(listOf(feature))
+ val mockTracker = mock<WindowInfoTracker>()
+ whenever(mockTracker.windowLayoutInfo(activity)).thenReturn(flowOf(expected))
+
+ val testSubscriber = mockTracker.windowLayoutInfoFlowable(activity).test()
+
+ testSubscriber.assertValue(expected)
+ }
}
diff --git a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
index 374bbbb..084292b 100644
--- a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
+++ b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/layout/WindowInfoTrackerRx.kt
@@ -18,6 +18,8 @@
package androidx.window.rxjava3.layout
import android.app.Activity
+import android.content.Context
+import androidx.annotation.UiContext
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import io.reactivex.rxjava3.core.Flowable
@@ -29,7 +31,7 @@
* Return an [Observable] stream of [WindowLayoutInfo].
* @see WindowInfoTracker.windowLayoutInfo
*/
-public fun WindowInfoTracker.windowLayoutInfoObservable(
+fun WindowInfoTracker.windowLayoutInfoObservable(
activity: Activity
): Observable<WindowLayoutInfo> {
return windowLayoutInfo(activity).asObservable()
@@ -39,8 +41,28 @@
* Return a [Flowable] stream of [WindowLayoutInfo].
* @see WindowInfoTracker.windowLayoutInfo
*/
-public fun WindowInfoTracker.windowLayoutInfoFlowable(
+fun WindowInfoTracker.windowLayoutInfoFlowable(
activity: Activity
): Flowable<WindowLayoutInfo> {
return windowLayoutInfo(activity).asFlowable()
}
+
+/**
+ * Return an [Observable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoObservable(
+ @UiContext context: Context
+): Observable<WindowLayoutInfo> {
+ return windowLayoutInfo(context).asObservable()
+}
+
+/**
+ * Return a [Flowable] stream of [WindowLayoutInfo].
+ * @see WindowInfoTracker.windowLayoutInfo
+ */
+fun WindowInfoTracker.windowLayoutInfoFlowable(
+ @UiContext context: Context
+): Flowable<WindowLayoutInfo> {
+ return windowLayoutInfo(context).asFlowable()
+}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 58ca5a5..ee2f53d 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -147,19 +147,15 @@
property public final float ratio;
}
- public interface SplitAttributesCalculator {
- method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
- }
-
- public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public final class SplitAttributesCalculatorParams {
+ method public boolean getAreDefaultConstraintsSatisfied();
method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
+ property public final boolean areDefaultConstraintsSatisfied;
property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
- property public final boolean isDefaultMinSizeSatisfied;
property public final android.content.res.Configuration parentConfiguration;
property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -170,11 +166,10 @@
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
- method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
method public boolean isSplitAttributesCalculatorSupported();
method public boolean isSplitSupported();
method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
- method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+ method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
field public static final androidx.window.embedding.SplitController.Companion Companion;
}
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 81ab30f..8d4cb95 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -186,19 +186,15 @@
property public final float ratio;
}
- public interface SplitAttributesCalculator {
- method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
- }
-
- public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public final class SplitAttributesCalculatorParams {
+ method public boolean getAreDefaultConstraintsSatisfied();
method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
+ property public final boolean areDefaultConstraintsSatisfied;
property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
- property public final boolean isDefaultMinSizeSatisfied;
property public final android.content.res.Configuration parentConfiguration;
property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -209,11 +205,10 @@
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
- method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
method public boolean isSplitAttributesCalculatorSupported();
method public boolean isSplitSupported();
method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
- method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+ method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
field public static final androidx.window.embedding.SplitController.Companion Companion;
}
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 58ca5a5..ee2f53d 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -147,19 +147,15 @@
property public final float ratio;
}
- public interface SplitAttributesCalculator {
- method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
- }
-
- public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+ public final class SplitAttributesCalculatorParams {
+ method public boolean getAreDefaultConstraintsSatisfied();
method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
method public String? getSplitRuleTag();
- method public boolean isDefaultMinSizeSatisfied();
+ property public final boolean areDefaultConstraintsSatisfied;
property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
- property public final boolean isDefaultMinSizeSatisfied;
property public final android.content.res.Configuration parentConfiguration;
property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
@@ -170,11 +166,10 @@
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
- method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
method public boolean isSplitAttributesCalculatorSupported();
method public boolean isSplitSupported();
method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
- method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
+ method public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
field public static final androidx.window.embedding.SplitController.Companion Companion;
}
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index b5e4534..ddbbf0d 100644
--- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -20,18 +20,13 @@
import android.graphics.Color
import androidx.annotation.Sampled
import androidx.window.embedding.SplitAttributes
-import androidx.window.embedding.SplitAttributesCalculator
import androidx.window.embedding.SplitController
import androidx.window.layout.FoldingFeature
@Sampled
fun splitAttributesCalculatorSample() {
SplitController.getInstance(context)
- .setSplitAttributesCalculator(
- object : SplitAttributesCalculator {
- override fun computeSplitAttributesForParams(
- params: SplitAttributesCalculator.SplitAttributesCalculatorParams
- ): SplitAttributes {
+ .setSplitAttributesCalculator { params ->
val tag = params.splitRuleTag
val parentWindowMetrics = params.parentWindowMetrics
val parentConfig = params.parentConfiguration
@@ -39,8 +34,8 @@
.filterIsInstance<FoldingFeature>()
val foldingState = if (foldingFeatures.size == 1) foldingFeatures[0] else null
// Tag can be used to filter the SplitRule to apply the SplitAttributes
- if (TAG_SPLIT_RULE_MAIN != tag && params.isDefaultMinSizeSatisfied) {
- return params.defaultSplitAttributes
+ if (TAG_SPLIT_RULE_MAIN != tag && params.areDefaultConstraintsSatisfied) {
+ return@setSplitAttributesCalculator params.defaultSplitAttributes
}
// This sample will make the app show a layout to
@@ -52,7 +47,7 @@
if (foldingState?.isSeparating == true) {
// Split the parent container that followed by the hinge if the hinge separates the
// parent window.
- return SplitAttributes.Builder()
+ return@setSplitAttributesCalculator SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.splitByHinge())
.setLayoutDirection(
if (foldingState.orientation == FoldingFeature.Orientation.HORIZONTAL) {
@@ -65,7 +60,9 @@
.setAnimationBackgroundColor(Color.GRAY)
.build()
}
- return if (parentConfig.screenWidthDp >= 600 && bounds.width() >= bounds.height()) {
+ return@setSplitAttributesCalculator if (
+ parentConfig.screenWidthDp >= 600 && bounds.width() >= bounds.height()
+ ) {
// Split the parent container equally and vertically if the device is in landscape.
SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.splitEqually())
@@ -79,22 +76,17 @@
.build()
}
}
- })
}
@Sampled
fun splitWithOrientations() {
SplitController.getInstance(context)
- .setSplitAttributesCalculator(
- object : SplitAttributesCalculator {
- override fun computeSplitAttributesForParams(
- params: SplitAttributesCalculator.SplitAttributesCalculatorParams
- ): SplitAttributes {
+ .setSplitAttributesCalculator { params ->
// A sample to split with the dimension that larger than 600 DP. If there's no dimension
// larger than 600 DP, show the presentation to fill the task bounds.
val parentConfiguration = params.parentConfiguration
val builder = SplitAttributes.Builder()
- return if (parentConfiguration.screenWidthDp >= 600) {
+ return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) {
builder
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
// Set the color to use when switching between vertical and horizontal
@@ -113,39 +105,33 @@
.build()
}
}
- })
}
@Sampled
fun expandContainersInPortrait() {
SplitController.getInstance(context)
- .setSplitAttributesCalculator(
- object : SplitAttributesCalculator {
- override fun computeSplitAttributesForParams(
- params: SplitAttributesCalculator.SplitAttributesCalculatorParams
- ): SplitAttributes {
+ .setSplitAttributesCalculator { params ->
// A sample to always fill task bounds when the device is in portrait.
val tag = params.splitRuleTag
val bounds = params.parentWindowMetrics.bounds
val defaultSplitAttributes = params.defaultSplitAttributes
- val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
+ val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied
val expandContainersAttrs = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.expandContainers())
.build()
- if (!isDefaultMinSizeSatisfied) {
- return expandContainersAttrs
+ if (!areDefaultConstraintsSatisfied) {
+ return@setSplitAttributesCalculator expandContainersAttrs
}
// Always expand containers for the splitRule tagged as
// TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT if the device is in portrait
- // even if [isDefaultMinSizeSatisfied] reports true.
+ // even if [areDefaultConstraintsSatisfied] reports true.
if (bounds.height() > bounds.width() && TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT.equals(tag)) {
- return expandContainersAttrs
+ return@setSplitAttributesCalculator expandContainersAttrs
}
// Otherwise, use the default splitAttributes.
- return defaultSplitAttributes
+ return@setSplitAttributesCalculator defaultSplitAttributes
}
- })
}
/** Assume it's a valid [Application]... */
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index b1db6b7..156652f 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -30,9 +30,10 @@
*
* If `true`, the system is permitted to override the app's windowing
* behavior and implement activity embedding split rules, such as displaying
- * activities side by side. A system override informs the app that the
- * activity embedding APIs are disabled so the app will not provide its own
- * activity embedding rules, which would conflict with the system's rules.
+ * activities adjacent to each other. A system override informs the app that
+ * the activity embedding APIs are disabled so the app will not provide its
+ * own activity embedding rules, which would conflict with the system's
+ * rules.
*
* If `false`, the system is not permitted to override the windowing
* behavior of the app. Set the property to `false` if the app provides its
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index a060969..c96432f 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -22,8 +22,7 @@
import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
-import androidx.window.extensions.embedding.SplitAttributesCalculator as OEMSplitAttributesCalculator
-import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
+import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import androidx.window.extensions.embedding.SplitPairRule as OEMSplitPairRule
import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
@@ -43,8 +42,8 @@
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
import androidx.window.embedding.SplitAttributes.SplitType
-import androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams
import androidx.window.extensions.WindowExtensions
+import androidx.window.extensions.core.util.function.Function
import androidx.window.extensions.core.util.function.Predicate
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
@@ -136,14 +135,9 @@
SplitType.ratio(splitRatio.ratio)
fun translateSplitAttributesCalculator(
- calculator: SplitAttributesCalculator
- ): OEMSplitAttributesCalculator =
- OEMSplitAttributesCalculator { oemSplitAttributesCalculatorParams ->
- translateSplitAttributes(
- calculator.computeSplitAttributesForParams(
- translate(oemSplitAttributesCalculatorParams)
- )
- )
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ ): Function<OEMSplitAttributesCalculatorParams, OEMSplitAttributes> = Function { oemParams ->
+ translateSplitAttributes(calculator.invoke(translate(oemParams)))
}
@SuppressLint("NewApi")
@@ -152,18 +146,18 @@
): SplitAttributesCalculatorParams = let {
val taskWindowMetrics = params.parentWindowMetrics
val taskConfiguration = params.parentConfiguration
- val defaultSplitAttributes = params.defaultSplitAttributes
- val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
val windowLayoutInfo = params.parentWindowLayoutInfo
+ val defaultSplitAttributes = params.defaultSplitAttributes
+ val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
val splitRuleTag = params.splitRuleTag
val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
SplitAttributesCalculatorParams(
windowMetrics,
taskConfiguration,
- translate(defaultSplitAttributes),
- isDefaultMinSizeSatisfied,
ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
+ translate(defaultSplitAttributes),
+ areDefaultConstraintsSatisfied,
splitRuleTag,
)
}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index b54fbe9..77ed5ef 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -44,11 +44,11 @@
fun isActivityEmbedded(activity: Activity): Boolean
- fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+ fun setSplitAttributesCalculator(
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ )
fun clearSplitAttributesCalculator()
- fun getSplitAttributesCalculator(): SplitAttributesCalculator?
-
fun isSplitAttributesCalculatorSupported(): Boolean
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index 46ef801..0109ecf 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -83,7 +83,9 @@
return embeddingExtension.isActivityEmbedded(activity)
}
- override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+ override fun setSplitAttributesCalculator(
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ ) {
if (!isSplitAttributesCalculatorSupported()) {
throw UnsupportedOperationException("#setSplitAttributesCalculator is not supported " +
"on the device.")
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index 2317536..8c74aec 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -35,7 +35,9 @@
fun isActivityEmbedded(activity: Activity): Boolean
- fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+ fun setSplitAttributesCalculator(
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ )
fun clearSplitAttributesCalculator()
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
index 0f44fcc..987360f 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
@@ -25,8 +25,8 @@
* A unique string to identify this [EmbeddingRule], which defaults to `null`.
* The suggested usage is to set the tag in the corresponding rule builder to be able to
* differentiate between different rules in the callbacks. For example, it can be used to
- * compute the right [SplitAttributes] for the right split rule in
- * [SplitAttributesCalculator.computeSplitAttributesForParams].
+ * compute the right [SplitAttributes] for the right split rule in callback set in
+ * [SplitController.setSplitAttributesCalculator].
*
* @see androidx.window.embedding.RuleController.addRule
*/
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index dc1d5a4..666b488 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -41,7 +41,6 @@
@VisibleForTesting
val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
- private var splitAttributesCalculator: SplitAttributesCalculator? = null
init {
splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
@@ -304,23 +303,20 @@
return embeddingExtension?.isActivityEmbedded(activity) ?: false
}
- override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+ override fun setSplitAttributesCalculator(
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ ) {
globalLock.withLock {
- splitAttributesCalculator = calculator
embeddingExtension?.setSplitAttributesCalculator(calculator)
}
}
override fun clearSplitAttributesCalculator() {
globalLock.withLock {
- splitAttributesCalculator = null
embeddingExtension?.clearSplitAttributesCalculator()
}
}
- override fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
- globalLock.withLock { splitAttributesCalculator }
-
override fun isSplitAttributesCalculatorSupported(): Boolean =
embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index a05dd47..d83ce52 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -218,10 +218,10 @@
* containers each expand to fill the parent window; the secondary
* container overlays the primary container.
*
- * Use this method with [SplitAttributesCalculator] to expand the
- * activity containers in some device states. The following sample
- * shows how to always fill the parent bounds if the device is in
- * portrait orientation:
+ * Use this method with the function set in
+ * [SplitController.setSplitAttributesCalculator] to expand the activity containers in
+ * some device states. The following sample shows how to always fill the parent bounds
+ * if the device is in portrait orientation:
*
* @sample androidx.window.samples.embedding.expandContainersInPortrait
*
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
deleted file mode 100644
index 77720a8..0000000
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
+++ /dev/null
@@ -1,113 +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.window.embedding
-
-import android.content.res.Configuration
-import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetrics
-
-/**
- * A developer-defined [SplitAttributes] calculator to compute the current [SplitAttributes] with
- * the current device and window state if it is registered via
- * [SplitController.setSplitAttributesCalculator]. Then
- * [computeSplitAttributesForParams] will be called when there's
- * - An activity is started and matches a registered [SplitRule].
- * - There's a parent configuration update and there's an existing split pair.
- *
- * By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
- * [WindowMetrics] satisfies the [SplitRule]'s minimum dimensions requirements, which are
- * [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
- * The [SplitRule.defaultSplitAttributes] can be set by
- * - [SplitRule] Builder APIs, which are
- * [SplitPairRule.Builder.setDefaultSplitAttributes] and
- * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
- * - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
- * `<SplitPlaceHolderRule>` tags in XML files.
- *
- * However, developers may want to apply different [SplitAttributes] for different device or window
- * states. For example, on foldable devices, developers may want to split the screen vertically if
- * the device is in landscape, fill the screen if the device is in portrait and split the screen
- * horizontally if the device is in
- * [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
- * In this case, the [SplitAttributes] can be customized by this callback, which takes effects after
- * calling [SplitController.setSplitAttributesCalculator]. Developers can also
- * clear the callback by [SplitController.clearSplitAttributesCalculator].
- * Then, developers could implement [computeSplitAttributesForParams] as the sample linked below
- * shows.
- *
- * **Note** that [SplitController.setSplitAttributesCalculator] and
- * [SplitController.clearSplitAttributesCalculator] are only supported if
- * [SplitController.isSplitAttributesCalculatorSupported] reports `true`. It's
- * callers' responsibility to check if [SplitAttributesCalculator] is supported by
- * [SplitController.isSplitAttributesCalculatorSupported] before using the
- * [SplitAttributesCalculator] feature. It is suggested to always set meaningful
- * [SplitRule.defaultSplitAttributes] in case [SplitAttributesCalculator] is not supported on
- * some devices.
- *
- * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
- *
- * @see androidx.window.embedding.SplitRule.defaultSplitAttributes
- * @see androidx.window.embedding.SplitController.getSplitAttributesCalculator
- */
-interface SplitAttributesCalculator {
- /**
- * Computes the [SplitAttributes] with the current device and window states.
- * @param params See [SplitAttributesCalculatorParams]
- */
- fun computeSplitAttributesForParams(params: SplitAttributesCalculatorParams): SplitAttributes
-
- /** The container of [SplitAttributesCalculator] parameters */
- class SplitAttributesCalculatorParams internal constructor(
- /** The parent container's [WindowMetrics] */
- val parentWindowMetrics: WindowMetrics,
- /** The parent container's [Configuration] */
- val parentConfiguration: Configuration,
- /**
- * The [SplitRule.defaultSplitAttributes]. It could be from [SplitRule] Builder APIs
- * ([SplitPairRule.Builder.setDefaultSplitAttributes] or
- * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes]) or from the `splitRatio` and
- * `splitLayoutDirection` attributes from static rule definitions.
- */
- val defaultSplitAttributes: SplitAttributes,
- /**
- * Whether the [parentWindowMetrics] are larger than [SplitRule]'s minimum size criteria,
- * which are [SplitRule.minWidthDp], [SplitRule.minHeightDp] and
- * [SplitRule.minSmallestWidthDp]
- */
- @get: JvmName("isDefaultMinSizeSatisfied")
- val isDefaultMinSizeSatisfied: Boolean,
- /** The parent container's [WindowLayoutInfo] */
- val parentWindowLayoutInfo: WindowLayoutInfo,
- /**
- * The [tag of `SplitRule`][SplitRule.tag] to apply this [SplitAttributes], which is `null`
- * if the tag is not set.
- *
- * @see SplitPairRule.Builder.setTag
- * @see SplitPlaceholderRule.Builder.setTag
- */
- val splitRuleTag: String?,
- ) {
- override fun toString(): String =
- "${SplitAttributesCalculatorParams::class.java.simpleName}:{" +
- "windowMetrics=$parentWindowMetrics" +
- ", configuration=$parentConfiguration" +
- ", windowLayoutInfo=$parentWindowLayoutInfo" +
- ", defaultSplitAttributes=$defaultSplitAttributes" +
- ", isDefaultMinSizeSatisfied=$isDefaultMinSizeSatisfied" +
- ", tag=$splitRuleTag}"
- }
-}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
new file mode 100644
index 0000000..c099362
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.embedding
+
+import android.content.res.Configuration
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * The parameter container used to report the current device and window state in
+ * [SplitController.setSplitAttributesCalculator] and references the corresponding [SplitRule] by
+ * [splitRuleTag] if [SplitPairRule.tag] is specified.
+ */
+class SplitAttributesCalculatorParams internal constructor(
+ /** The parent container's [WindowMetrics] */
+ val parentWindowMetrics: WindowMetrics,
+ /** The parent container's [Configuration] */
+ val parentConfiguration: Configuration,
+ /** The parent container's [WindowLayoutInfo] */
+ val parentWindowLayoutInfo: WindowLayoutInfo,
+ /**
+ * The [SplitRule.defaultSplitAttributes]. It could be from [SplitRule] Builder APIs
+ * ([SplitPairRule.Builder.setDefaultSplitAttributes] or
+ * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes]) or from the `splitRatio` and
+ * `splitLayoutDirection` attributes from static rule definitions.
+ */
+ val defaultSplitAttributes: SplitAttributes,
+ /**
+ * Whether the [parentWindowMetrics] satisfies the dimensions and aspect
+ * ratios requirements specified in the [SplitRule], which are:
+ * - [SplitRule.minWidthDp]
+ * - [SplitRule.minHeightDp]
+ * - [SplitRule.minSmallestWidthDp]
+ * - [SplitRule.maxAspectRatioInPortrait]
+ * - [SplitRule.maxAspectRatioInLandscape]
+ */
+ @get: JvmName("areDefaultConstraintsSatisfied")
+ val areDefaultConstraintsSatisfied: Boolean,
+ /**
+ * The [tag of `SplitRule`][SplitRule.tag] to apply this [SplitAttributes], which is `null`
+ * if the tag is not set.
+ *
+ * @see SplitPairRule.Builder.setTag
+ * @see SplitPlaceholderRule.Builder.setTag
+ */
+ val splitRuleTag: String?,
+) {
+ override fun toString(): String =
+ "${SplitAttributesCalculatorParams::class.java.simpleName}:{" +
+ "windowMetrics=$parentWindowMetrics" +
+ ", configuration=$parentConfiguration" +
+ ", windowLayoutInfo=$parentWindowLayoutInfo" +
+ ", defaultSplitAttributes=$defaultSplitAttributes" +
+ ", areDefaultConstraintsSatisfied=$areDefaultConstraintsSatisfied" +
+ ", tag=$splitRuleTag}"
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index efb4f9f..221d0d1 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -26,6 +26,7 @@
import androidx.core.util.Consumer
import androidx.window.WindowProperties
import androidx.window.embedding.SplitController.Api31Impl.isSplitPropertyEnabled
+import androidx.window.layout.WindowMetrics
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -112,26 +113,59 @@
}
/**
- * Sets or updates the previously registered [SplitAttributesCalculator].
+ * Sets or replaces the previously registered [SplitAttributes] calculator.
*
- * **Note** that if the [SplitAttributesCalculator] is replaced, the existing split pairs will
- * be updated after there's a window or device state change.
- * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
+ * **Note** that it's callers' responsibility to check if this API is supported by calling
+ * [isSplitAttributesCalculatorSupported] before using the this API. It is suggested to always
+ * set meaningful [SplitRule.defaultSplitAttributes] in case this API is not supported on some
+ * devices.
*
- * @param calculator the calculator to set. It will replace the previously set
- * [SplitAttributesCalculator] if it exists.
+ * Also, replacing the calculator will only update existing split pairs after a change
+ * in the window or device state, such as orientation changes or folding state changes.
+ *
+ * The [SplitAttributes] calculator is a function to compute the current [SplitAttributes] for
+ * the given [SplitRule] with the current device and window state. Then The calculator will be
+ * invoked if either:
+ * - An activity is started and matches a registered [SplitRule].
+ * - A parent configuration is updated and there's an existing split pair.
+ *
+ * By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
+ * [WindowMetrics] satisfies the [SplitRule]'s dimensions requirements, which are
+ * [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
+ * The [SplitRule.defaultSplitAttributes] can be set by
+ * - [SplitRule] Builder APIs, which are
+ * [SplitPairRule.Builder.setDefaultSplitAttributes] and
+ * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
+ * - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
+ * `<SplitPlaceHolderRule>` tags in XML files.
+ *
+ * Developers may want to apply different [SplitAttributes] for different device or window
+ * states. For example, on foldable devices, developers may want to split the screen vertically
+ * if the device is in landscape, fill the screen if the device is in portrait and split
+ * the screen horizontally if the device is in
+ * [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
+ * In this case, the [SplitAttributes] can be customized by the [SplitAttributes] calculator,
+ * which takes effects after calling this API. Developers can also clear the calculator
+ * by [clearSplitAttributesCalculator].
+ * Then, developers could implement the [SplitAttributes] calculator as the sample linked below
+ * shows.
+ *
+ * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
+ * @param calculator the function to calculate [SplitAttributes] based on the
+ * [SplitAttributesCalculatorParams]. It will replace the previously set if it exists.
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
- fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+ fun setSplitAttributesCalculator(
+ calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
+ ) {
embeddingBackend.setSplitAttributesCalculator(calculator)
}
/**
- * Clears the previously set [SplitAttributesCalculator].
+ * Clears the callback previously set by [setSplitAttributesCalculator].
* The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
*
- * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
@@ -139,11 +173,7 @@
embeddingBackend.clearSplitAttributesCalculator()
}
- /** Returns the current set [SplitAttributesCalculator]. */
- fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
- embeddingBackend.getSplitAttributesCalculator()
-
- /** Returns whether [SplitAttributesCalculator] is supported or not. */
+ /** Returns whether [setSplitAttributesCalculator] is supported or not. */
fun isSplitAttributesCalculatorSupported(): Boolean =
embeddingBackend.isSplitAttributesCalculatorSupported()
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index 38e0927..ad966ab 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -21,8 +21,8 @@
import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
/**
- * Split configuration rules for activity pairs. Define when activities that were launched on top of
- * each other should be shown side-by-side, and the visual properties of such splits. Can be set
+ * Split configuration rules for activity pairs. Define when activities that were launched on top
+ * should be placed adjacent to the one below, and the visual properties of such splits. Can be set
* either by [RuleController.setRules] or [RuleController.addRule]. The rules are always
* applied only to activities that will be started after the rules were set.
*/
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index 05428b6..7cdfefe 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -41,15 +41,13 @@
* started after the rules were set.
*
* Note that regardless of whether the minimal requirements ([minWidthDp], [minHeightDp] and
- * [minSmallestWidthDp]) are met or not, [SplitAttributesCalculator.computeSplitAttributesForParams]
- * will still be called for the rule if the calculator is registered via
- * [SplitController.setSplitAttributesCalculator]. Whether this [SplitRule]'s
- * minimum requirements are satisfied is dispatched in
- * [SplitAttributesCalculator.SplitAttributesCalculatorParams.isDefaultMinSizeSatisfied] instead.
- * The width and height could be verified in
- * [SplitAttributesCalculator.computeSplitAttributesForParams] as the sample linked below shows.
- * It is useful if this rule is supported to split the parent container in different directions
- * with different device states.
+ * [minSmallestWidthDp]) are met or not, the callback set in
+ * [SplitController.setSplitAttributesCalculator] will still be called for the rule if the
+ * calculator is registered via [SplitController.setSplitAttributesCalculator].
+ * Whether this [SplitRule]'s minimum requirements are satisfied is dispatched in
+ * [SplitAttributesCalculatorParams.areDefaultConstraintsSatisfied] instead.
+ * The width and height could be verified in the [SplitAttributes] calculator callback
+ * as the sample linked below shows.
*
* It is useful if this [SplitRule] is supported to split the parent container in different
* directions with different device states.
@@ -65,8 +63,8 @@
* When the window size is smaller than requested here, activities in the secondary container
* will be stacked on top of the activities in the primary one, completely overlapping them.
*
- * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
- * `0` means to always allow split.
+ * Uses `0` to always allow split regardless of the parent task width.
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
*/
@IntRange(from = 0)
val minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
@@ -77,8 +75,8 @@
* will be stacked on top of the activities in the primary one, completely overlapping them.
* It is useful if it's necessary to split the parent window horizontally for this [SplitRule].
*
- * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
- * `0` means to always allow split.
+ * Uses `0` to always allow split regardless of the parent task height.
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
*
* @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
* @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
@@ -92,8 +90,8 @@
* activities in the secondary container will be stacked on top of the activities in the primary
* one, completely overlapping them.
*
- * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
- * `0` means to always allow split.
+ * Uses `0` to always allow split regardless of the parent task smallest width.
+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT].
*/
@IntRange(from = 0)
val minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
@@ -106,8 +104,8 @@
*
* This value is only used when the parent window is in portrait (height >= width).
*
- * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is the
- * recommend value to only allow split when the parent window is not too stretched in portrait.
+ * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT], which is the recommend value to
+ * only allow split when the parent window is not too stretched in portrait.
*
* @see EmbeddingAspectRatio.ratio
* @see EmbeddingAspectRatio.alwaysAllow
@@ -123,8 +121,8 @@
*
* This value is only used when the parent window is in landscape (width > height).
*
- * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which is
- * the recommend value to always allow split when the parent window is in landscape.
+ * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT], which is the recommend value to
+ * always allow split when the parent window is in landscape.
*
* @see EmbeddingAspectRatio.ratio
* @see EmbeddingAspectRatio.alwaysAllow
@@ -183,7 +181,7 @@
*
* For example, given that [SplitPairRule.finishPrimaryWithSecondary] is [ADJACENT] and
* secondary container finishes. The primary associated container is finished if it's
- * side-by-side with secondary container. The primary associated container is not finished
+ * adjacent to the secondary container. The primary associated container is not finished
* if it occupies entire task bounds.
*
* @see SplitPairRule.finishPrimaryWithSecondary
@@ -208,9 +206,9 @@
@JvmField
val ALWAYS = FinishBehavior("ALWAYS", 1)
/**
- * Only finish the associated container when displayed side-by-side/adjacent to the one
- * being finished. Does not finish the associated one when containers are stacked on top
- * of each other.
+ * Only finish the associated container when displayed adjacent to the one being
+ * finished. Does not finish the associated one when containers are stacked on top of
+ * each other.
*/
@JvmField
val ADJACENT = FinishBehavior("ADJACENT", 2)
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index df33e9a..d4af572 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -15,96 +15,125 @@
limitations under the License.
-->
<resources xmlns:xs="http://schemas.android.com/apk/res/android">
- <!-- Defines what activity container should be given to the primary part of the task
- bounds. Values in range (0.0, 1.0) define the size of the primary container of the split
- relative to the corresponding task dimension size. 0.0 means the secondary activity container
- fills the full Task bounds, and occludes the primary activity container, which is also expanded
- to fill the full Task bounds. 0.5 means the primary and secondary container shares an equal
- split. Ratio larger than `0.5` means the primary container takes more partition. Otherwise,
- the secondary container takes more partition. -->
+ <!-- Defines what part of the Task bounds should be given to the primary container in split.
+ Values in range `(0.0, 1.0)` define the size of the primary container of the split relative
+ to the corresponding parent Task bounds.
+ The default is `0.5`, which is to split with equal width. -->
<attr name="splitRatio" format="float" />
- <!-- The smallest value of width of the parent window when the split should be used. -->
+ <!-- The smallest value of width of the parent Task bounds when the split should be used, in DP.
+ Uses `0` to always allow split regardless of the parent Task width.
+ The default is `600`. -->
<attr name="splitMinWidthDp" format="integer" />
- <!-- The smallest value of height of the parent window when the split should be used. -->
+ <!-- The smallest value of height of the parent Task bounds when the split should be used, in
+ DP.
+ Uses `0` to always allow split regardless of the parent Task height.
+ The default is `600`. -->
<attr name="splitMinHeightDp" format="integer" />
- <!-- The smallest value of the smallest-width (sw) of the parent window in any rotation when
- the split should be used. -->
+ <!-- The smallest value of the smallest possible width of the parent Task bounds in any rotation
+ when the split should be used, in DP.
+ Uses `0` to always allow split regardless of the parent Task smallest width.
+ The default is `600`. -->
<attr name="splitMinSmallestWidthDp" format="integer" />
<!-- The largest value of the aspect ratio, expressed as (height / width) in decimal form, of
- the parent window bounds in portrait when the split should be used.
- `0` or `alwaysAllow` means to always allow split in portrait.
- `-1` or `alwaysDisallow` means to always disallow split in portrait.
- Any other values less than 1 are invalid. -->
+ the parent Task bounds in portrait when the split should be used.
+ Uses `0` or `alwaysAllow` to always allow split in portrait.
+ Uses `-1` or `alwaysDisallow` to always disallow split in portrait.
+ Any other values less than `1` are invalid.
+ The default is `1.4`. -->
<attr name="splitMaxAspectRatioInPortrait" format="float">
- <!-- Special value to always allow split in portrait. -->
+ <!-- Special value to always allow split in portrait regardless of the aspect ratio. -->
<enum name="alwaysAllow" value="0" />
- <!-- Special value to always disallow split in portrait. -->
+ <!-- Special value to always disallow split in portrait regardless of the aspect ratio. -->
<enum name="alwaysDisallow" value="-1" />
</attr>
<!-- The largest value of the aspect ratio, expressed as (width / height) in decimal form, of
- the parent window bounds in landscape when the split should be used.
- `0` or `alwaysAllow` means to always allow split in landscape.
- `-1` or `alwaysDisallow` means to always disallow split in landscape.
- Any other values less than 1 are invalid. -->
+ the parent Task bounds in landscape when the split should be used.
+ Uses `0` or `alwaysAllow` to always allow split in landscape.
+ Uses `-1` or `alwaysDisallow` to always disallow split in landscape.
+ Any other values less than `1` are invalid.
+ The default is `alwaysAllow`. -->
<attr name="splitMaxAspectRatioInLandscape" format="float">
- <!-- Special value to always allow split in landscape. -->
+ <!-- Special value to always allow split in landscape regardless of the aspect ratio. -->
<enum name="alwaysAllow" value="0" />
- <!-- Special value to always disallow split in landscape. -->
+ <!-- Special value to always disallow split in landscape regardless of the aspect ratio. -->
<enum name="alwaysDisallow" value="-1" />
</attr>
+ <!-- The layout direction for the split. -->
<attr name="splitLayoutDirection" format="enum">
<!-- A layout direction that splits the task bounds vertically, and the direction is deduced
- from the language script of locale. The direction can be either rtl or ltr -->
+ from the language script of locale. The direction can be either `rtl` or `ltr` -->
<enum name="locale" value="0" />
<!-- The primary container is placed on the left, and the secondary is on the right hand
- side. -->
+ side. -->
<enum name="ltr" value="1" />
<!-- The primary container is placed on the right, and the secondary is on the left hand
- side. -->
+ side. -->
<enum name="rtl" value="2" />
<!-- The primary container is placed on top, and the secondary is at the bottom. -->
<enum name="topToBottom" value="3" />
<!-- The primary container is placed on bottom, and the secondary is at the top. -->
<enum name="bottomToTop" value="4" />
</attr>
+ <!-- Determines what happens with the primary container when all activities are finished in
+ the associated secondary container.
+ The default is `never`. -->
<attr name="finishPrimaryWithSecondary" format="enum">
+ <!-- Never finish the primary container. -->
<enum name="never" value="0" />
+ <!-- Always finish the primary container. -->
<enum name="always" value="1" />
+ <!-- Only finish the primary container when the primary container is displayed adjacent to
+ the secondary container.
+ Does not finish the primary one when the secondary container is stacked on top of
+ the primary. -->
<enum name="adjacent" value="2" />
</attr>
- <attr name="finishPrimaryWithPlaceholder" format="enum">
- <enum name="always" value="1" />
- <enum name="adjacent" value="2" />
- </attr>
+ <!-- Determines what happens with the secondary container when all activities are finished
+ in the associated primary container.
+ The default is `always`. -->
<attr name="finishSecondaryWithPrimary" format="enum">
+ <!-- Never finish the secondary container. -->
<enum name="never" value="0" />
+ <!-- Always finish the secondary container. -->
<enum name="always" value="1" />
+ <!-- Only finish the secondary container when the primary container is displayed adjacent to
+ the secondary container.
+ Does not finish the secondary one when the secondary container is stacked on top of
+ the primary. -->
<enum name="adjacent" value="2" />
</attr>
- <!-- An optional but unique string to identify a SplitPairRule, SplitPlaceholderRule or
- ActivityRule. The suggested usage is to set the tag to be able to differentiate between
- different rules in the callbacks. For example, it can be used to compute the right
- SplitAttributes for the right split rule in
- SplitAttributesCalculator#computeSplitAttributesForState.-->
+ <!-- Determines what happens with the primary container when the associated placeholder
+ activity is being finished.
+ The default is `always`. -->
+ <attr name="finishPrimaryWithPlaceholder" format="enum">
+ <!-- Always finish the primary container. -->
+ <enum name="always" value="1" />
+ <!-- Only finish the primary container when the primary container is displayed adjacent to
+ the placeholder container.
+ Does not finish the primary one when the placeholder container is stacked on top of
+ the primary. -->
+ <enum name="adjacent" value="2" />
+ </attr>
+ <!-- An optional but unique string to identify a `SplitPairRule`, `SplitPlaceholderRule` or
+ `ActivityRule`. The suggested usage is to set the tag to be able to differentiate between
+ different rules in the callbacks.
+ For example, it can be used to compute the right `SplitAttributes` for the given split rule
+ in `SplitAttributesCalculator.computeSplitAttributesForParams`.-->
<attr name="tag" format="string" />
- <!-- Background color of animation if the animation requires a background. Defaults to the
- theme's windowBackground. -->
+ <!-- Background color of animation if the animation requires a background.
+ The default is to use the theme's windowBackground. -->
<attr name="animationBackgroundColor" format="color" />
- <!-- Split configuration rules for activity pairs. Must contain at least one SplitPairFilter.
- See androidx.window.embedding.SplitPairRule for complete documentation. -->
+ <!-- Split configuration rules for activity pairs. Must contain at least one `SplitPairFilter`.
+ -->
<declare-styleable name="SplitPairRule">
- <!-- When all activities are finished in the secondary container, the activity in the
- primary container that created the split should also be finished. Defaults to "never". -->
- <attr name="finishPrimaryWithSecondary" />
- <!-- When all activities are finished in the primary container, the activities in the
- secondary container in the split should also be finished. Defaults to "always". -->
- <attr name="finishSecondaryWithPrimary" />
<!-- If there is an existing split with the same primary container, indicates whether the
- existing secondary container on top and all activities in it should be destroyed when a new
- split is created using this rule. Otherwise the new secondary will appear on top. Defaults
- to "false". -->
+ existing secondary container on top and all activities in it should be destroyed when a
+ new split is created using this rule. Otherwise the new secondary will appear on top.
+ The default is 'false'. -->
<attr name="clearTop" format="boolean" />
+ <attr name="finishPrimaryWithSecondary" />
+ <attr name="finishSecondaryWithPrimary" />
<attr name="splitRatio"/>
<attr name="splitMinWidthDp"/>
<attr name="splitMinHeightDp"/>
@@ -116,17 +145,16 @@
<attr name="animationBackgroundColor"/>
</declare-styleable>
- <!-- Configuration rules for split placeholders. Must contain at least one ActivityFilter for
- the primary activity for which the rule should be applied.
- See androidx.window.embedding.SplitPlaceholderRule for complete documentation. -->
+ <!-- Configuration rules for split placeholders. Must contain at least one `ActivityFilter` for
+ the primary activity for which the rule should be applied. -->
<declare-styleable name="SplitPlaceholderRule">
- <!-- Component name of the placeholder activity in the split. Must be non-empty. -->
+ <!-- Component name of the placeholder activity to launch in the split.
+ Must be non-empty. -->
<attr name="placeholderActivityName" format="string" />
- <!-- Determines whether the placeholder will show on top in a smaller window size after
- it first appeared in a split with sufficient minimum width. -->
+ <!-- Determines whether the placeholder will show on top if Task window constraints are not
+ satisfied after it first appeared in a split with sufficient Task window constraints.
+ The default is `false`. -->
<attr name="stickyPlaceholder" format="boolean" />
- <!-- When all activities are finished in the secondary container, the activity in the
- primary container that created the split should also be finished. Defaults to "always". -->
<attr name="finishPrimaryWithPlaceholder"/>
<attr name="splitRatio"/>
<attr name="splitMinWidthDp"/>
@@ -139,35 +167,38 @@
<attr name="animationBackgroundColor"/>
</declare-styleable>
- <!-- Filter used to find if a pair of activities should be put in a split. -->
- <declare-styleable name="SplitPairFilter">
- <!-- Component name of the primary activity in the split. Must be non-empty. Can contain a
- wildcard at the end or instead of package name and/or class name. -->
- <attr name="primaryActivityName" format="string" />
- <!-- Component name of the secondary activity in the split. Must be non-empty. Can contain a
- wildcard at the end or instead of package name and/or class name. -->
- <attr name="secondaryActivityName" format="string" />
- <!-- Action used for secondary activity launch. May be empty. Must not contain wildcards.
- -->
- <attr name="secondaryActivityAction" format="string" />
- </declare-styleable>
-
<!-- Layout configuration rules for individual activities. Takes precedence over
- SplitPairRule. Must contain at least one ActivityFilter.
- See androidx.window.layout.ActivityRule for complete documentation. -->
+ `SplitPairRule`. Must contain at least one `ActivityFilter`. -->
<declare-styleable name="ActivityRule">
- <!-- Whether the activity should always be expanded on launch. -->
+ <!-- Whether the activity should always be expanded on launch.
+ The default is `false`. -->
<attr name="alwaysExpand" format="boolean" />
<attr name="tag"/>
</declare-styleable>
- <!-- Filter for ActivityRule. -->
- <declare-styleable name="ActivityFilter">
+ <!-- Filter for `SplitPairRule` to find if a pair of activities should be put in a split. -->
+ <declare-styleable name="SplitPairFilter">
<!-- Component name of the primary activity in the split. Must be non-empty. Can contain a
- single wildcard at the end. -->
+ wildcard at the end or instead of package name and/or class name. -->
+ <attr name="primaryActivityName" format="string" />
+ <!-- Component name of the secondary activity in the split. Must be non-empty. Can contain a
+ wildcard at the end or instead of package name and/or class name. -->
+ <attr name="secondaryActivityName" format="string" />
+ <!-- Action used for the secondary activity launch. May be empty. Must not contain
+ wildcards.
+ When it is set, the filter only matches if the secondary activity is launched with an
+ intent with the given action. -->
+ <attr name="secondaryActivityAction" format="string" />
+ </declare-styleable>
+
+ <!-- Filter for `ActivityRule` and `SplitPlaceholderRule`. -->
+ <declare-styleable name="ActivityFilter">
+ <!-- Component name of the launched activity. Must be non-empty. Can contain a single
+ wildcard at the end. -->
<attr name="activityName" format="string" />
- <!-- Action used for activity launch. May be empty. Must not contain wildcards.
- -->
+ <!-- Action used for the activity launch. May be empty. Must not contain wildcards.
+ When it is set, the filter only matches if the activity is launched with an intent with
+ the given action. -->
<attr name="activityAction" format="string" />
</declare-styleable>
</resources>
\ No newline at end of file