Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8286e2f
Abstract out Anchor definition
andrewbailey Jan 9, 2026
659fef5
Abstract out GroupSourceInformation
andrewbailey Jan 9, 2026
18d0daf
Align RuntimeUtils with HitTestResult Null-Safety & Not-Hit Logic
Dec 31, 2025
754453b
Output the frame number during lock3AForCapture
codelogic Jan 16, 2026
fe89e88
Remove old project creator script
Jan 16, 2026
3e2fdaf
Bump versions for ProtoLayout, Tiles, Glance Wear and RemoteCompose f…
Jan 16, 2026
3cc0dbe
Reenable non-jvm tests for ksp runner
juliamcclellan Jan 16, 2026
4f65f94
Merge "Abstract out Anchor definition" into androidx-main
Jan 16, 2026
ffce909
Merge "Reenable non-jvm tests for ksp runner" into androidx-main
Jan 16, 2026
4c8c86c
Update FrameState to use ListenerState to track the status of callbacks
Jan 15, 2026
b57551b
Merge "Bump versions for ProtoLayout, Tiles, Glance Wear and RemoteCo…
Jan 16, 2026
a2cc0e8
Merge "Abstract out GroupSourceInformation" into androidx-main
Jan 16, 2026
4798011
Merge "Update FrameState to use ListenerState to track the status of …
Jan 16, 2026
8ddcb75
Merge "Remove old project creator script" into androidx-main
Jan 16, 2026
b2e20ca
Merge "Align RuntimeUtils with HitTestResult Null-Safety & Not-Hit Lo…
Jan 16, 2026
a5c782d
Add MediaBlendingMode to ImpressApi
YsvGoog Jan 16, 2026
f76a6eb
Merge "Output the frame number during lock3AForCapture" into androidx…
Jan 17, 2026
c3285a6
Add a single-stream captureWith function for FrameGraph
codelogic Jan 16, 2026
5bfd077
Merge "Add MediaBlendingMode to ImpressApi" into androidx-main
YsvGoog Jan 17, 2026
9d5f789
Merge "Add a single-stream captureWith function for FrameGraph" into …
Jan 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/ksp-snapshot-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
# Gradle flags match those used in presubmit.yml, plus:
# * disabling validating integration patches as a patch file may already be applied
# * disabling klibs cross compilation because it is not supported with cinterops
# * disabling non-Jvm tests to avoid memory exhaustion
# * setting max workers to 2 to avoid memory exhaustion
gradle-flags: >
--max-workers=2
-Pkotlin.native.enableKlibsCrossCompilation=false
Expand All @@ -81,8 +81,5 @@ jobs:
--stacktrace
-x validateIntegrationPatches
-x checkKotlinApiTarget
-x linuxX64Test
-x wasmJsBrowserTest
-x jsBrowserTest
# Disable the cache since this is the only build using the latest KSP.
gradle-cache-disabled: true
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public interface FrameGraph : CameraGraphBase<FrameGraph.Session>, CameraControl
public fun captureWith(
streamIds: Set<StreamId> = emptySet(),
parameters: Map<Any, Any?> = emptyMap(),
capacity: Int = 1,
capacity: Int = DEFAULT_FRAME_BUFFER_CAPACITY,
): FrameBuffer

/**
Expand All @@ -74,4 +74,16 @@ public interface FrameGraph : CameraGraphBase<FrameGraph.Session>, CameraControl
* Example: A [Session] should *not* be held during video recording.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public interface Session : CameraGraph.Session

public companion object {
private const val DEFAULT_FRAME_BUFFER_CAPACITY = 1

/** Utility function for the common case of attaching a single stream. See [captureWith]. */
@JvmStatic
public fun FrameGraph.captureWith(
streamId: StreamId,
parameters: Map<Any, Any?> = emptyMap(),
capacity: Int = DEFAULT_FRAME_BUFFER_CAPACITY,
): FrameBuffer = captureWith(setOf(streamId), parameters, capacity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import androidx.annotation.RestrictTo
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmInline
public value class FrameNumber(public val value: Long)
public value class FrameNumber(public val value: Long) {
override fun toString(): String = "Frame-$value"
}

/** [FrameInfo] is a wrapper around [TotalCaptureResult]. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,10 @@ internal class Controller3A(
}

debug {
"lock3AForCapture result: meetsAeCondition = $meetsAeCondition" +
", meetsAfCondition = $meetsAfCondition, meetsAwbCondition = $meetsAwbCondition"
"lock3AForCapture state ${frameMetadata.frameNumber}: " +
"meetsAeCondition = $meetsAeCondition, " +
"meetsAfCondition = $meetsAfCondition, " +
"meetsAwbCondition = $meetsAwbCondition"
}

meetsAeCondition && meetsAfCondition && meetsAwbCondition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,28 @@ internal class FrameState(

private val state = atomic(STARTED)
private val streamResultCount = atomic(0)
private val outputFrameListeners = CopyOnWriteArrayList<Frame.Listener>()
// A list of ListenerState, one for each listener.
private val listenerStates = CopyOnWriteArrayList<ListenerState>()

fun addListener(listener: Frame.Listener) {
listener.onFrameStarted(frameNumber, frameTimestamp)

// Note: This operation is safe since the outputFrameListeners is a CopyOnWriteArrayList.
outputFrameListeners.add(listener)
val listenerState = ListenerState(listener)
listenerStates.add(listenerState)

val currentFrameState = state.value

// Listeners can be added during any Frame state. We want to trigger the callbacks that were
// already triggered before the listener is added.
when (currentFrameState) {
STARTED -> listenerState.invokeOnStarted(frameNumber, frameTimestamp)
FRAME_INFO_COMPLETE ->
listenerState.invokeOnFrameInfoAvailable(frameNumber, frameTimestamp)
STREAM_RESULTS_COMPLETE ->
listenerState.invokeOnImagesAvailable(frameNumber, frameTimestamp)
COMPLETE -> listenerState.invokeOnFrameComplete(frameNumber, frameTimestamp)
}
}

fun onFrameInfoComplete() {
// Invoke the onOutputResultsAvailable onOutputMetadataAvailable.
for (i in outputFrameListeners.indices) {
outputFrameListeners[i].onFrameInfoAvailable()
}

val state =
state.updateAndGet { current ->
when (current) {
Expand All @@ -105,25 +112,23 @@ internal class FrameState(
}
}

for (listenerState in listenerStates) {
listenerState.invokeOnFrameInfoAvailable(frameNumber, frameTimestamp)
}

if (state == COMPLETE) {
invokeOnFrameComplete()
}
}

fun onStreamResultComplete(streamId: StreamId) {
val allResultsCompleted = streamResultCount.incrementAndGet() != imageOutputs.size
val hasResultsRemaining = streamResultCount.incrementAndGet() != imageOutputs.size

// Invoke the onOutputResultsAvailable listener.
for (i in outputFrameListeners.indices) {
outputFrameListeners[i].onImageAvailable(streamId)
for (listenerState in listenerStates) {
listenerState.invokeOnImageAvailable(streamId)
}

if (allResultsCompleted) return

// Invoke the onOutputResultsAvailable listener.
for (i in outputFrameListeners.indices) {
outputFrameListeners[i].onImagesAvailable()
}
if (hasResultsRemaining) return

val state =
state.updateAndGet { current ->
Expand All @@ -137,15 +142,18 @@ internal class FrameState(
}
}

for (listenerState in listenerStates) {
listenerState.invokeOnImagesAvailable(frameNumber, frameTimestamp)
}

if (state == COMPLETE) {
invokeOnFrameComplete()
}
}

private fun invokeOnFrameComplete() {
// Invoke the onOutputResultsAvailable listener.
for (i in outputFrameListeners.indices) {
outputFrameListeners[i].onFrameComplete()
for (listenerState in listenerStates) {
listenerState.invokeOnFrameComplete(frameNumber, frameTimestamp)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package androidx.camera.camera2.pipe.internal
import androidx.camera.camera2.pipe.CameraTimestamp
import androidx.camera.camera2.pipe.Frame
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.StreamId
import kotlinx.atomicfu.atomic

internal class ListenerState(val listener: Frame.Listener) {
Expand Down Expand Up @@ -83,4 +84,13 @@ internal class ListenerState(val listener: Frame.Listener) {
listener.onFrameComplete()
}
}

/**
* Invokes [listener.onImageAvailable(streamId)].
*
* @param streamId The [StreamId] that the image is available
*/
fun invokeOnImageAvailable(streamId: StreamId) {
listener.onImageAvailable(streamId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.internal

import androidx.camera.camera2.pipe.CameraTimestamp
import androidx.camera.camera2.pipe.Frame
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.OutputId
import androidx.camera.camera2.pipe.OutputStatus
Expand All @@ -29,6 +30,11 @@ import androidx.camera.camera2.pipe.testing.FakeImage
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.camera2.pipe.testing.FakeSurfaces
import com.google.common.truth.Truth.assertThat
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -63,6 +69,35 @@ class FrameStateTest {
private val fakeFrameInfo =
FakeFrameInfo(metadata = fakeFrameMetadata, requestMetadata = fakeRequestMetadata)

private val fakeListener =
object : Frame.Listener {
val frameStartedCalled = atomic(0)
val frameInfoAvailableCalled = atomic(0)
val imageAvailableCalled = atomic(0)
val frameCompletedCalled = atomic(0)

override fun onFrameStarted(frameNumber: FrameNumber, frameTimestamp: CameraTimestamp) {
frameStartedCalled.incrementAndGet()
}

override fun onFrameInfoAvailable() {
frameInfoAvailableCalled.incrementAndGet()
}

override fun onImageAvailable(streamId: StreamId) {
// Do nothing. ListenerState doesn't care about onImageAvailable on stream level
// currently.
}

override fun onImagesAvailable() {
imageAvailableCalled.incrementAndGet()
}

override fun onFrameComplete() {
frameCompletedCalled.incrementAndGet()
}
}

private val frameState =
FrameState(
requestMetadata = fakeRequestMetadata,
Expand Down Expand Up @@ -207,4 +242,125 @@ class FrameStateTest {
assertThat(frameState.frameInfoOutput.status).isEqualTo(OutputStatus.UNAVAILABLE)
assertThat(frameState.frameInfoOutput.outputOrNull()).isNull()
}

@Test
fun addListener_invokesOnStarted_whenStateIsStarted() {
// FrameState's initial state is STARTED
frameState.addListener(fakeListener)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun addListener_stateIsFrameInfoAvailable_invokesStartAndFrameInfoComplete() {
frameState.onFrameInfoComplete()

frameState.addListener(fakeListener)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun addListener_stateIsImagesAvailable_invokesStartAndImagesAvailable() {
// All stream result completed
frameState.onStreamResultComplete(stream1Id)
frameState.onStreamResultComplete(stream2Id)

frameState.addListener(fakeListener)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun addListener_stateIsFrameComplete_invokesAllCallbacks() {
frameState.onStreamResultComplete(stream1Id)
frameState.onStreamResultComplete(stream2Id)
frameState.onFrameInfoComplete()

frameState.addListener(fakeListener)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(1)
}

@Test
fun onFrameInfoComplete_invokesOnFrameInfoAvailable() {
frameState.addListener(fakeListener)

frameState.onFrameInfoComplete()

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun onStreamResultComplete_doesNotHaveStreamResultForAllStreams_doesNotInvokesOnImagesAvailable() {
frameState.addListener(fakeListener)

frameState.onStreamResultComplete(stream1Id)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun onStreamResultComplete_invokesOnImagesAvailable_afterAllStreamsComplete() {
frameState.addListener(fakeListener)

frameState.onStreamResultComplete(stream1Id)
frameState.onStreamResultComplete(stream2Id)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(0)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(0)
}

@Test
fun frameState_transitionsToComplete_allCallbacksAreTriggered() {
frameState.addListener(fakeListener)

frameState.onFrameInfoComplete()
frameState.onStreamResultComplete(stream1Id)
frameState.onStreamResultComplete(stream2Id)

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(1)
}

@Test
fun concurrentFrameStateChangeAndNewListenerAdded_ensureCallbacksCalledOnce() = runBlocking {
val numCoroutines = 4

val jobs =
listOf(
launch(Dispatchers.Default) { frameState.onFrameInfoComplete() },
launch(Dispatchers.Default) { frameState.onStreamResultComplete(stream1Id) },
launch(Dispatchers.Default) { frameState.addListener(fakeListener) },
launch(Dispatchers.Default) { frameState.onStreamResultComplete(stream2Id) },
)
jobs.joinAll()

assertThat(fakeListener.frameStartedCalled.value).isEqualTo(1)
assertThat(fakeListener.frameInfoAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.imageAvailableCalled.value).isEqualTo(1)
assertThat(fakeListener.frameCompletedCalled.value).isEqualTo(1)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) <YEAR> The Android Open Source Project
* Copyright 2026 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.
Expand All @@ -14,7 +14,8 @@
* limitations under the License.
*/

/**
* Insert package level documentation here
*/
package <PACKAGE>;
package androidx.compose.runtime

internal interface Anchor {
val valid: Boolean
}
Loading
Loading