From 8286e2f156e9519f237c647c70674a59678179f6 Mon Sep 17 00:00:00 2001 From: Andrew Bailey Date: Fri, 9 Jan 2026 11:46:26 -0500 Subject: [PATCH 01/10] Abstract out Anchor definition This CL factors out an abstract `Anchor` class to be implemented separately for the LinkTable and SlotTable. Both implementations of the Composer require Anchors (though the LinkTable in substantially fewer cases), so ultimately one implementation will remain and the base class introduced in this CL will be removed. Test: N/A Relnote: N/A Change-Id: I7a78e02e725eeef3421de27bb4b3fb4f91f61c7a --- .../kotlin/androidx/compose/runtime/Anchor.kt | 21 +++ .../androidx/compose/runtime/Composer.kt | 8 +- .../androidx/compose/runtime/Composition.kt | 2 - .../androidx/compose/runtime/GapComposer.kt | 21 +-- .../compose/runtime/MovableContent.kt | 1 - .../compose/runtime/RecomposeScopeImpl.kt | 6 +- .../runtime/composer/gapbuffer/GapAnchor.kt | 45 ++++++ .../runtime/composer/gapbuffer/SlotTable.kt | 146 ++++++++---------- .../gapbuffer/changelist/ChangeList.kt | 14 +- .../changelist/ComposerChangeListWriter.kt | 14 +- .../gapbuffer/changelist/FixupList.kt | 4 +- .../gapbuffer/changelist/Operation.kt | 30 ++-- .../tooling/ComposeStackTraceBuilder.kt | 20 ++- .../composer/gapbuffer/SlotTableTests.kt | 76 ++++----- 14 files changed, 231 insertions(+), 177 deletions(-) create mode 100644 compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Anchor.kt create mode 100644 compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/GapAnchor.kt diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Anchor.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Anchor.kt new file mode 100644 index 0000000000000..be5b373850790 --- /dev/null +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Anchor.kt @@ -0,0 +1,21 @@ +/* + * 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. + * 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.runtime + +internal interface Anchor { + val valid: Boolean +} diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt index 171f74da39778..3e4ef84c0d4d8 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt @@ -21,10 +21,10 @@ package androidx.compose.runtime import androidx.compose.runtime.Composer.Companion.Empty import androidx.compose.runtime.collection.ScopeMap -import androidx.compose.runtime.composer.gapbuffer.Anchor import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter +import androidx.compose.runtime.composer.gapbuffer.asGapAnchor import androidx.compose.runtime.tooling.ComposeStackTrace import androidx.compose.runtime.tooling.ComposeStackTraceFrame import androidx.compose.runtime.tooling.ComposeStackTraceMode @@ -1365,7 +1365,7 @@ internal inline fun SlotWriter.withAfterAnchorInfo(anchor: Anchor?, cb: (Int var priority = -1 var endRelativeAfter = -1 if (anchor != null && anchor.valid) { - priority = anchorIndex(anchor) + priority = anchorIndex(anchor.asGapAnchor()) endRelativeAfter = slotsSize - slotsEndAllIndex(priority) } cb(priority, endRelativeAfter) @@ -1531,7 +1531,7 @@ internal fun extractMovableContentAtCurrent( if (anchor.valid) { val extracted = (composition as CompositionImpl).extractInvalidationsOfGroup { - slots.inGroup(anchor, it) + slots.inGroup(anchor.asGapAnchor(), it.asGapAnchor()) } reference.invalidations += extracted } @@ -1550,7 +1550,7 @@ internal fun extractMovableContentAtCurrent( writer.update(reference.parameter) // Move the content into current location - val anchors = slots.moveTo(reference.anchor, 1, writer) + val anchors = slots.moveTo(reference.anchor.asGapAnchor(), 1, writer) // skip the group that was just inserted. writer.skipGroup() diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt index df4c3307a6ea3..94700b538ef54 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt @@ -25,7 +25,6 @@ import androidx.collection.ScatterSet import androidx.compose.runtime.collection.ScopeMap import androidx.compose.runtime.collection.fastForEach import androidx.compose.runtime.composer.DebugStringFormattable -import androidx.compose.runtime.composer.gapbuffer.Anchor import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable import androidx.compose.runtime.composer.gapbuffer.changelist.ChangeList @@ -38,7 +37,6 @@ import androidx.compose.runtime.snapshots.ReaderKind import androidx.compose.runtime.snapshots.StateObjectImpl import androidx.compose.runtime.snapshots.fastAll import androidx.compose.runtime.snapshots.fastAny -import androidx.compose.runtime.snapshots.fastForEach import androidx.compose.runtime.tooling.CompositionErrorContextImpl import androidx.compose.runtime.tooling.CompositionObserver import androidx.compose.runtime.tooling.CompositionObserverHandle diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt index 69ab0a80d56e5..e605f1a300d01 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/GapComposer.kt @@ -29,11 +29,12 @@ import androidx.compose.runtime.collection.MultiValueMap import androidx.compose.runtime.collection.ScopeMap import androidx.compose.runtime.composer.GroupInfo import androidx.compose.runtime.composer.GroupKind -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.KeyInfo import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter +import androidx.compose.runtime.composer.gapbuffer.asGapAnchor import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable import androidx.compose.runtime.composer.gapbuffer.changelist.ChangeList import androidx.compose.runtime.composer.gapbuffer.changelist.ComposerChangeListWriter @@ -284,7 +285,7 @@ internal class GapComposer( override var deferredChanges: ChangeList? = null private val changeListWriter = ComposerChangeListWriter(this, changes) - private var insertAnchor: Anchor = insertTable.read { it.anchor(0) } + private var insertAnchor: GapAnchor = insertTable.read { it.anchor(0) } private var insertFixups = FixupList() private var pausable: Boolean = false @@ -1981,7 +1982,7 @@ internal class GapComposer( override fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean { val anchor = scope.anchor ?: return false val slotTable = reader.table - val location = anchor.toIndexFor(slotTable) + val location = anchor.asGapAnchor().toIndexFor(slotTable) if (isComposing && location >= reader.currentGroup) { // if we are invalidating a scope that is going to be traversed during this // composition. @@ -2304,7 +2305,7 @@ internal class GapComposer( changeListWriter.withChangeList(lateChanges) { changeListWriter.resetSlots() references.fastForEach { (to, from) -> - val anchor = to.anchor + val anchor = to.anchor.asGapAnchor() val toSlotTable = to.slotStorage.asGapBufferSlotTable() val location = toSlotTable.anchorIndex(anchor) val effectiveNodeIndex = IntRef() @@ -2354,7 +2355,7 @@ internal class GapComposer( val resolvedState = parentContext.movableContentStateResolve(from) val resolvedSlotTable = resolvedState?.slotStorage?.asGapBufferSlotTable() val fromTable = resolvedSlotTable ?: from.slotStorage.asGapBufferSlotTable() - val fromAnchor = resolvedSlotTable?.anchor(0) ?: from.anchor + val fromAnchor = (resolvedSlotTable?.anchor(0) ?: from.anchor).asGapAnchor() val nodesToInsert = fromTable.collectNodesFrom(fromAnchor) // Insert nodes if necessary @@ -2383,7 +2384,7 @@ internal class GapComposer( fromTable.read { reader -> withReader(reader) { - val newLocation = fromTable.anchorIndex(fromAnchor) + val newLocation = fromTable.anchorIndex(fromAnchor.asGapAnchor()) reader.reposition(newLocation) changeListWriter.moveReaderToAbsolute(newLocation) val offsetChanges = ChangeList() @@ -2591,7 +2592,7 @@ internal class GapComposer( // that are no longer in the slot table. for (i in invalidations.lastIndex downTo 0) { val invalidation = invalidations[i] - val anchor = invalidation.scope.anchor + val anchor = invalidation.scope.anchor?.asGapAnchor() if (anchor != null && anchor.valid) { if (invalidation.location != anchor.location) invalidation.location = anchor.location @@ -2603,7 +2604,7 @@ internal class GapComposer( // Add the requested invalidations invalidationsRequested.map.forEach { scope, instances -> scope as RecomposeScopeImpl - val location = scope.anchor?.location ?: return@forEach + val location = scope.anchor?.asGapAnchor()?.location ?: return@forEach invalidations.add( Invalidation(scope, location, instances.takeUnless { it === ScopeInvalidated }) ) @@ -2689,7 +2690,7 @@ internal class GapComposer( runtimeCheck(!nodeExpected) { "A call to createNode(), emitNode() or useNode() expected" } } - private fun recordInsert(anchor: Anchor) { + private fun recordInsert(anchor: GapAnchor) { if (insertFixups.isEmpty()) { changeListWriter.insertSlots(anchor, insertTable) } else { @@ -3330,7 +3331,7 @@ private fun Boolean.asInt() = if (this) 1 else 0 private fun Int.asBool() = this != 0 -private fun SlotTable.collectNodesFrom(anchor: Anchor): List { +private fun SlotTable.collectNodesFrom(anchor: GapAnchor): List { val result = mutableListOf() read { reader -> val index = anchorIndex(anchor) diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/MovableContent.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/MovableContent.kt index bb70aa690b340..2b33212cec4f7 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/MovableContent.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/MovableContent.kt @@ -17,7 +17,6 @@ package androidx.compose.runtime import androidx.compose.runtime.annotation.RememberInComposition -import androidx.compose.runtime.composer.gapbuffer.Anchor /** * Convert a lambda into one that moves the remembered state and nodes created in a previous call to diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt index 2b3e62e092730..d8757b436b051 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt @@ -19,7 +19,7 @@ package androidx.compose.runtime import androidx.collection.MutableObjectIntMap import androidx.collection.MutableScatterMap import androidx.collection.ScatterSet -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.snapshots.fastAny @@ -426,7 +426,7 @@ internal class RecomposeScopeImpl(internal var owner: RecomposeScopeOwner?) : companion object { internal fun adoptAnchoredScopes( slots: SlotWriter, - anchors: List, + anchors: List, newOwner: RecomposeScopeOwner, ) { if (anchors.isNotEmpty()) { @@ -439,7 +439,7 @@ internal class RecomposeScopeImpl(internal var owner: RecomposeScopeOwner?) : } } - internal fun hasAnchoredRecomposeScopes(slots: SlotTable, anchors: List) = + internal fun hasAnchoredRecomposeScopes(slots: SlotTable, anchors: List) = anchors.isNotEmpty() && anchors.fastAny { slots.ownsAnchor(it) && diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/GapAnchor.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/GapAnchor.kt new file mode 100644 index 0000000000000..671fcaa3511f6 --- /dev/null +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/GapAnchor.kt @@ -0,0 +1,45 @@ +/* + * 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. + * 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.runtime.composer.gapbuffer + +import androidx.compose.runtime.Anchor +import androidx.compose.runtime.composeRuntimeError + +/** + * An [Anchor] tracks a groups as its index changes due to other groups being inserted and removed + * before it. If the group the [Anchor] is tracking is removed, directly or indirectly, [valid] will + * return false. The current index of the group can be determined by passing either the [SlotTable] + * or [] to [toIndexFor]. If a [SlotWriter] is active, it must be used instead of the [SlotTable] as + * the anchor index could have shifted due to operations performed on the writer. + */ +internal class GapAnchor(loc: Int) : Anchor { + internal var location: Int = loc + + override val valid + get() = location != Int.MIN_VALUE + + fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this) + + fun toIndexFor(writer: SlotWriter) = writer.anchorIndex(this) + + override fun toString(): String { + return "${super.toString()}{ location = $location }" + } +} + +internal fun Anchor.asGapAnchor(): GapAnchor = + this as? GapAnchor ?: composeRuntimeError("Inconsistent composition") diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt index ec6e22833c2c1..ba8927f648283 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt @@ -28,6 +28,7 @@ import androidx.collection.emptyScatterMap import androidx.collection.mutableIntListOf import androidx.collection.mutableIntSetOf import androidx.collection.mutableScatterMapOf +import androidx.compose.runtime.Anchor import androidx.compose.runtime.Applier import androidx.compose.runtime.Composer import androidx.compose.runtime.IntStack @@ -159,10 +160,10 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable = arrayListOf() + internal var anchors: ArrayList = arrayListOf() /** A map of source information to anchor. */ - internal var sourceInformationMap: HashMap? = null + internal var sourceInformationMap: HashMap? = null /** * A map of source marker numbers to their, potentially indirect, parent key. This is recorded @@ -239,20 +240,21 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable= 0 && anchors[it] == anchor } } @@ -283,20 +285,21 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable?, + sourceInformationMap: HashMap?, ) { runtimeCheck(reader.table === this && readers > 0) { "Unexpected reader close()" } readers-- @@ -323,8 +326,8 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable, slotsSize: Int, - anchors: ArrayList, - sourceInformationMap: HashMap?, + anchors: ArrayList, + sourceInformationMap: HashMap?, calledByMap: MutableIntObjectMap?, ) { requirePrecondition(writer.table === this && this.writer) { "Unexpected writer close()" } @@ -341,8 +344,8 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable, slotsSize: Int, - anchors: ArrayList, - sourceInformationMap: HashMap?, + anchors: ArrayList, + sourceInformationMap: HashMap?, calledByMap: MutableIntObjectMap?, ) { // Adopt the slots from the writer @@ -368,7 +371,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable? { - val anchors = mutableListOf() + val anchors = mutableListOf() val scopes = mutableListOf() var allScopesFound = true val set = @@ -388,7 +391,9 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable when (item) { - is Anchor -> { + is GapAnchor -> { requirePrecondition(item.valid) { "Source map contains invalid anchor" } requirePrecondition(ownsAnchor(item)) { "Source map anchor is not owned by the slot table" @@ -587,7 +592,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable - scope.anchor?.let { anchor -> + scope.anchor?.asGapAnchor()?.let { anchor -> checkPrecondition(scope in slotsOf(anchor.toIndexFor(this))) { val dataIndex = slots.indexOf(scope) "Misaligned anchor $anchor in scope $scope encountered, scope found at " + @@ -628,7 +633,9 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable, ): ScatterMap { val referencesToExtract = - references.fastFilter { ownsAnchor(it.anchor) }.sortedBy { anchorIndex(it.anchor) } + references + .fastFilter { ownsAnchor(it.anchor.asGapAnchor()) } + .sortedBy { anchorIndex(it.anchor.asGapAnchor()) } if (referencesToExtract.isEmpty()) return emptyScatterMap() val result = mutableScatterMapOf() write { writer -> @@ -651,7 +658,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable - val newGroup = writer.anchorIndex(reference.anchor) + val newGroup = writer.anchorIndex(reference.anchor.asGapAnchor()) val newParent = writer.parent(newGroup) closeToGroupContaining(newParent) openParent(newParent) @@ -817,28 +824,6 @@ private inline fun Array.fastForEach(action: (T) -> Unit) { for (i in 0 until size) action(this[i]) } -/** - * An [Anchor] tracks a groups as its index changes due to other groups being inserted and removed - * before it. If the group the [Anchor] is tracking is removed, directly or indirectly, [valid] will - * return false. The current index of the group can be determined by passing either the [SlotTable] - * or [] to [toIndexFor]. If a [SlotWriter] is active, it must be used instead of the [SlotTable] as - * the anchor index could have shifted due to operations performed on the writer. - */ -internal class Anchor(loc: Int) { - internal var location: Int = loc - - val valid - get() = location != Int.MIN_VALUE - - fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this) - - fun toIndexFor(writer: SlotWriter) = writer.anchorIndex(this) - - override fun toString(): String { - return "${super.toString()}{ location = $location }" - } -} - internal class GroupSourceInformation( val key: Int, var sourceInformation: String?, @@ -896,18 +881,18 @@ internal class GroupSourceInformation( groups.add(group) } - private fun hasAnchor(anchor: Anchor): Boolean = + private fun hasAnchor(anchor: GapAnchor): Boolean = groups?.fastAny { it == anchor || (it is GroupSourceInformation && it.hasAnchor(anchor)) } == true - fun removeAnchor(anchor: Anchor): Boolean { + fun removeAnchor(anchor: GapAnchor): Boolean { val groups = groups if (groups != null) { var index = groups.size - 1 while (index >= 0) { when (val item = groups[index]) { - is Anchor -> if (item == anchor) groups.removeAt(index) + is GapAnchor -> if (item == anchor) groups.removeAt(index) is GroupSourceInformation -> if (!item.removeAnchor(anchor)) { groups.removeAt(index) @@ -968,7 +953,7 @@ internal class SlotReader( * A local copy of the [sourceInformationMap] being created to be merged into [table] when the * reader closes. */ - private var sourceInformationMap: HashMap? = null + private var sourceInformationMap: HashMap? = null /** True if the reader has been closed */ var closed: Boolean = false @@ -1106,7 +1091,7 @@ internal class SlotReader( get() = if (currentGroup < currentEnd) groups.node(currentGroup) else null /** Get the group key at [anchor]. This return 0 if the anchor is not valid. */ - fun groupKey(anchor: Anchor) = if (anchor.valid) groups.key(table.anchorIndex(anchor)) else 0 + fun groupKey(anchor: GapAnchor) = if (anchor.valid) groups.key(table.anchorIndex(anchor)) else 0 /** Returns true when the group at [index] was marked with [SlotWriter.markGroup]. */ fun hasMark(index: Int) = groups.hasMark(index) @@ -1332,7 +1317,7 @@ internal class SlotReader( /** Create an anchor to the current reader location or [index]. */ fun anchor(index: Int = currentGroup) = - table.anchors.getOrAdd(index, groupsSize) { Anchor(index) } + table.anchors.getOrAdd(index, groupsSize) { GapAnchor(index) } private fun IntArray.node(index: Int) = if (isNode(index)) { @@ -1391,7 +1376,7 @@ internal class SlotWriter( private var slots: Array = table.slots /** A copy of the [SlotTable.anchors] to avoid having to index through [table]. */ - private var anchors: ArrayList = table.anchors + private var anchors: ArrayList = table.anchors /** A copy of [SlotTable.sourceInformationMap] to avoid having to index through [table] */ private var sourceInformationMap = table.sourceInformationMap @@ -1549,7 +1534,7 @@ internal class SlotWriter( } /** Return the node at [anchor] if it is a node group or null. */ - fun node(anchor: Anchor) = node(anchor.toIndexFor(this)) + fun node(anchor: GapAnchor) = node(anchor.toIndexFor(this)) /** Return the index of the nearest group that contains [currentGroup]. */ var parent: Int = -1 @@ -1562,7 +1547,7 @@ internal class SlotWriter( * Return the index of the parent for the group referenced by [anchor]. If the anchor is not * valid it returns -1. */ - fun parent(anchor: Anchor) = if (anchor.valid) groups.parent(anchorIndex(anchor)) else -1 + fun parent(anchor: GapAnchor) = if (anchor.valid) groups.parent(anchorIndex(anchor)) else -1 /** True if the writer has been closed */ var closed = false @@ -1630,7 +1615,7 @@ internal class SlotWriter( } /** Append a slot to the [parent] group. */ - fun appendSlot(anchor: Anchor, value: Any?) { + fun appendSlot(anchor: GapAnchor, value: Any?) { runtimeCheck(insertCount == 0) { "Can only append a slot if not current inserting" } var previousCurrentSlot = currentSlot var previousCurrentSlotEnd = currentSlotEnd @@ -1753,7 +1738,8 @@ internal class SlotWriter( fun updateNode(value: Any?) = updateNodeOfGroup(currentGroup, value) /** Update the node of a the group at [anchor] to [value]. */ - fun updateNode(anchor: Anchor, value: Any?) = updateNodeOfGroup(anchor.toIndexFor(this), value) + fun updateNode(anchor: GapAnchor, value: Any?) = + updateNodeOfGroup(anchor.toIndexFor(this), value) /** Updates the node of the parent group. */ fun updateParentNode(value: Any?) = updateNodeOfGroup(parent, value) @@ -1812,7 +1798,7 @@ internal class SlotWriter( * Read the [index] slot at the group at [anchor]. Returns [Composer.Empty] if the slot is empty * (e.g. out of range). */ - fun slot(anchor: Anchor, index: Int) = slot(anchorIndex(anchor), index) + fun slot(anchor: GapAnchor, index: Int) = slot(anchorIndex(anchor), index) /** * Read the [index] slot at the group at index [groupIndex]. Returns [Composer.Empty] if the @@ -1885,7 +1871,7 @@ internal class SlotWriter( * Seek the current location to [anchor]. The [anchor] must be an anchor to a possibly indirect * child of [parent]. */ - fun seek(anchor: Anchor) = advanceBy(anchor.toIndexFor(this) - currentGroup) + fun seek(anchor: GapAnchor) = advanceBy(anchor.toIndexFor(this) - currentGroup) /** Skip to the end of the current group. */ fun skipToGroupEnd() { @@ -2139,7 +2125,7 @@ internal class SlotWriter( } } - fun ensureStarted(anchor: Anchor) = ensureStarted(anchor.toIndexFor(this)) + fun ensureStarted(anchor: GapAnchor) = ensureStarted(anchor.toIndexFor(this)) /** Skip the current group. Returns the number of nodes skipped by skipping the group. */ fun skipGroup(): Int { @@ -2424,7 +2410,7 @@ internal class SlotWriter( } } - fun inGroup(groupAnchor: Anchor, anchor: Anchor): Boolean { + fun inGroup(groupAnchor: GapAnchor, anchor: GapAnchor): Boolean { val group = anchorIndex(groupAnchor) val groupEnd = group + groups.groupSize(group) return anchor.location in group until groupEnd @@ -2438,7 +2424,7 @@ internal class SlotWriter( updateFromCursor: Boolean, updateToCursor: Boolean, removeSourceGroup: Boolean = true, - ): List { + ): List { val groupsToMove = fromWriter.groupSize(fromIndex) val sourceGroupsEnd = fromIndex + groupsToMove val sourceSlotsStart = fromWriter.dataIndex(fromIndex) @@ -2523,7 +2509,7 @@ internal class SlotWriter( val anchors = if (startAnchors < endAnchors) { val sourceAnchors = fromWriter.anchors - val anchors = ArrayList(endAnchors - startAnchors) + val anchors = ArrayList(endAnchors - startAnchors) // update the anchor locations to their new location val anchorDelta = currentGroup - fromIndex @@ -2637,7 +2623,7 @@ internal class SlotWriter( * * This requires [writer] be inserting and this writer to not be inserting. */ - fun moveTo(anchor: Anchor, offset: Int, writer: SlotWriter): List { + fun moveTo(anchor: GapAnchor, offset: Int, writer: SlotWriter): List { runtimeCheck(writer.insertCount > 0) runtimeCheck(insertCount == 0) runtimeCheck(anchor.valid) @@ -2687,7 +2673,7 @@ internal class SlotWriter( * * @return a list of the anchors that were moved */ - fun moveFrom(table: SlotTable, index: Int, removeSourceGroup: Boolean = true): List { + fun moveFrom(table: SlotTable, index: Int, removeSourceGroup: Boolean = true): List { runtimeCheck(insertCount > 0) if ( @@ -2757,7 +2743,7 @@ internal class SlotWriter( * * @return a list of the anchors that were moved. */ - fun moveIntoGroupFrom(offset: Int, table: SlotTable, index: Int): List { + fun moveIntoGroupFrom(offset: Int, table: SlotTable, index: Int): List { runtimeCheck(insertCount <= 0 && groupSize(currentGroup + offset) == 1) val previousCurrentGroup = currentGroup val previousCurrentSlot = currentSlot @@ -2778,9 +2764,9 @@ internal class SlotWriter( } /** Allocate an anchor to the current group or [index]. */ - fun anchor(index: Int = currentGroup): Anchor = + fun anchor(index: Int = currentGroup): GapAnchor = anchors.getOrAdd(index, size) { - Anchor(if (index <= groupGapStart) index else -(size - index)) + GapAnchor(if (index <= groupGapStart) index else -(size - index)) } fun markGroup(group: Int = parent) { @@ -2840,7 +2826,7 @@ internal class SlotWriter( } /** Return the current anchor location while changing the slot table. */ - fun anchorIndex(anchor: Anchor) = anchor.location.let { if (it < 0) size + it else it } + fun anchorIndex(anchor: GapAnchor) = anchor.location.let { if (it < 0) size + it else it } override fun toString(): String { return "SlotWriter(current = $currentGroup end=$currentGroupEnd size = $size " + @@ -3221,7 +3207,7 @@ internal class SlotWriter( private fun removeAnchors( gapStart: Int, size: Int, - sourceInformationMap: HashMap?, + sourceInformationMap: HashMap?, ): Boolean { val gapLen = groupGapLen val removeEnd = gapStart + size @@ -3257,7 +3243,7 @@ internal class SlotWriter( // Remove all the anchors in range from the original location val index = anchors.locationOf(originalLocation, groupsSize) - val removedAnchors = mutableListOf() + val removedAnchors = mutableListOf() if (index >= 0) { while (index < anchors.size) { val anchor = anchors[index] @@ -3548,7 +3534,7 @@ private class SlotTableGroup( } override fun find(identityToFind: Any): CompositionGroup? { - fun findAnchoredGroup(anchor: Anchor): CompositionGroup? { + fun findAnchoredGroup(anchor: GapAnchor): CompositionGroup? { if (table.ownsAnchor(anchor)) { val anchorGroup = table.anchorIndex(anchor) if (anchorGroup >= group && (anchorGroup - group < table.groups.groupSize(group))) { @@ -3562,7 +3548,7 @@ private class SlotTableGroup( group.compositionGroups.drop(index).firstOrNull() return when (identityToFind) { - is Anchor -> findAnchoredGroup(identityToFind) + is GapAnchor -> findAnchoredGroup(identityToFind) is SourceInformationSlotTableGroupIdentity -> find(identityToFind.parentIdentity)?.let { findRelativeGroup(it, identityToFind.index) @@ -3882,7 +3868,7 @@ private class SourceInformationGroupIterator( override fun next(): CompositionGroup { return when (val group = group.groups?.get(index++)) { - is Anchor -> SlotTableGroup(table, group.location, version) + is GapAnchor -> SlotTableGroup(table, group.location, version) is GroupSourceInformation -> SourceInformationSlotTableGroup( table = table, @@ -4104,11 +4090,11 @@ private fun IntArray.updateGroupKey(address: Int, key: Int) { this[arrayIndex + Key_Offset] = key } -private inline fun ArrayList.getOrAdd( +private inline fun ArrayList.getOrAdd( index: Int, effectiveSize: Int, - block: () -> Anchor, -): Anchor { + block: () -> GapAnchor, +): GapAnchor { val location = search(index, effectiveSize) return if (location < 0) { val anchor = block() @@ -4117,13 +4103,13 @@ private inline fun ArrayList.getOrAdd( } else get(location) } -private fun ArrayList.find(index: Int, effectiveSize: Int): Anchor? { +private fun ArrayList.find(index: Int, effectiveSize: Int): GapAnchor? { val location = search(index, effectiveSize) return if (location >= 0) get(location) else null } /** This is inlined here instead to avoid allocating a lambda for the compare when this is used. */ -private fun ArrayList.search(location: Int, effectiveSize: Int): Int { +private fun ArrayList.search(location: Int, effectiveSize: Int): Int { var low = 0 var high = size - 1 @@ -4145,7 +4131,7 @@ private fun ArrayList.search(location: Int, effectiveSize: Int): Int { * A wrapper on [search] that always returns an index in to [this] even if [index] is not in the * array list. */ -private fun ArrayList.locationOf(index: Int, effectiveSize: Int) = +private fun ArrayList.locationOf(index: Int, effectiveSize: Int) = search(index, effectiveSize).let { if (it >= 0) it else -(it + 1) } /** diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ChangeList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ChangeList.kt index cb8107236292b..8ec0581c8fef1 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ChangeList.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ChangeList.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.RecomposeScopeImpl import androidx.compose.runtime.RememberManager import androidx.compose.runtime.RememberObserverHolder import androidx.compose.runtime.SlotStorage -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable @@ -135,14 +135,14 @@ internal class ChangeList : Changes() { } } - fun pushUpdateAnchoredValue(value: Any?, anchor: Anchor, groupSlotIndex: Int) { + fun pushUpdateAnchoredValue(value: Any?, anchor: GapAnchor, groupSlotIndex: Int) { operations.push(UpdateAnchoredValue) { setObjects(UpdateAnchoredValue.Value, value, UpdateAnchoredValue.Anchor, anchor) setInt(UpdateAnchoredValue.GroupSlotIndex, groupSlotIndex) } } - fun pushAppendValue(anchor: Anchor, value: Any?) { + fun pushAppendValue(anchor: GapAnchor, value: Any?) { operations.push(AppendValue) { setObjects(AppendValue.Anchor, anchor, AppendValue.Value, value) } @@ -168,7 +168,7 @@ internal class ChangeList : Changes() { operations.push(EnsureRootGroupStarted) } - fun pushEnsureGroupStarted(anchor: Anchor) { + fun pushEnsureGroupStarted(anchor: GapAnchor) { operations.push(EnsureGroupStarted) { setObject(EnsureGroupStarted.Anchor, anchor) } } @@ -184,13 +184,13 @@ internal class ChangeList : Changes() { operations.push(RemoveCurrentGroup) } - fun pushInsertSlots(anchor: Anchor, from: SlotTable) { + fun pushInsertSlots(anchor: GapAnchor, from: SlotTable) { operations.push(InsertSlots) { setObjects(InsertSlots.Anchor, anchor, InsertSlots.FromSlotTable, from) } } - fun pushInsertSlots(anchor: Anchor, from: SlotTable, fixups: FixupList) { + fun pushInsertSlots(anchor: GapAnchor, from: SlotTable, fixups: FixupList) { operations.push(InsertSlotsWithFixups) { setObjects( InsertSlotsWithFixups.Anchor, @@ -261,7 +261,7 @@ internal class ChangeList : Changes() { operations.push(SideEffect) { setObject(SideEffect.Effect, effect) } } - fun pushDetermineMovableContentNodeIndex(effectiveNodeIndexOut: IntRef, anchor: Anchor) { + fun pushDetermineMovableContentNodeIndex(effectiveNodeIndexOut: IntRef, anchor: GapAnchor) { operations.push(DetermineMovableContentNodeIndex) { setObjects( DetermineMovableContentNodeIndex.EffectiveNodeIndexOut, diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ComposerChangeListWriter.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ComposerChangeListWriter.kt index 72cf30a017d67..812e5a4d78f6f 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ComposerChangeListWriter.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/ComposerChangeListWriter.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.MovableContentStateReference import androidx.compose.runtime.RecomposeScopeImpl import androidx.compose.runtime.RememberObserverHolder import androidx.compose.runtime.Stack -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.internal.IntRef @@ -150,7 +150,7 @@ internal class ComposerChangeListWriter( } } - private fun ensureGroupStarted(anchor: Anchor) { + private fun ensureGroupStarted(anchor: GapAnchor) { pushSlotTableOperationPreamble() changeList.pushEnsureGroupStarted(anchor) startedGroup = true @@ -210,12 +210,12 @@ internal class ComposerChangeListWriter( changeList.pushUpdateValue(value, groupSlotIndex) } - fun updateAnchoredValue(value: Any?, anchor: Anchor, groupSlotIndex: Int) { + fun updateAnchoredValue(value: Any?, anchor: GapAnchor, groupSlotIndex: Int) { // Because this uses an anchor, it can be performed without positioning the writer. changeList.pushUpdateAnchoredValue(value, anchor, groupSlotIndex) } - fun appendValue(anchor: Anchor, value: Any?) { + fun appendValue(anchor: GapAnchor, value: Any?) { // Because this uses an anchor, it can be performed without positioning the writer. changeList.pushAppendValue(anchor, value) } @@ -271,14 +271,14 @@ internal class ComposerChangeListWriter( writersReaderDelta += reader.groupSize } - fun insertSlots(anchor: Anchor, from: SlotTable) { + fun insertSlots(anchor: GapAnchor, from: SlotTable) { pushPendingUpsAndDowns() pushSlotEditingOperationPreamble() realizeNodeMovementOperations() changeList.pushInsertSlots(anchor, from) } - fun insertSlots(anchor: Anchor, from: SlotTable, fixups: FixupList) { + fun insertSlots(anchor: GapAnchor, from: SlotTable, fixups: FixupList) { pushPendingUpsAndDowns() pushSlotEditingOperationPreamble() realizeNodeMovementOperations() @@ -406,7 +406,7 @@ internal class ComposerChangeListWriter( changeList.pushSideEffect(effect) } - fun determineMovableContentNodeIndex(effectiveNodeIndexOut: IntRef, anchor: Anchor) { + fun determineMovableContentNodeIndex(effectiveNodeIndexOut: IntRef, anchor: GapAnchor) { pushPendingUpsAndDowns() changeList.pushDetermineMovableContentNodeIndex(effectiveNodeIndexOut, anchor) } diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/FixupList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/FixupList.kt index e07ea60618fac..d6b1ea9608a20 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/FixupList.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/FixupList.kt @@ -18,7 +18,7 @@ package androidx.compose.runtime.composer.gapbuffer.changelist import androidx.compose.runtime.Applier import androidx.compose.runtime.RememberManager -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.runtimeCheck @@ -56,7 +56,7 @@ internal class FixupList : OperationsDebugStringFormattable() { ) } - fun createAndInsertNode(factory: () -> Any?, insertIndex: Int, groupAnchor: Anchor) { + fun createAndInsertNode(factory: () -> Any?, insertIndex: Int, groupAnchor: GapAnchor) { operations.push(Operation.InsertNodeFixup) { setObject(Operation.InsertNodeFixup.Factory, factory) setInt(Operation.InsertNodeFixup.InsertIndex, insertIndex) diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/Operation.kt index 78731a6ee5049..917da6569c3af 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/Operation.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/changelist/Operation.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.RememberManager import androidx.compose.runtime.RememberObserverHolder import androidx.compose.runtime.TestOnly import androidx.compose.runtime.composeRuntimeError -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable @@ -65,7 +65,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { } } - protected open fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): Anchor? = null + protected open fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): GapAnchor? = null protected abstract fun OperationArgContainer.execute( applier: Applier<*>, @@ -256,7 +256,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { object AppendValue : Operation(objects = 2) { inline val Anchor - get() = ObjectParameter(0) + get() = ObjectParameter(0) inline val Value get() = ObjectParameter(1) @@ -356,7 +356,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { get() = ObjectParameter(0) inline val Anchor - get() = ObjectParameter(1) + get() = ObjectParameter(1) inline val GroupSlotIndex get() = 0 @@ -432,7 +432,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { object EnsureGroupStarted : Operation(objects = 1) { inline val Anchor - get() = ObjectParameter(0) + get() = ObjectParameter(0) override fun objectParamName(parameter: ObjectParameter<*>) = when (parameter) { @@ -621,7 +621,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { object InsertSlots : Operation(objects = 2) { inline val Anchor - get() = ObjectParameter(0) + get() = ObjectParameter(0) inline val FromSlotTable get() = ObjectParameter(1) @@ -654,7 +654,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { object InsertSlotsWithFixups : Operation(objects = 3) { inline val Anchor - get() = ObjectParameter(0) + get() = ObjectParameter(0) inline val FromSlotTable get() = ObjectParameter(1) @@ -706,7 +706,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { get() = 0 inline val GroupAnchor - get() = ObjectParameter(1) + get() = ObjectParameter(1) override fun intParamName(parameter: IntParameter) = when (parameter) { @@ -721,7 +721,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { else -> super.objectParamName(parameter) } - override fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): Anchor? = + override fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): GapAnchor? = getObject(GroupAnchor) override fun OperationArgContainer.execute( @@ -746,7 +746,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { get() = 0 inline val GroupAnchor - get() = ObjectParameter(0) + get() = ObjectParameter(0) override fun intParamName(parameter: IntParameter) = when (parameter) { @@ -760,7 +760,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { else -> super.objectParamName(parameter) } - override fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): Anchor? = + override fun OperationArgContainer.getGroupAnchor(slots: SlotWriter): GapAnchor? = getObject(GroupAnchor) override fun OperationArgContainer.execute( @@ -809,7 +809,7 @@ internal sealed class Operation(val ints: Int = 0, val objects: Int = 0) { get() = ObjectParameter(0) inline val Anchor - get() = ObjectParameter(1) + get() = ObjectParameter(1) override fun objectParamName(parameter: ObjectParameter<*>) = when (parameter) { @@ -1077,7 +1077,7 @@ private fun currentNodeIndex(slots: SlotWriter): Int { return index } -private fun positionToInsert(slots: SlotWriter, anchor: Anchor, applier: Applier): Int { +private fun positionToInsert(slots: SlotWriter, anchor: GapAnchor, applier: Applier): Int { val destination = slots.anchorIndex(anchor) runtimeCheck(slots.currentGroup < destination) positionToParentOf(slots, applier, destination) @@ -1102,7 +1102,7 @@ private fun positionToInsert(slots: SlotWriter, anchor: Anchor, applier: Applier private inline fun withCurrentStackTrace( errorContext: OperationErrorContext?, writer: SlotWriter, - location: Anchor?, + location: GapAnchor?, block: () -> Unit, ) { try { @@ -1117,7 +1117,7 @@ private inline fun withCurrentStackTrace( private fun Throwable.attachComposeStackTrace( errorContext: OperationErrorContext?, writer: SlotWriter, - anchor: Anchor?, + anchor: GapAnchor?, ): Throwable { if (errorContext == null) return this return attachComposeStackTrace { diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt index 1d6001c55cf6e..efac09f8c0ae8 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt @@ -15,15 +15,17 @@ */ package androidx.compose.runtime.tooling +import androidx.compose.runtime.Anchor import androidx.compose.runtime.Composer import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.GapComposer.CompositionContextHolder import androidx.compose.runtime.RememberObserverHolder -import androidx.compose.runtime.composer.gapbuffer.Anchor +import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.GroupSourceInformation import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter +import androidx.compose.runtime.composer.gapbuffer.asGapAnchor import androidx.compose.runtime.defaultsKey import androidx.compose.runtime.reference import androidx.compose.runtime.referenceKey @@ -31,16 +33,18 @@ import androidx.compose.runtime.snapshots.fastForEach internal class WriterTraceBuilder(private val writer: SlotWriter) : ComposeStackTraceBuilder() { override fun sourceInformationOf(anchor: Anchor): GroupSourceInformation? = - writer.sourceInformationOf(writer.anchorIndex(anchor)) + writer.sourceInformationOf(writer.anchorIndex(anchor.asGapAnchor())) - override fun groupKeyOf(anchor: Anchor): Int = writer.groupKey(writer.anchorIndex(anchor)) + override fun groupKeyOf(anchor: Anchor): Int = + writer.groupKey(writer.anchorIndex(anchor.asGapAnchor())) } internal class ReaderTraceBuilder(private val reader: SlotReader) : ComposeStackTraceBuilder() { override fun sourceInformationOf(anchor: Anchor): GroupSourceInformation? = - reader.table.sourceInformationOf(reader.table.anchorIndex(anchor)) + reader.table.sourceInformationOf(reader.table.anchorIndex(anchor.asGapAnchor())) - override fun groupKeyOf(anchor: Anchor): Int = reader.groupKey(reader.table.anchorIndex(anchor)) + override fun groupKeyOf(anchor: Anchor): Int = + reader.groupKey(reader.table.anchorIndex(anchor.asGapAnchor())) } internal abstract class ComposeStackTraceBuilder { @@ -86,7 +90,7 @@ internal abstract class ComposeStackTraceBuilder { sourceInfo != null && (sourceInfo.key == defaultsKey || (sourceInfo.key == 0 && - child is Anchor && + child is GapAnchor && groupKeyOf(child) == defaultsKey)) // If sourceInformation is null, it means that default group does not capture @@ -113,7 +117,7 @@ internal abstract class ComposeStackTraceBuilder { private fun sourceInformationOf(group: Any) = when (group) { - is Anchor -> sourceInformationOf(group) + is GapAnchor -> sourceInformationOf(group) is GroupSourceInformation -> group else -> error("Unexpected child source info $group") } @@ -177,7 +181,7 @@ internal abstract class ComposeStackTraceBuilder { children.fastForEach { child -> // find the edge that leads to target anchor when (child) { - is Anchor -> { + is GapAnchor -> { // edge found, return if (child == target) { appendTraceFrame(sourceInformation.key, sourceInformation, child) diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTableTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTableTests.kt index 66afb96225049..d97c921f85178 100644 --- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTableTests.kt +++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTableTests.kt @@ -259,7 +259,7 @@ class SlotTableTests { val slots = testSlotsNumbered() val anchors = slots.read { reader -> - val anchors = mutableListOf() + val anchors = mutableListOf() reader.startGroup() repeat(7) { repeat(10) { reader.skipGroup() } @@ -282,7 +282,7 @@ class SlotTableTests { val slots = testSlotsNumbered() val anchors = slots.read { reader -> - val anchors = mutableListOf() + val anchors = mutableListOf() reader.startGroup() repeat(7) { repeat(10) { reader.skipGroup() } @@ -372,8 +372,8 @@ class SlotTableTests { fun testAnchorMoves() { val slots = SlotTable() - fun buildSlots(range: List): Map { - val anchors = mutableMapOf() + fun buildSlots(range: List): Map { + val anchors = mutableMapOf() slots.write { writer -> fun item(value: Int, block: () -> Unit) { writer.startGroup(value) @@ -402,7 +402,7 @@ class SlotTableTests { return anchors } - fun validate(anchors: Map) { + fun validate(anchors: Map) { slots.verifyWellFormed() slots.read { reader -> for (anchor in anchors) { @@ -852,7 +852,7 @@ class SlotTableTests { fun testMoveGroup() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() fun buildSlots() { slots.write { writer -> @@ -1182,7 +1182,7 @@ class SlotTableTests { @Test fun testMovingOneGroup() { val sourceTable = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() sourceTable.write { writer -> writer.beginInsert() anchors.add(writer.anchor()) @@ -1224,7 +1224,7 @@ class SlotTableTests { @Test fun testMovingANodeGroup() { val sourceTable = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() sourceTable.write { writer -> writer.beginInsert() anchors.add(writer.anchor()) @@ -1270,7 +1270,7 @@ class SlotTableTests { @Test fun testMovingMultipleRootGroups() { val sourceTable = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() val moveCount = 5 sourceTable.write { writer -> writer.beginInsert() @@ -1338,7 +1338,7 @@ class SlotTableTests { } } - val movedAnchors = mutableSetOf() + val movedAnchors = mutableSetOf() slotsToMove.forEach { anchor -> if (anchor !in movedAnchors) { destinationTable.write { writer -> @@ -1383,7 +1383,7 @@ class SlotTableTests { val sourceTable = SlotTable() val destinationTable = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() sourceTable.write { writer -> writer.insert { writer.group(10) { @@ -1913,7 +1913,7 @@ class SlotTableTests { @Test fun testUpdatingNodeWithStartNode() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() slots.write { writer -> writer.insert { writer.group(treeRoot) { @@ -2110,7 +2110,7 @@ class SlotTableTests { @Test fun testUpdatingNodeWithUpdateParentNode() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() slots.write { writer -> writer.insert { writer.group(treeRoot) { @@ -2159,7 +2159,7 @@ class SlotTableTests { @Test fun testUpdatingNodeWithUpdateNode() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() slots.write { writer -> writer.insert { writer.group(treeRoot) { @@ -2204,7 +2204,7 @@ class SlotTableTests { @Test fun testUpdatingAuxWithUpdateAux() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() slots.write { writer -> writer.insert { writer.group(treeRoot) { @@ -2253,7 +2253,7 @@ class SlotTableTests { val innerGroupKeyBase = 1000 val dataCount = 5 - data class SlotInfo(val anchor: Anchor, val index: Int, val value: Int) + data class SlotInfo(val anchor: GapAnchor, val index: Int, val value: Int) slots.write { writer -> writer.insert { @@ -2424,7 +2424,7 @@ class SlotTableTests { @Test fun testMultipleRoots() { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() repeat(10) { slots.write { writer -> anchors.add(writer.anchor()) @@ -2444,13 +2444,13 @@ class SlotTableTests { @Test fun testCanRestoreParent() { - val anchors = mutableMapOf>() + val anchors = mutableMapOf>() val slots = SlotTable() slots.write { writer -> writer.beginInsert() writer.startGroup(treeRoot) repeat(10) { outerKey -> - val nestedAnchors = mutableListOf() + val nestedAnchors = mutableListOf() anchors[outerKey] = nestedAnchors writer.startGroup(outerKey) repeat(10) { innerKey -> @@ -2588,7 +2588,7 @@ class SlotTableTests { @Test fun testInsertOfZeroGroups() { - val sourceAnchors = mutableListOf() + val sourceAnchors = mutableListOf() val sourceTable = SlotTable().also { it.write { writer -> @@ -2611,8 +2611,8 @@ class SlotTableTests { } } - var container = Anchor(0) - val destinationAnchors = mutableListOf() + var container = GapAnchor(0) + val destinationAnchors = mutableListOf() val slots = SlotTable().also { it.write { writer -> @@ -3406,7 +3406,7 @@ class SlotTableTests { @Test fun canMoveTo() { val slots = SlotTable() - var anchor = Anchor(-1) + var anchor = GapAnchor(-1) // Create a slot table slots.write { writer -> @@ -3515,8 +3515,8 @@ class SlotTableTests { @Test fun canDeleteAGroupAfterMovingPartOfItsContent() { val slots = SlotTable() - var deleteAnchor = Anchor(-1) - var moveAnchor = Anchor(-1) + var deleteAnchor = GapAnchor(-1) + var moveAnchor = GapAnchor(-1) // Create a slot table slots.write { writer -> @@ -3587,9 +3587,9 @@ class SlotTableTests { @Test fun canMoveAndDeleteAfterAnInsert() { val slots = SlotTable() - var insertAnchor = Anchor(-1) - var deleteAnchor = Anchor(-1) - var moveAnchor = Anchor(-1) + var insertAnchor = GapAnchor(-1) + var deleteAnchor = GapAnchor(-1) + var moveAnchor = GapAnchor(-1) // Create a slot table slots.write { writer -> @@ -3653,7 +3653,7 @@ class SlotTableTests { @Test fun canMoveAGroupFromATableIntoAnotherGroup() { val slots = SlotTable().apply { collectSourceInformation() } - var insertAnchor = Anchor(-1) + var insertAnchor = GapAnchor(-1) // Create a slot table slots.write { writer -> @@ -4210,7 +4210,7 @@ class SlotTableTests { @Test fun canMoveAGroupFromATableIntoAnotherGroupAndModifyThatGroup() { val slots = SlotTable() - var insertAnchor = Anchor(-1) + var insertAnchor = GapAnchor(-1) // Create a slot table slots.write { writer -> @@ -4360,7 +4360,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_first_empty() { - var anchor: Anchor? = null + var anchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -4409,7 +4409,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_first_occupied() { - var anchor: Anchor? = null + var anchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -4470,7 +4470,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_after_occupied() { - var anchor: Anchor? = null + var anchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -4539,7 +4539,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_middle() { - var anchor: Anchor? = null + var anchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -4610,7 +4610,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_end() { - var anchor: Anchor? = null + var anchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -4672,7 +4672,7 @@ class SlotTableTests { @Test fun supportsAppendingSlots_ensureStarted() { - var insertAnchor: Anchor? = null + var insertAnchor: GapAnchor? = null val slots = SlotTable().apply { write { writer -> @@ -5569,9 +5569,9 @@ private fun validateItems(slots: SlotTable) { } } -private fun narrowTrees(): Pair> { +private fun narrowTrees(): Pair> { val slots = SlotTable() - val anchors = mutableListOf() + val anchors = mutableListOf() slots.write { writer -> writer.beginInsert() writer.startGroup(treeRoot) From 659fef58960646df2d7b4adec483fe90f9826fc7 Mon Sep 17 00:00:00 2001 From: Andrew Bailey Date: Fri, 9 Jan 2026 13:48:36 -0500 Subject: [PATCH 02/10] Abstract out GroupSourceInformation GroupSourceInformation is made an interface so that the LinkComposer and GapComposer may provide different implementations. Test: N/A Relnote: N/A Change-Id: I89f49df014a2d63d4ab3118764f1f50a41bca4b6 --- .../composer/GroupSourceInformation.kt | 26 ++++++++ .../runtime/composer/gapbuffer/SlotTable.kt | 63 ++++++++++--------- .../tooling/ComposeStackTraceBuilder.kt | 2 +- 3 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/GroupSourceInformation.kt diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/GroupSourceInformation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/GroupSourceInformation.kt new file mode 100644 index 0000000000000..26855b43681d3 --- /dev/null +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/GroupSourceInformation.kt @@ -0,0 +1,26 @@ +/* + * 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. + * 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.runtime.composer + +internal interface GroupSourceInformation { + val closed: Boolean + val dataEndOffset: Int + val dataStartOffset: Int + val key: Int + val sourceInformation: String? + val groups: ArrayList? +} diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt index ba8927f648283..b7a204bff0e4d 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/composer/gapbuffer/SlotTable.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.collection.fastCopyInto import androidx.compose.runtime.collection.fastFilter import androidx.compose.runtime.collection.sortedBy import androidx.compose.runtime.composeRuntimeError +import androidx.compose.runtime.composer.GroupSourceInformation import androidx.compose.runtime.deactivateCurrentGroup import androidx.compose.runtime.debugRuntimeCheck import androidx.compose.runtime.extractMovableContentAtCurrent @@ -163,7 +164,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable = arrayListOf() /** A map of source information to anchor. */ - internal var sourceInformationMap: HashMap? = null + internal var sourceInformationMap: HashMap? = null /** * A map of source marker numbers to their, potentially indirect, parent key. This is recorded @@ -299,7 +300,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable?, + sourceInformationMap: HashMap?, ) { runtimeCheck(reader.table === this && readers > 0) { "Unexpected reader close()" } readers-- @@ -327,7 +328,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable, slotsSize: Int, anchors: ArrayList, - sourceInformationMap: HashMap?, + sourceInformationMap: HashMap?, calledByMap: MutableIntObjectMap?, ) { requirePrecondition(writer.table === this && this.writer) { "Unexpected writer close()" } @@ -345,7 +346,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable, slotsSize: Int, anchors: ArrayList, - sourceInformationMap: HashMap?, + sourceInformationMap: HashMap?, calledByMap: MutableIntObjectMap?, ) { // Adopt the slots from the writer @@ -558,7 +559,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable when (item) { is GapAnchor -> { @@ -567,7 +568,7 @@ internal class SlotTable : SlotStorage(), CompositionData, Iterable verifySourceGroup(item) + is GapGroupSourceInformation -> verifySourceGroup(item) } } } @@ -824,17 +825,17 @@ private inline fun Array.fastForEach(action: (T) -> Unit) { for (i in 0 until size) action(this[i]) } -internal class GroupSourceInformation( - val key: Int, - var sourceInformation: String?, - val dataStartOffset: Int, -) { - var groups: ArrayList? = null - var closed = false - var dataEndOffset: Int = 0 +internal class GapGroupSourceInformation( + override val key: Int, + override var sourceInformation: String?, + override val dataStartOffset: Int, +) : GroupSourceInformation { + override var groups: ArrayList? = null + override var closed = false + override var dataEndOffset: Int = 0 fun startGrouplessCall(key: Int, sourceInformation: String, dataOffset: Int) { - openInformation().add(GroupSourceInformation(key, sourceInformation, dataOffset)) + openInformation().add(GapGroupSourceInformation(key, sourceInformation, dataOffset)) } fun endGrouplessCall(dataOffset: Int) { @@ -856,7 +857,7 @@ internal class GroupSourceInformation( val anchor = writer.tryAnchor(predecessor) if (anchor != null) { groups.fastIndexOf { - it == anchor || (it is GroupSourceInformation && it.hasAnchor(anchor)) + it == anchor || (it is GapGroupSourceInformation && it.hasAnchor(anchor)) } } else 0 } else 0 @@ -869,10 +870,10 @@ internal class GroupSourceInformation( } // Return the current open nested source information or this. - private fun openInformation(): GroupSourceInformation = + private fun openInformation(): GapGroupSourceInformation = (groups?.let { groups -> - groups.fastLastOrNull { it is GroupSourceInformation && !it.closed } - } as? GroupSourceInformation) + groups.fastLastOrNull { it is GapGroupSourceInformation && !it.closed } + } as? GapGroupSourceInformation) ?.openInformation() ?: this private fun add(group: Any /* Anchor | GroupSourceInformation */) { @@ -883,7 +884,7 @@ internal class GroupSourceInformation( private fun hasAnchor(anchor: GapAnchor): Boolean = groups?.fastAny { - it == anchor || (it is GroupSourceInformation && it.hasAnchor(anchor)) + it == anchor || (it is GapGroupSourceInformation && it.hasAnchor(anchor)) } == true fun removeAnchor(anchor: GapAnchor): Boolean { @@ -893,7 +894,7 @@ internal class GroupSourceInformation( while (index >= 0) { when (val item = groups[index]) { is GapAnchor -> if (item == anchor) groups.removeAt(index) - is GroupSourceInformation -> + is GapGroupSourceInformation -> if (!item.removeAnchor(anchor)) { groups.removeAt(index) } @@ -953,7 +954,7 @@ internal class SlotReader( * A local copy of the [sourceInformationMap] being created to be merged into [table] when the * reader closes. */ - private var sourceInformationMap: HashMap? = null + private var sourceInformationMap: HashMap? = null /** True if the reader has been closed */ var closed: Boolean = false @@ -1715,9 +1716,9 @@ internal class SlotWriter( private fun groupSourceInformationFor( parent: Int, sourceInformation: String?, - ): GroupSourceInformation? = + ): GapGroupSourceInformation? = sourceInformationMap?.getOrPut(anchor(parent)) { - val result = GroupSourceInformation(0, sourceInformation, 0) + val result = GapGroupSourceInformation(0, sourceInformation, 0) // If we called from a groupless call then the groups added before this call // are not reflected in this group information so they need to be added now @@ -3139,7 +3140,7 @@ internal class SlotWriter( } else false } - internal fun sourceInformationOf(group: Int): GroupSourceInformation? = + internal fun sourceInformationOf(group: Int): GapGroupSourceInformation? = sourceInformationMap?.let { map -> tryAnchor(group)?.let { anchor -> map[anchor] } } internal fun tryAnchor(group: Int) = @@ -3207,7 +3208,7 @@ internal class SlotWriter( private fun removeAnchors( gapStart: Int, size: Int, - sourceInformationMap: HashMap?, + sourceInformationMap: HashMap?, ): Boolean { val gapLen = groupGapLen val removeEnd = gapStart + size @@ -3597,7 +3598,7 @@ private class RelativeGroupPath(val parent: SourceInformationGroupPath, val inde private class SourceInformationSlotTableGroup( val table: SlotTable, val parent: Int, - val sourceInformation: GroupSourceInformation, + val sourceInformation: GapGroupSourceInformation, val identityPath: SourceInformationGroupPath, ) : CompositionGroup, Iterable { override val key: Any = sourceInformation.key @@ -3679,7 +3680,7 @@ private class DataIterator(val table: SlotTable, group: Int) : Iterable, I private class SourceInformationGroupDataIterator( val table: SlotTable, group: Int, - sourceInformation: GroupSourceInformation, + sourceInformation: GapGroupSourceInformation, ) : Iterable, Iterator { private val base = table.groups.dataAnchor(group) private val start: Int = sourceInformation.dataStartOffset @@ -3695,7 +3696,7 @@ private class SourceInformationGroupDataIterator( // Filter any groups val groups = sourceInformation.groups ?: return@also groups.fastForEach { info -> - if (info is GroupSourceInformation) { + if (info is GapGroupSourceInformation) { it.setRange(info.dataStartOffset, info.dataEndOffset) } } @@ -3858,7 +3859,7 @@ private val Long.firstBitSet private class SourceInformationGroupIterator( val table: SlotTable, val parent: Int, - val group: GroupSourceInformation, + val group: GapGroupSourceInformation, val path: SourceInformationGroupPath, ) : Iterator { private val version = table.version @@ -3869,7 +3870,7 @@ private class SourceInformationGroupIterator( override fun next(): CompositionGroup { return when (val group = group.groups?.get(index++)) { is GapAnchor -> SlotTableGroup(table, group.location, version) - is GroupSourceInformation -> + is GapGroupSourceInformation -> SourceInformationSlotTableGroup( table = table, parent = parent, diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt index efac09f8c0ae8..693de0dc0bd6e 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt @@ -20,8 +20,8 @@ import androidx.compose.runtime.Composer import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.GapComposer.CompositionContextHolder import androidx.compose.runtime.RememberObserverHolder +import androidx.compose.runtime.composer.GroupSourceInformation import androidx.compose.runtime.composer.gapbuffer.GapAnchor -import androidx.compose.runtime.composer.gapbuffer.GroupSourceInformation import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter From 18d0daf8b9366ba651d6f69d3d6c8d8bef6fa6a1 Mon Sep 17 00:00:00 2001 From: EthanWu Date: Wed, 31 Dec 2025 18:19:57 +0800 Subject: [PATCH 03/10] Align RuntimeUtils with HitTestResult Null-Safety & Not-Hit Logic This commit updates RuntimeUtilsTest to align with the stricter null-safety contract of XrExtensions.HitTestResult.Builder. The XrExtensions.HitTestResult.Builder now strictly enforces NonNull for its parameters. Test cases attempting to pass null to parameter `hitPosition` are no longer valid and will result in a throw. Specifically: - Updated `getHitTestResult_convertsFromExtensionHitTestResult_withNoHit`: Modified to ensure `hitPosition` reflects a practical value a `0,0,0` vector, aligning with the API contract and preventing unexpected throws. Bug: 475718769 Test: Presubmit Change-Id: I92242f57bdfd0e5cd169420c49685d92fd7a96c0 --- .../androidx/xr/scenecore/spatial/core/RuntimeUtilsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/RuntimeUtilsTest.java b/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/RuntimeUtilsTest.java index 5a193c1d1728d..67272d6f7747c 100644 --- a/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/RuntimeUtilsTest.java +++ b/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/RuntimeUtilsTest.java @@ -558,7 +558,8 @@ public void getHitTestResult_convertsFromExtensionHitTestResult() { @Test public void getHitTestResult_convertsFromExtensionHitTestResult_withNoHit() { float distance = Float.POSITIVE_INFINITY; - Vec3 hitPosition = null; + Vector3 expectedNoHitPosition = new Vector3(0.0f, 0.0f, 0.0f); + Vec3 hitPosition = new Vec3(0.0f, 0.0f, 0.0f); int surfaceType = com.android.extensions.xr.space.HitTestResult.SURFACE_UNKNOWN; com.android.extensions.xr.space.HitTestResult.Builder hitTestResultBuilder = @@ -570,7 +571,7 @@ public void getHitTestResult_convertsFromExtensionHitTestResult_withNoHit() { HitTestResult hitTestResult = RuntimeUtils.getHitTestResult(extensionsHitTestResult); assertThat(hitTestResult.getDistance()).isEqualTo(distance); - assertThat(hitTestResult.getHitPosition()).isNull(); + assertThat(hitTestResult.getHitPosition()).isEqualTo(expectedNoHitPosition); assertThat(hitTestResult.getSurfaceNormal()).isNull(); assertThat(hitTestResult.getSurfaceType()) .isEqualTo(HitTestResult.HitTestSurfaceType.HIT_TEST_RESULT_SURFACE_TYPE_UNKNOWN); From 754453b22a9a0bd37821933ef37181af530b041e Mon Sep 17 00:00:00 2001 From: Paul Rohde Date: Fri, 16 Jan 2026 10:09:22 -0800 Subject: [PATCH 04/10] Output the frame number during lock3AForCapture This improves it's utility when diagnosing issues where AF/AE is taking longer than expected. Change-Id: I6afaf449dd1dde7794838cb090ef3933d0fb4313 --- .../src/main/java/androidx/camera/camera2/pipe/Frames.kt | 4 +++- .../java/androidx/camera/camera2/pipe/graph/Controller3A.kt | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frames.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frames.kt index ae7bfdebb73ea..c0ecdfebc7df2 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frames.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frames.kt @@ -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) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt index dd7b4cda078df..26debff4cc184 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt @@ -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 From fe89e88a563cd9063c035cd19f74e6c95294bb1f Mon Sep 17 00:00:00 2001 From: Aidan Melvin Date: Fri, 16 Jan 2026 13:52:38 -0500 Subject: [PATCH 05/10] Remove old project creator script Bug: 468088488 Test: N/A Change-Id: I83fd18385863be5d34eb7c96979bfeb39f873448 --- development/project-creator/README.md | 43 -- .../project-creator/base-requirements.txt | 4 - .../compose-template/groupId/OWNERS | 1 - .../groupId/artifactId/api/current.txt | 1 - .../groupId/artifactId/build.gradle | 107 --- .../groupId/artifactId-documentation.md | 7 - development/project-creator/create_project.py | 708 ------------------ development/project-creator/create_project.sh | 27 - .../java-template/groupId/OWNERS | 1 - .../groupId/artifactId/api/current.txt | 1 - .../groupId/artifactId/build.gradle | 46 -- .../src/main/java/groupId/package-info.java | 20 - .../kotlin-template/groupId/OWNERS | 1 - .../groupId/artifactId/api/current.txt | 1 - .../groupId/artifactId/build.gradle | 45 -- .../java/groupId/artifactId-documentation.md | 7 - .../native-template/groupId/OWNERS | 1 - .../groupId/artifactId/api/current.txt | 1 - .../groupId/artifactId/build.gradle | 69 -- .../artifactId/src/main/cpp/CMakeLists.txt | 14 - .../artifactId/src/main/cpp/include/.keep | 0 .../main/cpp/version_scripts/libname.map.txt | 4 - .../java/groupId/artifactId-documentation.md | 7 - development/project-creator/requirements.txt | 2 - .../project-creator/test_project_creator.py | 337 --------- 25 files changed, 1455 deletions(-) delete mode 100644 development/project-creator/README.md delete mode 100644 development/project-creator/base-requirements.txt delete mode 100644 development/project-creator/compose-template/groupId/OWNERS delete mode 100644 development/project-creator/compose-template/groupId/artifactId/api/current.txt delete mode 100644 development/project-creator/compose-template/groupId/artifactId/build.gradle delete mode 100644 development/project-creator/compose-template/groupId/artifactId/src/commonMain/kotlin/groupId/artifactId-documentation.md delete mode 100755 development/project-creator/create_project.py delete mode 100755 development/project-creator/create_project.sh delete mode 100644 development/project-creator/java-template/groupId/OWNERS delete mode 100644 development/project-creator/java-template/groupId/artifactId/api/current.txt delete mode 100644 development/project-creator/java-template/groupId/artifactId/build.gradle delete mode 100644 development/project-creator/java-template/groupId/artifactId/src/main/java/groupId/package-info.java delete mode 100644 development/project-creator/kotlin-template/groupId/OWNERS delete mode 100644 development/project-creator/kotlin-template/groupId/artifactId/api/current.txt delete mode 100644 development/project-creator/kotlin-template/groupId/artifactId/build.gradle delete mode 100644 development/project-creator/kotlin-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md delete mode 100644 development/project-creator/native-template/groupId/OWNERS delete mode 100644 development/project-creator/native-template/groupId/artifactId/api/current.txt delete mode 100644 development/project-creator/native-template/groupId/artifactId/build.gradle delete mode 100644 development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt delete mode 100644 development/project-creator/native-template/groupId/artifactId/src/main/cpp/include/.keep delete mode 100644 development/project-creator/native-template/groupId/artifactId/src/main/cpp/version_scripts/libname.map.txt delete mode 100644 development/project-creator/native-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md delete mode 100644 development/project-creator/requirements.txt delete mode 100755 development/project-creator/test_project_creator.py diff --git a/development/project-creator/README.md b/development/project-creator/README.md deleted file mode 100644 index 31732f20cfaab..0000000000000 --- a/development/project-creator/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Project creator - -This script will create a new library project and associated Gradle module using -a `groupId` and `artifactId`. - -It will use the `groupId` and `artifactId` to guess which configuration is most -appropriate for the project you are creating. - -## Basic usage - -```bash -./create_project.py androidx.foo foo-bar -``` - -## Project types - -The script leverages -`buildSrc/public/src/main/kotlin/androidx/build/SoftwareType.kt` to create the -recommended defaults for your project. However, you can override the options to -best fit your requirements. - -## Additional documentation - -See go/androidx-api-guidelines#module-creation (internal-only) or the -[equivalent page](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:docs/api_guidelines/modules.md#module-creation) -on public Android Code Search for advanced usage and solutions to common issues. - -## Development - -If you make any changes to the script, please update this `README` and make -corresponding updates at go/androidx-api-guidelines#module-creation. - -### Testing the script - -Generic project integration test -```bash -./create_project.py androidx.foo.bar bar-qux -``` - -Script test suite -```bash -./test_project_creator.py -``` diff --git a/development/project-creator/base-requirements.txt b/development/project-creator/base-requirements.txt deleted file mode 100644 index bb7386aabb0ad..0000000000000 --- a/development/project-creator/base-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -virtualenv==20.31.2 --hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 -distlib==0.3.7 --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 -filelock==3.12.2 --hash=sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec -platformdirs==3.9.1 --hash=sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f diff --git a/development/project-creator/compose-template/groupId/OWNERS b/development/project-creator/compose-template/groupId/OWNERS deleted file mode 100644 index 15331e5f84a15..0000000000000 --- a/development/project-creator/compose-template/groupId/OWNERS +++ /dev/null @@ -1 +0,0 @@ -# example@google.com diff --git a/development/project-creator/compose-template/groupId/artifactId/api/current.txt b/development/project-creator/compose-template/groupId/artifactId/api/current.txt deleted file mode 100644 index e6f50d0d0fd11..0000000000000 --- a/development/project-creator/compose-template/groupId/artifactId/api/current.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 4.0 diff --git a/development/project-creator/compose-template/groupId/artifactId/build.gradle b/development/project-creator/compose-template/groupId/artifactId/build.gradle deleted file mode 100644 index 68530fd433db8..0000000000000 --- a/development/project-creator/compose-template/groupId/artifactId/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 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. - */ - -/** - * This file was created using the `create_project.py` script located in the - * `/development/project-creator` directory. - * - * Please use that script when creating a new project, rather than copying an existing project and - * modifying its settings. - */ - -import androidx.build.SoftwareType -import androidx.build.PlatformIdentifier - -plugins { - id("AndroidXPlugin") - id("AndroidXComposePlugin") -} - -androidXMultiplatform { - androidLibrary { - namespace = "" - - } - jvmStubs() - linuxX64Stubs() - - defaultPlatform(PlatformIdentifier.ANDROID) - - sourceSets { - commonMain { - dependencies { - } - } - - commonTest { - dependencies { - } - } - - jvmMain { - dependsOn(commonMain) - dependencies { - implementation(libs.testRules) - implementation(libs.testRunner) - implementation(libs.junit) - implementation(libs.truth) - } - } - - androidMain { - dependsOn(jvmMain) - dependencies { - api("androidx.annotation:annotation:1.8.1") - } - } - - jvmTest { - dependsOn(commonTest) - dependencies { - } - } - - androidDeviceTest { - dependsOn(jvmTest) - dependencies { - implementation(libs.testRules) - implementation(libs.testRunner) - implementation(libs.junit) - implementation(libs.truth) - } - } - - commonStubsMain { - dependsOn(commonMain) - } - - jvmStubsMain { - dependsOn(commonStubsMain) - } - - linuxx64StubsMain { - dependsOn(commonStubsMain) - } - } -} - -androidx { - name = "" - type = SoftwareType. - mavenVersion = LibraryVersions. - inceptionYear = "" - description = "" -} diff --git a/development/project-creator/compose-template/groupId/artifactId/src/commonMain/kotlin/groupId/artifactId-documentation.md b/development/project-creator/compose-template/groupId/artifactId/src/commonMain/kotlin/groupId/artifactId-documentation.md deleted file mode 100644 index a37b120e17182..0000000000000 --- a/development/project-creator/compose-template/groupId/artifactId/src/commonMain/kotlin/groupId/artifactId-documentation.md +++ /dev/null @@ -1,7 +0,0 @@ -# Module root - - - -# Package - -Insert package level documentation here diff --git a/development/project-creator/create_project.py b/development/project-creator/create_project.py deleted file mode 100755 index da2c6abd0b136..0000000000000 --- a/development/project-creator/create_project.py +++ /dev/null @@ -1,708 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import sys -import os -import argparse -from datetime import date -import subprocess -from enum import Enum -from textwrap import dedent -from shutil import rmtree -from shutil import copyfile -from shutil import copytree -import re - -try: - # non-default python3 module, be helpful if it is missing - import toml -except ModuleNotFoundError as e: - print(e) - print("Consider running `pip install toml` to install this module") - exit(-1) - -# cd into directory of script -os.chdir(os.path.dirname(os.path.abspath(__file__))) - -FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..')) -SAMPLE_OWNERS_FP = os.path.abspath(os.path.join(os.getcwd(), 'kotlin-template', 'groupId', 'OWNERS')) -SAMPLE_JAVA_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'java-template', 'groupId', 'artifactId')) -SAMPLE_KOTLIN_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'kotlin-template', 'groupId', 'artifactId')) -SAMPLE_COMPOSE_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'compose-template', 'groupId', 'artifactId')) -NATIVE_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'native-template', 'groupId', 'artifactId')) -SETTINGS_GRADLE_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..', "settings.gradle")) -LIBRARY_VERSIONS_REL = './libraryversions.toml' -LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL) -DOCS_TOT_BUILD_GRADLE_REL = './docs-tip-of-tree/build.gradle' -DOCS_TOT_BUILD_GRADLE_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, DOCS_TOT_BUILD_GRADLE_REL) - -# Set up input arguments -parser = argparse.ArgumentParser( - description=("""Genereates new project in androidx.""")) -parser.add_argument( - 'group_id', - help='group_id for the new library') -parser.add_argument( - 'artifact_id', - help='artifact_id for the new library') - - -class ProjectType(Enum): - KOTLIN = 0 - JAVA = 1 - NATIVE = 2 - -def print_e(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - -def cp(src_path_dir, dst_path_dir): - """Copies all files in the src_path_dir into the dst_path_dir - - Args: - src_path_dir: the source directory, which must exist - dst_path_dir: the distination directory - """ - if not os.path.exists(dst_path_dir): - os.makedirs(dst_path_dir) - if not os.path.exists(src_path_dir): - print_e('cp error: Source path %s does not exist.' % src_path_dir) - return None - try: - copytree(src_path_dir, dst_path_dir, dirs_exist_ok=True) - except Error as err: - print_e('FAIL: Unable to copy %s to destination %s' % (src_path_dir, dst_path_dir)) - return None - return dst_path_dir - -def rm(path): - if os.path.isdir(path): - rmtree(path) - elif os.path.exists(path): - os.remove(path) - -def mv_dir(src_path_dir, dst_path_dir): - """Moves a directory from src_path_dir to dst_path_dir. - - Args: - src_path_dir: the source directory, which must exist - dst_path_dir: the distination directory - """ - if os.path.exists(dst_path_dir): - print_e('rename error: Destination path %s already exists.' % dst_path_dir) - return None - # If moving to a new parent directory, create that directory - parent_dst_path_dir = os.path.dirname(dst_path_dir) - if not os.path.exists(parent_dst_path_dir): - os.makedirs(parent_dst_path_dir) - if not os.path.exists(src_path_dir): - print_e('mv error: Source path %s does not exist.' % src_path_dir) - return None - try: - os.rename(src_path_dir, dst_path_dir) - except OSError as error: - print_e('FAIL: Unable to copy %s to destination %s' % (src_path_dir, dst_path_dir)) - print_e(error) - return None - return dst_path_dir - -def rename_file(src_file, new_file_name): - """Renames a file from src_file to new_file_name, within the same directory. - - Args: - src_file: the source file, which must exist - new_file_name: the new file name - """ - if not os.path.exists(src_file): - print_e('mv file error: Source file %s does not exist.' % src_file) - return None - # Check that destination directory already exists - parent_src_file_dir = os.path.dirname(src_file) - new_file_path = os.path.join(parent_src_file_dir, new_file_name) - if os.path.exists(new_file_path): - print_e('mv file error: Source file %s already exists.' % new_file_path) - return None - try: - os.rename(src_file, new_file_path) - except OSError as error: - print_e('FAIL: Unable to rename %s to destination %s' % (src_file, new_file_path)) - print_e(error) - return None - return new_file_path - -def create_file(path): - """ - Creates an empty file if it does not already exist. - """ - open(path, "a").close() - -def generate_package_name(group_id, artifact_id): - final_group_id_word = group_id.split(".")[-1] - artifact_id_suffix = re.sub(r"\b%s\b" % final_group_id_word, "", artifact_id) - artifact_id_suffix = artifact_id_suffix.replace("-", ".") - if (final_group_id_word == artifact_id): - return group_id + artifact_id_suffix - elif (final_group_id_word != artifact_id): - if ("." in artifact_id_suffix): - return group_id + artifact_id_suffix - else: - return group_id + "." + artifact_id_suffix - -def validate_name(group_id, artifact_id): - if not group_id.startswith("androidx."): - print_e("Group ID must start with androidx.") - return False - final_group_id_word = group_id.split(".")[-1] - if not artifact_id.startswith(final_group_id_word): - print_e("Artifact ID must use the final word in the group Id " + \ - "as the prefix. For example, `androidx.foo.bar:bar-qux`" + \ - "or `androidx.foo:foo-bar` are valid names.") - return False - return True - -def get_year(): - return str(date.today().year) - -def get_group_id_version_macro(group_id): - group_id_version_macro = group_id.replace("androidx.", "").replace(".", "_").upper() - if group_id == "androidx.compose": - group_id_version_macro = "COMPOSE" - elif group_id.startswith("androidx.compose"): - group_id_version_macro = group_id.replace("androidx.compose.", "").replace(".", - "_").upper() - return group_id_version_macro - -def sed(before, after, file): - with open(file) as f: - file_contents = f.read() - new_file_contents = file_contents.replace(before, after) - # write back the file - with open(file,"w") as f: - f.write(new_file_contents) - -def remove_line(line_to_remove, file): - with open(file) as f: - file_contents = f.readlines() - new_file_contents = [] - for line in file_contents: - if line_to_remove not in line: - new_file_contents.append(line) - # write back the file - with open(file,"w") as f: - f.write("".join(new_file_contents)) - -def ask_yes_or_no(question): - while(True): - reply = str(input(question+' (y/n): ')).lower().strip() - if reply: - if reply[0] == 'y': return True - if reply[0] == 'n': return False - print("Please respond with y/n") - -def ask_project_type(): - """Asks the user which type of project they wish to create""" - message = dedent(""" - Please choose the type of project you would like to create: - 1: Kotlin (AAR) - 2: Java (AAR / JAR) - 3: Native (AAR) - """).strip() - while(True): - reply = str(input(message + "\n")).strip() - if reply == "1": return ProjectType.KOTLIN - if reply == "2": - if confirm_java_project_type(): - return ProjectType.JAVA - if reply == "3": return ProjectType.NATIVE - print("Please respond with one of the presented options") - -def confirm_java_project_type(): - return ask_yes_or_no("All new androidx projects are expected and encouraged " - "to use Kotlin. Java projects should only be used if " - "there is a business need to do so. " - "Please ack to proceed:") - -def ask_library_purpose(): - question = ("Project description (please complete the sentence): " - "This library makes it easy for developers to... ") - while(True): - reply = str(input(question)).strip() - if reply: return reply - print("Please input a description!") - -def ask_project_description(): - question = ("Please provide a project description: ") - while(True): - reply = str(input(question)).strip() - if reply: return reply - print("Please input a description!") - -def get_gradle_project_coordinates(group_id, artifact_id): - coordinates = group_id.replace("androidx", "").replace(".",":") - coordinates += ":" + artifact_id - return coordinates - -def run_update_api(group_id, artifact_id): - gradle_coordinates = get_gradle_project_coordinates(group_id, artifact_id) - gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew " + gradle_coordinates + ":updateApi" - try: - subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True) - except subprocess.CalledProcessError: - print_e('FAIL: Unable run updateApi with command: %s' % gradle_cmd) - return None - return True - -def get_library_type(artifact_id): - """Returns the appropriate androidx.build.SoftwareType for the project. - """ - if "sample" in artifact_id: - library_type = "SAMPLES" - elif "compiler" in artifact_id: - library_type = "ANNOTATION_PROCESSOR" - elif "lint" in artifact_id: - library_type = "LINT" - elif "inspection" in artifact_id: - library_type = "IDE_PLUGIN" - else: - library_type = "PUBLISHED_LIBRARY" - return library_type - -def get_group_id_path(group_id): - """Generates the group ID filepath - - Given androidx.foo.bar, the structure will be: - frameworks/support/foo/bar - - Args: - group_id: group_id of the new library - """ - return FRAMEWORKS_SUPPORT_FP + "/" + group_id.replace("androidx.", "").replace(".", "/") - -def get_full_artifact_path(group_id, artifact_id): - """Generates the full artifact ID filepath - - Given androidx.foo.bar:bar-qux, the structure will be: - frameworks/support/foo/bar/bar-qux - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - group_id_path = get_group_id_path(group_id) - return group_id_path + "/" + artifact_id - -def get_package_documentation_file_dir(group_id, artifact_id): - """Generates the full package documentation directory - - Given androidx.foo.bar:bar-qux, the structure will be: - frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/package-info.java - - For Kotlin: - frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/--documentation.md - - For Compose: - frameworks/support/foo/bar/bar-qux/src/commonMain/kotlin/androidx/foo/--documentation.md - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - full_artifact_path = get_full_artifact_path(group_id, artifact_id) - if "compose" in group_id: - group_id_subpath = "/src/commonMain/kotlin/" + \ - group_id.replace(".", "/") - else: - group_id_subpath = "/src/main/java/" + \ - group_id.replace(".", "/") - return full_artifact_path + group_id_subpath - -def get_package_documentation_filename(group_id, artifact_id, project_type): - """Generates the documentation filename - - Given androidx.foo.bar:bar-qux, the structure will be: - package-info.java - - or for Kotlin: - --documentation.md - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - is_kotlin_project: whether or not the library is a kotin project - """ - if project_type == ProjectType.JAVA: - return "package-info.java" - else: - formatted_group_id = group_id.replace(".", "-") - return "%s-%s-documentation.md" % (formatted_group_id, artifact_id) - -def is_compose_project(group_id, artifact_id): - """Returns true if project can be inferred to be a compose / Kotlin project - """ - return "compose" in group_id or "compose" in artifact_id - -def create_directories(group_id, artifact_id, project_type, is_compose_project): - """Creates the standard directories for the given group_id and artifact_id. - - Given androidx.foo.bar:bar-qux, the structure will be: - frameworks/support/foo/bar/bar-qux/build.gradle - frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/bar/package-info.java - frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/bar/artifact-documentation.md - frameworks/support/foo/bar/bar-qux/api/current.txt - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - full_artifact_path = get_full_artifact_path(group_id, artifact_id) - if not os.path.exists(full_artifact_path): - os.makedirs(full_artifact_path) - - # Copy over the OWNERS file if it doesn't exit - group_id_path = get_group_id_path(group_id) - if not os.path.exists(group_id_path + "/OWNERS"): - copyfile(SAMPLE_OWNERS_FP, group_id_path + "/OWNERS") - - # Copy the full src structure, depending on the project source code - if is_compose_project: - print("Auto-detected Compose project.") - cp(SAMPLE_COMPOSE_SRC_FP, full_artifact_path) - elif project_type == ProjectType.NATIVE: - cp(NATIVE_SRC_FP, full_artifact_path) - elif project_type == ProjectType.KOTLIN: - cp(SAMPLE_KOTLIN_SRC_FP, full_artifact_path) - else: - cp(SAMPLE_JAVA_SRC_FP, full_artifact_path) - - # Populate the library type - library_type = get_library_type(artifact_id) - - # If it's a sample project, remove the api directory - if library_type == "SAMPLES": - api_dir_path = os.path.join(full_artifact_path, "api") - if os.path.exists(api_dir_path): - rm(api_dir_path) - # Java only libraries have no dependency on android. - # Java-only produces a jar, whereas an android library produces an aar. - if (project_type == ProjectType.JAVA and - (get_library_type(artifact_id) == "LINT" or - ask_yes_or_no("Is this a java-only library? Java-only libraries produce" - " JARs, whereas Android libraries produce AARs."))): - sed("com.android.library", "java-library", - full_artifact_path + "/build.gradle") - sed("org.jetbrains.kotlin.android", "kotlin", - full_artifact_path + "/build.gradle") - - # Atomic group Ids have their version configured automatically, - # so we can remove the version line from the build file. - if is_group_id_atomic(group_id): - remove_line("mavenVersion = LibraryVersions.", - full_artifact_path + "/build.gradle") - - # If the project is a library that produces a jar/aar that will go - # on GMaven, ask for a special project description. - if get_library_type(artifact_id) == "PUBLISHED_LIBRARY": - project_description = ask_library_purpose() - else: - project_description = ask_project_description() - - # Set up the package documentation. - full_package_docs_dir = get_package_documentation_file_dir(group_id, artifact_id) - package_docs_filename = get_package_documentation_filename(group_id, artifact_id, project_type) - full_package_docs_file = os.path.join(full_package_docs_dir, package_docs_filename) - # Compose projects use multiple main directories, so we handle it separately - if is_compose_project: - # Kotlin projects use -documentation.md files, so we need to rename it appropriately. - rename_file(full_artifact_path + "/src/commonMain/kotlin/groupId/artifactId-documentation.md", - package_docs_filename) - mv_dir(full_artifact_path + "/src/commonMain/kotlin/groupId", full_package_docs_dir) - else: - if project_type != ProjectType.JAVA: - # Kotlin projects use -documentation.md files, so we need to rename it appropriately. - # We also rename this file for native projects in case they also have public Kotlin APIs - rename_file(full_artifact_path + "/src/main/java/groupId/artifactId-documentation.md", - package_docs_filename) - mv_dir(full_artifact_path + "/src/main/java/groupId", full_package_docs_dir) - - if project_type == ProjectType.NATIVE and library_type == "PUBLISHED_LIBRARY": - library_type = "PUBLISHED_NATIVE_LIBRARY" - sed("", library_type, full_artifact_path + "/build.gradle") - - # Populate the YEAR - year = get_year() - sed("", year, full_artifact_path + "/build.gradle") - sed("", year, full_package_docs_file) - - # Populate the PACKAGE - package = generate_package_name(group_id, artifact_id) - sed("", package, full_package_docs_file) - sed("", package, full_artifact_path + "/build.gradle") - - # Populate the VERSION macro - group_id_version_macro = get_group_id_version_macro(group_id) - sed("", group_id_version_macro, full_artifact_path + "/build.gradle") - # Update the name and description in the build.gradle - sed("", group_id + ":" + artifact_id, full_artifact_path + "/build.gradle") - if project_type == ProjectType.NATIVE: - sed("", artifact_id, full_artifact_path + "/src/main/cpp/CMakeLists.txt") - sed("", artifact_id, full_artifact_path + "/build.gradle") - create_file(full_artifact_path + "/src/main/cpp/" + artifact_id + ".cpp") - sed("", project_description, full_artifact_path + "/build.gradle") - - -def get_new_settings_gradle_line(group_id, artifact_id): - """Generates the line needed for frameworks/support/settings.gradle. - - For a library androidx.foo.bar:bar-qux, the new gradle command will be - the form: - ./gradlew :foo:bar:bar-qux: - - We special case on compose that we can properly populate the build type - of either MAIN or COMPOSE. - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - - build_type = "MAIN" - if is_compose_project(group_id, artifact_id): - build_type = "COMPOSE" - - gradle_cmd = get_gradle_project_coordinates(group_id, artifact_id) - return "includeProject(\"" + gradle_cmd + "\", [BuildType." + build_type + "])\n" - -def update_settings_gradle(group_id, artifact_id): - """Updates frameworks/support/settings.gradle with the new library. - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - # Open file for reading and get all lines - with open(SETTINGS_GRADLE_FP, 'r') as f: - settings_gradle_lines = f.readlines() - num_lines = len(settings_gradle_lines) - - new_settings_gradle_line = get_new_settings_gradle_line(group_id, artifact_id) - for i in range(num_lines): - cur_line = settings_gradle_lines[i] - if "includeProject" not in cur_line: - continue - # Iterate through until you found the alphabetical place to insert the new line - if new_settings_gradle_line <= cur_line: - insert_line = i - break - else: - insert_line = i + 1 - settings_gradle_lines.insert(insert_line, new_settings_gradle_line) - - # Open file for writing and update all lines - with open(SETTINGS_GRADLE_FP, 'w') as f: - f.writelines(settings_gradle_lines) - -def get_new_docs_tip_of_tree_build_grade_line(group_id, artifact_id): - """Generates the line needed for docs-tip-of-tree/build.gradle. - - For a library androidx.foo.bar:bar-qux, the new line will be of the form: - docs(project(":foo:bar:bar-qux")) - - If it is a sample project, then it will return None. samples(project(":foo:bar:bar-qux-sample")) needs to be added to the androidx block of the library build.gradle file. - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - - gradle_cmd = get_gradle_project_coordinates(group_id, artifact_id) - prefix = "docs" - if "sample" in gradle_cmd: - print("Auto-detected sample project. Please add the sample dependency to androidx block of the library build.gradle file. See compose/ui/ui/build.gradle for an example.") - return None - return " %s(project(\"%s\"))\n" % (prefix, gradle_cmd) - -def update_docs_tip_of_tree_build_grade(group_id, artifact_id): - """Updates docs-tip-of-tree/build.gradle with the new library. - - We ask for confirmation if the library contains either "benchmark" - or "test". - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - # Confirm with user that we want to generate docs for anything - # that might be a test or a benchmark. - if ("test" in group_id or "test" in artifact_id - or "benchmark" in group_id or "benchmark" in artifact_id): - if not ask_yes_or_no(("Should tip-of-tree documentation be generated " - "for project %s:%s?" % (group_id, artifact_id))): - return - - # Open file for reading and get all lines - with open(DOCS_TOT_BUILD_GRADLE_FP, 'r') as f: - docs_tot_bg_lines = f.readlines() - index_of_real_dependencies_block = next( - idx for idx, line in enumerate(docs_tot_bg_lines) if line.startswith("dependencies {") - ) - if (index_of_real_dependencies_block == None): - raise RuntimeError("Couldn't find dependencies block") - num_lines = len(docs_tot_bg_lines) - - new_docs_tot_bq_line = get_new_docs_tip_of_tree_build_grade_line(group_id, artifact_id) - for i in range(index_of_real_dependencies_block, num_lines): - cur_line = docs_tot_bg_lines[i] - if "project" not in cur_line: - continue - if new_docs_tot_bq_line == None: - return - # Iterate through until you found the alphabetical place to insert the new line - if new_docs_tot_bq_line.split("project")[1] <= cur_line.split("project")[1]: - insert_line = i - break - else: - insert_line = i + 1 - docs_tot_bg_lines.insert(insert_line, new_docs_tot_bq_line) - - # Open file for writing and update all lines - with open(DOCS_TOT_BUILD_GRADLE_FP, 'w') as f: - f.writelines(docs_tot_bg_lines) - - -def insert_new_group_id_into_library_versions_toml(group_id): - """Inserts a group ID into the libraryversions.toml file. - - If one already exists, then this function just returns and reuses - the existing one. - - Args: - group_id: group_id of the new library - """ - new_group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper() - - # Open toml file - library_versions = toml.load(LIBRARY_VERSIONS_FP, decoder=toml.TomlPreserveCommentDecoder()) - if not new_group_id_variable_name in library_versions["versions"]: - library_versions["versions"][new_group_id_variable_name] = "1.0.0-alpha01" - if not new_group_id_variable_name in library_versions["groups"]: - decoder = toml.decoder.TomlDecoder() - group_entry = decoder.get_empty_inline_table() - group_entry["group"] = group_id - group_entry["atomicGroupVersion"] = "versions." + new_group_id_variable_name - library_versions["groups"][new_group_id_variable_name] = group_entry - - # Sort the entries - library_versions["versions"] = dict(sorted(library_versions["versions"].items())) - library_versions["groups"] = dict(sorted(library_versions["groups"].items())) - - # Open file for writing and update toml - with open(LIBRARY_VERSIONS_FP, 'w') as f: - # Encoder arg enables preservation of inline dicts. - versions_toml_file_string = toml.dumps(library_versions, - encoder=toml.TomlPreserveCommentEncoder(preserve=True)) - versions_toml_file_string_new = re.sub(",]", " ]", versions_toml_file_string) - versions_toml_file_string_new - f.write(versions_toml_file_string_new) - - -def is_group_id_atomic(group_id): - """Checks if a group ID is atomic using the libraryversions.toml file. - - If one already exists, then this function evaluates the group id - and returns the appropriate atomicity. Otherwise, it returns - False. - - Example of an atomic library group: - ACTIVITY = { group = "androidx.work", atomicGroupVersion = "WORK" } - Example of a non-atomic library group: - WEAR = { group = "androidx.wear" } - - Args: - group_id: group_id of the library we're checking. - """ - library_versions = toml.load(LIBRARY_VERSIONS_FP) - for library_group in library_versions["groups"]: - if group_id == library_versions["groups"][library_group]["group"]: - return "atomicGroupVersion" in library_versions["groups"][library_group] - - return False - - -def print_todo_list(group_id, artifact_id, project_type): - """Prints to the todo list once the script has finished. - - There are some pieces that can not be automated or require human eyes. - List out the appropriate todos so that the users knows what needs - to be done prior to uploading. - - Args: - group_id: group_id of the new library - artifact_id: group_id of the new library - """ - build_gradle_path = get_full_artifact_path(group_id, artifact_id) + \ - "/build.gradle" - owners_file_path = get_group_id_path(group_id) + "/OWNERS" - package_docs_path = os.path.join( - get_package_documentation_file_dir(group_id, artifact_id), - get_package_documentation_filename(group_id, artifact_id, project_type)) - print("---\n") - print("Created the project. The following TODOs need to be completed by " - "you:\n") - print("\t1. Check that the OWNERS file is in the correct place. It is " - "currently at:" - "\n\t\t" + owners_file_path) - print("\t2. Add your name (and others) to the OWNERS file:" + \ - "\n\t\t" + owners_file_path) - print("\t3. Check that the correct library version is assigned in the " - "build.gradle:" - "\n\t\t" + build_gradle_path) - print("\t4. Fill out the project/module name in the build.gradle:" - "\n\t\t" + build_gradle_path) - print("\t5. Update the project/module package documentation:" - "\n\t\t" + package_docs_path) - -def main(args): - # Parse arguments and check for existence of build ID or file - args = parser.parse_args() - if not args.group_id or not args.artifact_id: - parser.error("You must specify a group_id and an artifact_id") - sys.exit(1) - if not validate_name(args.group_id, args.artifact_id): - sys.exit(1) - if is_compose_project(args.group_id, args.artifact_id): - project_type = ProjectType.KOTLIN - else: - project_type = ask_project_type() - insert_new_group_id_into_library_versions_toml( - args.group_id - ) - create_directories( - args.group_id, - args.artifact_id, - project_type, - is_compose_project(args.group_id, args.artifact_id) - ) - update_settings_gradle(args.group_id, args.artifact_id) - update_docs_tip_of_tree_build_grade(args.group_id, args.artifact_id) - print("Created directories. \nRunning updateApi for the new " - "library, this may take a minute...", end='') - if run_update_api(args.group_id, args.artifact_id): - print("done.") - else: - print("failed. Please investigate manually.") - print_todo_list(args.group_id, args.artifact_id, project_type) - -if __name__ == '__main__': - main(sys.argv) diff --git a/development/project-creator/create_project.sh b/development/project-creator/create_project.sh deleted file mode 100755 index d976d21a4f70b..0000000000000 --- a/development/project-creator/create_project.sh +++ /dev/null @@ -1,27 +0,0 @@ -SCRIPT_DIR="$(cd $(dirname $0) && pwd)" - -if [ "$(uname)" = "Darwin" ]; then - VIRTUAL_ENV_INSTALL_COMMAND="pip3 install --require-hashes -r base-requirements.txt" - else - VIRTUAL_ENV_INSTALL_COMMAND="sudo apt-get install virtualenv python3-venv" -fi - - -# check if virtualenv is installed -if !(pyenv_version=$(virtualenv --version > /dev/null 2>&1)); then - echo "virtualenv is not installed. Please install with '$VIRTUAL_ENV_INSTALL_COMMAND'" - exit 1 -fi - -# create virtualenv -virtualenv androidx_project_creator - -# install necessary tools -androidx_project_creator/bin/pip3 install --require-hashes -r $SCRIPT_DIR/requirements.txt - -# run project creator -androidx_project_creator/bin/python3 $SCRIPT_DIR/create_project.py "$@" - - -# clean up virtualenv directory -rm -rf ./androidx-project_creator diff --git a/development/project-creator/java-template/groupId/OWNERS b/development/project-creator/java-template/groupId/OWNERS deleted file mode 100644 index 15331e5f84a15..0000000000000 --- a/development/project-creator/java-template/groupId/OWNERS +++ /dev/null @@ -1 +0,0 @@ -# example@google.com diff --git a/development/project-creator/java-template/groupId/artifactId/api/current.txt b/development/project-creator/java-template/groupId/artifactId/api/current.txt deleted file mode 100644 index e6f50d0d0fd11..0000000000000 --- a/development/project-creator/java-template/groupId/artifactId/api/current.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 4.0 diff --git a/development/project-creator/java-template/groupId/artifactId/build.gradle b/development/project-creator/java-template/groupId/artifactId/build.gradle deleted file mode 100644 index 975c2293180c7..0000000000000 --- a/development/project-creator/java-template/groupId/artifactId/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 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. - */ - -/** - * This file was created using the `create_project.py` script located in the - * `/development/project-creator` directory. - * - * Please use that script when creating a new project, rather than copying an existing project and - * modifying its settings. - */ -import androidx.build.SoftwareType - -plugins { - id("AndroidXPlugin") - id("com.android.library") -} - -dependencies { - annotationProcessor(libs.nullaway) - // Add dependencies here -} - -android { - namespace = "" -} - -androidx { - name = "" - type = SoftwareType. - mavenVersion = LibraryVersions. - inceptionYear = "" - description = "" -} diff --git a/development/project-creator/java-template/groupId/artifactId/src/main/java/groupId/package-info.java b/development/project-creator/java-template/groupId/artifactId/src/main/java/groupId/package-info.java deleted file mode 100644 index a51e68c20c760..0000000000000 --- a/development/project-creator/java-template/groupId/artifactId/src/main/java/groupId/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 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. - */ - -/** - * Insert package level documentation here - */ -package ; diff --git a/development/project-creator/kotlin-template/groupId/OWNERS b/development/project-creator/kotlin-template/groupId/OWNERS deleted file mode 100644 index 15331e5f84a15..0000000000000 --- a/development/project-creator/kotlin-template/groupId/OWNERS +++ /dev/null @@ -1 +0,0 @@ -# example@google.com diff --git a/development/project-creator/kotlin-template/groupId/artifactId/api/current.txt b/development/project-creator/kotlin-template/groupId/artifactId/api/current.txt deleted file mode 100644 index e6f50d0d0fd11..0000000000000 --- a/development/project-creator/kotlin-template/groupId/artifactId/api/current.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 4.0 diff --git a/development/project-creator/kotlin-template/groupId/artifactId/build.gradle b/development/project-creator/kotlin-template/groupId/artifactId/build.gradle deleted file mode 100644 index 56b830eeb1a50..0000000000000 --- a/development/project-creator/kotlin-template/groupId/artifactId/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 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. - */ - -/** - * This file was created using the `create_project.py` script located in the - * `/development/project-creator` directory. - * - * Please use that script when creating a new project, rather than copying an existing project and - * modifying its settings. - */ -import androidx.build.SoftwareType - -plugins { - id("AndroidXPlugin") - id("com.android.library") -} - -dependencies { - // Add dependencies here -} - -android { - namespace = "" -} - -androidx { - name = "" - type = SoftwareType. - mavenVersion = LibraryVersions. - inceptionYear = "" - description = "" -} diff --git a/development/project-creator/kotlin-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md b/development/project-creator/kotlin-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md deleted file mode 100644 index a37b120e17182..0000000000000 --- a/development/project-creator/kotlin-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md +++ /dev/null @@ -1,7 +0,0 @@ -# Module root - - - -# Package - -Insert package level documentation here diff --git a/development/project-creator/native-template/groupId/OWNERS b/development/project-creator/native-template/groupId/OWNERS deleted file mode 100644 index 15331e5f84a15..0000000000000 --- a/development/project-creator/native-template/groupId/OWNERS +++ /dev/null @@ -1 +0,0 @@ -# example@google.com diff --git a/development/project-creator/native-template/groupId/artifactId/api/current.txt b/development/project-creator/native-template/groupId/artifactId/api/current.txt deleted file mode 100644 index e6f50d0d0fd11..0000000000000 --- a/development/project-creator/native-template/groupId/artifactId/api/current.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 4.0 diff --git a/development/project-creator/native-template/groupId/artifactId/build.gradle b/development/project-creator/native-template/groupId/artifactId/build.gradle deleted file mode 100644 index a118661d01917..0000000000000 --- a/development/project-creator/native-template/groupId/artifactId/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 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. - */ - -/** - * This file was created using the `create_project.py` script located in the - * `/development/project-creator` directory. - * - * Please use that script when creating a new project, rather than copying an existing project and - * modifying its settings. - */ -import androidx.build.SoftwareType - -plugins { - id("AndroidXPlugin") - id("com.android.library") -} - -dependencies { - // Add dependencies here -} - -androidx { - name = "" - type = SoftwareType. - mavenVersion = LibraryVersions. - inceptionYear = "" - description = "" -} - -android { - namespace = "" - defaultConfig { - externalNativeBuild { - cmake { - arguments "-DANDROID_STL=c++_shared" - targets "" - } - } - } - externalNativeBuild { - cmake { - version = libs.versions.cmake.get() - path "src/main/cpp/CMakeLists.txt" - } - } - - buildFeatures { - prefabPublishing true - } - - prefab { - { - headers "src/main/cpp/include" - } - } -} \ No newline at end of file diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt deleted file mode 100644 index c1ae2b9ddf136..0000000000000 --- a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.22.1) - -project( LANGUAGES CXX) - -add_library( - SHARED - .cpp) - -set_property(TARGET - APPEND_STRING PROPERTY - LINK_FLAGS - " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_scripts/libname.map.txt") - -target_link_options( PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/include/.keep b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/include/.keep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/version_scripts/libname.map.txt b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/version_scripts/libname.map.txt deleted file mode 100644 index fe39d9e8e1ef2..0000000000000 --- a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/version_scripts/libname.map.txt +++ /dev/null @@ -1,4 +0,0 @@ -LIBNAME { - local: - *; -}; \ No newline at end of file diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md b/development/project-creator/native-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md deleted file mode 100644 index a37b120e17182..0000000000000 --- a/development/project-creator/native-template/groupId/artifactId/src/main/java/groupId/artifactId-documentation.md +++ /dev/null @@ -1,7 +0,0 @@ -# Module root - - - -# Package - -Insert package level documentation here diff --git a/development/project-creator/requirements.txt b/development/project-creator/requirements.txt deleted file mode 100644 index bfdc60517646d..0000000000000 --- a/development/project-creator/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools==78.1.1 --hash=sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561 -toml==0.10.2 --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b diff --git a/development/project-creator/test_project_creator.py b/development/project-creator/test_project_creator.py deleted file mode 100755 index bcc62bb2137dc..0000000000000 --- a/development/project-creator/test_project_creator.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import unittest -import os -from create_project import * - -class TestNewDirectory(unittest.TestCase): - - def test_package_name(self): - package = generate_package_name("androidx.foo", "foo") - self.assertEqual("androidx.foo", package) - - package = generate_package_name("androidx.foo", "foo-bar") - self.assertEqual("androidx.foo.bar", package) - - package = generate_package_name("androidx.foo.bar", "bar") - self.assertEqual("androidx.foo.bar", package) - - package = generate_package_name("androidx.foo.bar", "bar-qux") - self.assertEqual("androidx.foo.bar.qux", package) - - def test_name_correctness(self): - self.assertFalse(validate_name("foo", "bar")) - self.assertFalse(validate_name("foo", "foo")) - self.assertFalse(validate_name("androidx.foo", "bar")) - self.assertFalse(validate_name("androidx.foo", "bar-qux")) - self.assertFalse(validate_name("androidx.foo.bar", "foo")) - self.assertTrue(validate_name("androidx.foo", "foo")) - self.assertTrue(validate_name("androidx.foo", "foo-bar")) - self.assertTrue(validate_name("androidx.foo.bar", "bar")) - self.assertTrue(validate_name("androidx.foo.bar", "bar-qux")) - - def test_full_directory_name(self): - full_fp = get_full_artifact_path("androidx.foo", "foo") - self.assertTrue(full_fp.endswith("frameworks/support/foo/foo")) - - full_fp = get_full_artifact_path("androidx.foo", "foo-bar") - self.assertTrue(full_fp.endswith("frameworks/support/foo/foo-bar")) - - full_fp = get_full_artifact_path("androidx.foo.bar", "bar") - self.assertTrue(full_fp.endswith("frameworks/support/foo/bar/bar")) - - full_fp = get_full_artifact_path("androidx.foo.bar", "bar-qux") - self.assertTrue(full_fp.endswith("frameworks/support/foo/bar/bar-qux")) - - def test_get_package_documentation_file_dir(self): - package_info_dir_fp = get_package_documentation_file_dir("androidx.foo", "foo") - frameworks_support_fp = os.path.abspath(os.path.join(os.getcwd(), '..', '..')) - self.assertEqual(frameworks_support_fp + "/foo/foo/src/main/java/androidx/foo", package_info_dir_fp) - - package_info_dir_fp = get_package_documentation_file_dir("androidx.foo", "foo-bar") - self.assertEqual(frameworks_support_fp + "/foo/foo-bar/src/main/java/androidx/foo", package_info_dir_fp) - - package_info_dir_fp = get_package_documentation_file_dir("androidx.foo.bar", "bar") - self.assertEqual(frameworks_support_fp + "/foo/bar/bar/src/main/java/androidx/foo/bar", package_info_dir_fp) - - package_info_dir_fp = get_package_documentation_file_dir("androidx.foo.bar", "bar-qux") - self.assertEqual(frameworks_support_fp + "/foo/bar/bar-qux/src/main/java/androidx/foo/bar", package_info_dir_fp) - - package_info_dir_fp = get_package_documentation_file_dir("androidx.compose.bar", "bar-qux") - self.assertEqual(frameworks_support_fp + "/compose/bar/bar-qux/src/commonMain/kotlin/androidx/compose/bar", - package_info_dir_fp) - - package_info_dir_fp = get_package_documentation_file_dir("androidx.foo.compose", "compose-qux") - self.assertEqual(frameworks_support_fp + "/foo/compose/compose-qux/src/commonMain/kotlin/androidx/foo/compose", - package_info_dir_fp) - - def test_get_package_documentation_filename(self): - frameworks_support_fp = os.path.abspath(os.path.join(os.getcwd(), '..', '..')) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo", "foo", ProjectType.KOTLIN) - self.assertEqual("androidx-foo-foo-documentation.md", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo", "foo", ProjectType.NATIVE) - self.assertEqual("androidx-foo-foo-documentation.md", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo", "foo-bar", ProjectType.KOTLIN) - self.assertEqual("androidx-foo-foo-bar-documentation.md", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo.bar", "bar", ProjectType.KOTLIN) - self.assertEqual("androidx-foo-bar-bar-documentation.md", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo.bar", "bar-qux", ProjectType.KOTLIN) - self.assertEqual("androidx-foo-bar-bar-qux-documentation.md", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo", "foo", ProjectType.JAVA) - self.assertEqual("package-info.java", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo", "foo-bar", ProjectType.JAVA) - self.assertEqual("package-info.java", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo.bar", "bar", ProjectType.JAVA) - self.assertEqual("package-info.java", package_info_dir_filename) - - package_info_dir_filename = get_package_documentation_filename("androidx.foo.bar", "bar-qux", ProjectType.JAVA) - self.assertEqual("package-info.java", package_info_dir_filename) - - def test_group_id_directory_name(self): - full_fp = get_group_id_path("androidx.foo") - self.assertTrue(full_fp.endswith("frameworks/support/foo")) - - full_fp = get_group_id_path("androidx.foo") - self.assertTrue(full_fp.endswith("frameworks/support/foo")) - - full_fp = get_group_id_path("androidx.foo.bar") - self.assertTrue(full_fp.endswith("frameworks/support/foo/bar")) - - full_fp = get_group_id_path("androidx.foo.bar") - self.assertTrue(full_fp.endswith("frameworks/support/foo/bar")) - - -class TestSettingsGradle(unittest.TestCase): - - def test_settings_gradle_line(self): - line = get_new_settings_gradle_line("androidx.foo", "foo") - self.assertEqual("includeProject(\":foo:foo\", [BuildType.MAIN])\n", line) - - line = get_new_settings_gradle_line("androidx.foo", "foo-bar") - self.assertEqual("includeProject(\":foo:foo-bar\", [BuildType.MAIN])\n", line) - - line = get_new_settings_gradle_line("androidx.foo.bar", "bar") - self.assertEqual("includeProject(\":foo:bar:bar\", [BuildType.MAIN])\n", line) - - line = get_new_settings_gradle_line("androidx.foo.bar", "bar-qux") - self.assertEqual("includeProject(\":foo:bar:bar-qux\", [BuildType.MAIN])\n", line) - - line = get_new_settings_gradle_line("androidx.compose", "compose-foo") - self.assertEqual("includeProject(\":compose:compose-foo\", [BuildType.COMPOSE])\n", line) - - line = get_new_settings_gradle_line("androidx.compose.foo", "foo-bar") - self.assertEqual("includeProject(\":compose:foo:foo-bar\", [BuildType.COMPOSE])\n", line) - - line = get_new_settings_gradle_line("androidx.foo.bar", "bar-compose") - self.assertEqual("includeProject(\":foo:bar:bar-compose\", [BuildType.COMPOSE])\n", line) - - def test_gradle_project_coordinates(self): - coordinates = get_gradle_project_coordinates("androidx.foo", "foo") - self.assertEqual(":foo:foo", coordinates) - - coordinates = get_gradle_project_coordinates("androidx.foo", "foo-bar") - self.assertEqual(":foo:foo-bar", coordinates) - - coordinates = get_gradle_project_coordinates("androidx.foo.bar", "bar") - self.assertEqual(":foo:bar:bar", coordinates) - - coordinates = get_gradle_project_coordinates("androidx.foo.bar", "bar-qux") - self.assertEqual(":foo:bar:bar-qux", coordinates) - -class TestBuildGradle(unittest.TestCase): - def test_correct_library_type_is_returned(self): - library_type = get_library_type("foo-samples") - self.assertEqual("SAMPLES", library_type) - - library_type = get_library_type("foo-compiler") - self.assertEqual("ANNOTATION_PROCESSOR", library_type) - - library_type = get_library_type("foo-lint") - self.assertEqual("LINT", library_type) - - library_type = get_library_type("foo-inspection") - self.assertEqual("IDE_PLUGIN", library_type) - - library_type = get_library_type("foo") - self.assertEqual("PUBLISHED_LIBRARY", library_type) - - library_type = get_library_type("foo-inspect") - self.assertEqual("PUBLISHED_LIBRARY", library_type) - - library_type = get_library_type("foocomp") - self.assertEqual("PUBLISHED_LIBRARY", library_type) - - library_type = get_library_type("foo-bar") - self.assertEqual("PUBLISHED_LIBRARY", library_type) - - -class TestDocsTipOfTree(unittest.TestCase): - - def test_docs_tip_of_tree_build_grade_line(self): - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo", "foo") - self.assertEqual(" docs(project(\":foo:foo\"))\n", line) - - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo", "foo-bar") - self.assertEqual(" docs(project(\":foo:foo-bar\"))\n", line) - - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo.bar", "bar") - self.assertEqual(" docs(project(\":foo:bar:bar\"))\n", line) - - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo.bar", "bar-qux") - self.assertEqual(" docs(project(\":foo:bar:bar-qux\"))\n", line) - - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo", "foo-samples") - self.assertIsNone(line) - - line = get_new_docs_tip_of_tree_build_grade_line("androidx.foo.bar", "bar-qux-samples") - self.assertIsNone(line) - -class TestReplacements(unittest.TestCase): - - def test_version_macro(self): - macro = get_group_id_version_macro("androidx.foo") - self.assertEqual("FOO", macro) - - macro = get_group_id_version_macro("androidx.foo.bar") - self.assertEqual("FOO_BAR", macro) - - macro = get_group_id_version_macro("androidx.compose.bar") - self.assertEqual("BAR", macro) - - macro = get_group_id_version_macro("androidx.compose.foo.bar") - self.assertEqual("FOO_BAR", macro) - - macro = get_group_id_version_macro("androidx.compose") - self.assertEqual("COMPOSE", macro) - - def test_sed(self): - out_dir = "./out" - test_file = out_dir + "/temp.txt" - test_file_contents = "a\nb\nc" - if not os.path.exists(out_dir): - os.makedirs(out_dir) - with open(test_file,"w") as f: - f.write("a\nb\nc") - sed("a", "d", test_file) - - # write back the file - with open(test_file) as f: - file_contents = f.read() - self.assertEqual("d\nb\nc", file_contents) - rm(out_dir) - - def test_mv_dir_within_same_dir(self): - src_out_dir = "./src_out" - test_src_file = src_out_dir + "/temp.txt" - test_file_contents = "a\nb\nc" - if not os.path.exists(src_out_dir): - os.makedirs(src_out_dir) - with open(test_src_file,"w") as f: - f.write("a\nb\nc") - - dst_out_dir = "./dst_out" - mv_dir(src_out_dir, dst_out_dir) - # write back the file - with open(dst_out_dir + "/temp.txt") as f: - file_contents = f.read() - self.assertEqual("a\nb\nc", file_contents) - rm(src_out_dir) - rm(dst_out_dir) - - def test_mv_dir_to_different_dir(self): - src_out_dir = "./src_out_2" - test_src_file = src_out_dir + "/temp.txt" - test_file_contents = "a\nb\nc" - if not os.path.exists(src_out_dir): - os.makedirs(src_out_dir) - with open(test_src_file,"w") as f: - f.write("a\nb\nc") - - dst_out_dir_parent = "./dst_out_2" - dst_out_dir = dst_out_dir_parent + "/hello/world" - mv_dir(src_out_dir, dst_out_dir) - # write back the file - with open(dst_out_dir + "/temp.txt") as f: - file_contents = f.read() - self.assertEqual("a\nb\nc", file_contents) - rm(src_out_dir) - rm(dst_out_dir_parent) - - def test_rename_file_within_same_dir(self): - test_src_file = "./temp.txt" - test_file_contents = "a\nb\nc" - with open(test_src_file,"w") as f: - f.write("a\nb\nc") - - test_dst_file = "./temp_out.txt" - rename_file(test_src_file, test_dst_file) - # read back the file - with open(test_dst_file) as f: - file_contents = f.read() - self.assertEqual("a\nb\nc", file_contents) - rm(test_dst_file) - - def test_remove_line(self): - out_dir = "./out" - test_file = out_dir + "/temp.txt" - test_file_contents = "a\nb\nc" - if not os.path.exists(out_dir): - os.makedirs(out_dir) - - with open(test_file,"w") as f: - f.write("a\nb\nc") - remove_line("b", test_file) - # read back the file and check - with open(test_file) as f: - file_contents = f.read() - self.assertEqual("a\nc", file_contents) - - with open(test_file,"w") as f: - f.write("abc\ndef\nghi") - remove_line("c", test_file) - # read back the file and check - with open(test_file) as f: - file_contents = f.read() - self.assertEqual("def\nghi", file_contents) - - # Clean up - rm(out_dir) - - -class TestLibraryGroupKt(unittest.TestCase): - - def test_library_group_atomicity_is_correctly_determined(self): - self.assertFalse(is_group_id_atomic("androidx.core")) - self.assertFalse(is_group_id_atomic("androidx.foo")) - self.assertFalse(is_group_id_atomic("")) - self.assertFalse(is_group_id_atomic("androidx.compose.foo")) - self.assertTrue(is_group_id_atomic("androidx.cardview")) - self.assertTrue(is_group_id_atomic("androidx.tracing")) - self.assertTrue(is_group_id_atomic("androidx.compose.foundation")) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From 3e2fdafd0e3ccc9bbb7484e73df6b627b70f70a3 Mon Sep 17 00:00:00 2001 From: Neda Topoljanac Date: Fri, 16 Jan 2026 10:53:16 -0800 Subject: [PATCH 06/10] Bump versions for ProtoLayout, Tiles, Glance Wear and RemoteCompose for upcoming release Change-Id: Id0382e9e3b6f4228b5df1fbdd607f06f6bb59e67 --- libraryversions.toml | 8 ++++---- wear/tiles/tiles-renderer/lint-baseline.xml | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraryversions.toml b/libraryversions.toml index d848ceb0bb6e3..6adcdea80e10c 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -70,7 +70,7 @@ EXIFINTERFACE = "1.4.0-rc01" FRAGMENT = "1.9.0-alpha01" FUTURES = "1.3.0-rc01" GLANCE = "1.3.0-alpha01" -GLANCE_WEAR = "1.0.0-alpha01" +GLANCE_WEAR = "1.0.0-alpha02" GLANCE_WEAR_TILES = "1.0.0-alpha07" GRAPHICS_CORE = "1.0.0" GRAPHICS_FILTERS = "1.0.0-alpha01" @@ -119,7 +119,7 @@ RECOMMENDATION = "1.1.0-alpha01" RECYCLERVIEW = "1.5.0-alpha01" RECYCLERVIEW_SELECTION = "1.3.0-alpha01" REMOTECALLBACK = "1.0.0-alpha03" -REMOTECOMPOSE = "1.0.0-alpha02" +REMOTECOMPOSE = "1.0.0-alpha03" REMOTECOMPOSE_WEAR = "1.0.0-alpha01" RESOURCEINSPECTION = "1.1.0-alpha01" ROOM3 = "3.0.0-alpha01" @@ -167,9 +167,9 @@ WEAR_INPUT = "1.2.0-rc01" WEAR_INPUT_TESTING = "1.2.0-rc01" WEAR_ONGOING = "1.1.0-rc01" WEAR_PHONE_INTERACTIONS = "1.1.0-rc01" -WEAR_PROTOLAYOUT = "1.4.0-alpha04" +WEAR_PROTOLAYOUT = "1.4.0-alpha05" WEAR_REMOTE_INTERACTIONS = "1.2.0-rc01" -WEAR_TILES = "1.6.0-alpha04" +WEAR_TILES = "1.6.0-alpha05" WEAR_TOOLING_PREVIEW = "1.0.0-rc01" WEAR_WATCHFACE = "1.3.0-rc01" WEAR_WATCHFACEPUSH = "1.0.0-rc01" diff --git a/wear/tiles/tiles-renderer/lint-baseline.xml b/wear/tiles/tiles-renderer/lint-baseline.xml index 46c9dafd1ef4f..2090255b36d74 100644 --- a/wear/tiles/tiles-renderer/lint-baseline.xml +++ b/wear/tiles/tiles-renderer/lint-baseline.xml @@ -777,7 +777,7 @@ Date: Fri, 16 Jan 2026 15:21:32 -0500 Subject: [PATCH 07/10] Reenable non-jvm tests for ksp runner These work now that max workers is set to 2. Bug: 469741876 Test: tested on github fork - https://github.com/juliamcclellan/androidx/actions/runs/21077090046 Change-Id: I57c510d0a4e58857b507980370681ab304ad18ec --- .github/workflows/ksp-snapshot-integration.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ksp-snapshot-integration.yml b/.github/workflows/ksp-snapshot-integration.yml index 160674e1e6796..e11890ad23c85 100644 --- a/.github/workflows/ksp-snapshot-integration.yml +++ b/.github/workflows/ksp-snapshot-integration.yml @@ -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 @@ -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 From 4c8c86c6fb23f733de0bbd2ed30251fef673236d Mon Sep 17 00:00:00 2001 From: Tiffany Lu Date: Thu, 15 Jan 2026 19:04:26 +0000 Subject: [PATCH 08/10] Update FrameState to use ListenerState to track the status of callbacks 1. Track list of ListenerState instead of Listeners 2. Move FrameState updates to the beginning of the functions before firing listener callbacks Bug: 473901206 Test: FrameStateTest Change-Id: Ie96a5efa0420fed5643ee96a5436647f1a7568bb --- .../camera2/pipe/internal/FrameState.kt | 54 +++--- .../camera2/pipe/internal/ListenerState.kt | 10 ++ .../camera2/pipe/internal/FrameStateTest.kt | 156 ++++++++++++++++++ 3 files changed, 197 insertions(+), 23 deletions(-) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt index ca569fbc0b651..a076c1c9ac299 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt @@ -78,21 +78,28 @@ internal class FrameState( private val state = atomic(STARTED) private val streamResultCount = atomic(0) - private val outputFrameListeners = CopyOnWriteArrayList() + // A list of ListenerState, one for each listener. + private val listenerStates = CopyOnWriteArrayList() 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) { @@ -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 -> @@ -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) } } diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/ListenerState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/ListenerState.kt index 55458fa9236c9..585a9366b247e 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/ListenerState.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/ListenerState.kt @@ -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) { @@ -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) + } } diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt index cf3ac579430d4..6213d773d9e3b 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt @@ -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 @@ -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 @@ -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, @@ -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) + } } From a5c782d48e066b3dbc24b05a9bfc88239a95dac8 Mon Sep 17 00:00:00 2001 From: Egor Yusov Date: Fri, 16 Jan 2026 15:36:24 -0800 Subject: [PATCH 09/10] Add MediaBlendingMode to ImpressApi Adds MediaBlendingMode annotation to ImpressApi with TRANSLUCENT and OPAQUE values. Updates FakeImpressApiImplTest to verify usage. BUG: 437956549 Change-Id: I0d484658600c150a9243e4399d3ba1695ec221c4 --- .../impl/impress/FakeImpressApiImpl.kt | 17 ++++++++ .../xr/scenecore/impl/impress/ImpressApi.kt | 38 +++++++++++++++++ .../scenecore/impl/impress/ImpressApiImpl.kt | 42 ++++++++++++------- .../impl/impress/FakeImpressApiImplTest.kt | 20 +++++++++ 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImpl.kt b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImpl.kt index 7bc45f8bb9ae6..2b4709c007dea 100644 --- a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImpl.kt +++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImpl.kt @@ -27,6 +27,7 @@ import androidx.xr.scenecore.impl.impress.ImpressApi.ColorRange import androidx.xr.scenecore.impl.impress.ImpressApi.ColorSpace import androidx.xr.scenecore.impl.impress.ImpressApi.ColorTransfer import androidx.xr.scenecore.impl.impress.ImpressApi.ContentSecurityLevel +import androidx.xr.scenecore.impl.impress.ImpressApi.MediaBlendingMode import androidx.xr.scenecore.impl.impress.ImpressApi.StereoMode import androidx.xr.scenecore.runtime.KhronosPbrMaterialSpec import androidx.xr.scenecore.runtime.TextureSampler @@ -53,6 +54,7 @@ public class FakeImpressApiImpl : ImpressApi { public var surface: Surface? = null, public var useSuperSampling: Boolean = false, @StereoMode public var stereoMode: Int = 0, + @MediaBlendingMode public var mediaBlendingMode: Int = 0, public var width: Float = 0f, public var height: Float = 0f, public var radius: Float = 0f, @@ -352,6 +354,20 @@ public class FakeImpressApiImpl : ImpressApi { @StereoMode stereoMode: Int, @ContentSecurityLevel contentSecurityLevel: Int, useSuperSampling: Boolean, + ): ImpressNode { + return createStereoSurface( + stereoMode, + MediaBlendingMode.TRANSPARENT, + contentSecurityLevel, + useSuperSampling, + ) + } + + override fun createStereoSurface( + @StereoMode stereoMode: Int, + @MediaBlendingMode mediaBlendingMode: Int, + @ContentSecurityLevel contentSecurityLevel: Int, + useSuperSampling: Boolean, ): ImpressNode { val impressNode: ImpressNode = createImpressNode() val data = @@ -360,6 +376,7 @@ public class FakeImpressApiImpl : ImpressApi { surface = TestSurface(impressNode.handle), useSuperSampling = useSuperSampling, stereoMode = stereoMode, + mediaBlendingMode = mediaBlendingMode, canvasShape = null, ) stereoSurfaceEntities[data.impressNode] = data diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApi.kt b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApi.kt index defe3751c1496..fa6221fde1d20 100644 --- a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApi.kt +++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApi.kt @@ -77,6 +77,22 @@ public interface ImpressApi { } } + /** + * Specifies the blending mode of the content. + * + * Values here match values from imp::MediaBlendingMode. + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef(MediaBlendingMode.OPAQUE, MediaBlendingMode.TRANSPARENT) + public annotation class MediaBlendingMode { + public companion object { + // Content is alpha-blended with the background. + public const val TRANSPARENT: Int = 0 + // Content is opaque and does not blend with the background. + public const val OPAQUE: Int = 1 + } + } + /** * Specifies the color standard of the content. * @@ -344,6 +360,28 @@ public interface ImpressApi { useSuperSampling: Boolean, ): ImpressNode + /** + * This method creates an Impress node with a stereo panel and returns the node object. Note + * that the StereoSurfaceEntity will not be render anything until the canvas shape is set. + * + * @param stereoMode The [Int] stereoMode to apply. Must be a member of StereoMode. + * @param mediaBlendingMode The [Int] mediaBlendingMode to apply. Must be a member of + * MediaBlendingMode. + * @param contentSecurityLevel The [Int] contentSecurityLevel to apply. Must be a member of + * ContentSecurityLevel. + * @param useSuperSampling This [Boolean] specifies if the super sampling filter is enabled when + * rendering the surface. + * @return An int impress node ID which can be used for updating the surface later + * @throws InvalidArgumentException if stereoMode, mediaBlendingMode or contentSecurityLevel are + * invalid. + */ + public fun createStereoSurface( + @StereoMode stereoMode: Int, + @MediaBlendingMode mediaBlendingMode: Int, + @ContentSecurityLevel contentSecurityLevel: Int, + useSuperSampling: Boolean, + ): ImpressNode + /** * This method sets the Surface pixel dimenesions for a StereoSurfaceEntity. * diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApiImpl.kt b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApiImpl.kt index 5470d308dcba8..9e229c8111efa 100644 --- a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApiImpl.kt +++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/impl/impress/ImpressApiImpl.kt @@ -28,6 +28,7 @@ import androidx.xr.scenecore.impl.impress.ImpressApi.ColorRange import androidx.xr.scenecore.impl.impress.ImpressApi.ColorSpace import androidx.xr.scenecore.impl.impress.ImpressApi.ColorTransfer import androidx.xr.scenecore.impl.impress.ImpressApi.ContentSecurityLevel +import androidx.xr.scenecore.impl.impress.ImpressApi.MediaBlendingMode import androidx.xr.scenecore.impl.impress.ImpressApi.StereoMode import androidx.xr.scenecore.runtime.KhronosPbrMaterialSpec import androidx.xr.scenecore.runtime.TextureSampler @@ -61,6 +62,16 @@ public class ImpressApiImpl : ImpressApi { ) } + private fun validateMediaBlendingMode(@MediaBlendingMode mediaBlendingMode: Int): Int = + when (mediaBlendingMode) { + MediaBlendingMode.TRANSPARENT, + MediaBlendingMode.OPAQUE -> mediaBlendingMode + else -> + throw IllegalArgumentException( + "Unsupported value for ImpressApi.MediaBlendingMode: $mediaBlendingMode" + ) + } + /* * This is mostly here to throw on unsupported values. The int cast works as long as * ImpressApi.ContentSecurityLevel is in sync with imp::ContentSecurityLevel. @@ -465,30 +476,29 @@ public class ImpressApiImpl : ImpressApi { ) override fun createStereoSurface(@StereoMode stereoMode: Int): ImpressNode = - ImpressNode( - nCreateStereoSurfaceEntity( - getViewNativeHandle(view), - validateStereoMode(stereoMode), - ContentSecurityLevel.NONE, - /* useSuperSampling= */ false, - ) - ) + createStereoSurface(stereoMode, ContentSecurityLevel.NONE) override fun createStereoSurface( @StereoMode stereoMode: Int, @ContentSecurityLevel contentSecurityLevel: Int, ): ImpressNode = - ImpressNode( - nCreateStereoSurfaceEntity( - getViewNativeHandle(view), - validateStereoMode(stereoMode), - validateContentSecurityLevel(contentSecurityLevel), - /* useSuperSampling= */ false, - ) + createStereoSurface(stereoMode, contentSecurityLevel, /* useSuperSampling= */ false) + + override fun createStereoSurface( + @StereoMode stereoMode: Int, + @ContentSecurityLevel contentSecurityLevel: Int, + useSuperSampling: Boolean, + ): ImpressNode = + createStereoSurface( + stereoMode, + MediaBlendingMode.TRANSPARENT, + contentSecurityLevel, + useSuperSampling, ) override fun createStereoSurface( @StereoMode stereoMode: Int, + @MediaBlendingMode mediaBlendingMode: Int, @ContentSecurityLevel contentSecurityLevel: Int, useSuperSampling: Boolean, ): ImpressNode = @@ -496,6 +506,7 @@ public class ImpressApiImpl : ImpressApi { nCreateStereoSurfaceEntity( getViewNativeHandle(view), validateStereoMode(stereoMode), + validateMediaBlendingMode(mediaBlendingMode), validateContentSecurityLevel(contentSecurityLevel), useSuperSampling, ) @@ -1513,6 +1524,7 @@ public class ImpressApiImpl : ImpressApi { private external fun nCreateStereoSurfaceEntity( view: Long, stereoMode: Int, + blendingMode: Int, contentSecurityLevel: Int, useSuperSampling: Boolean, ): Int diff --git a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImplTest.kt b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImplTest.kt index 3af71234fb1b6..155f388c3d387 100644 --- a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImplTest.kt +++ b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/impl/impress/FakeImpressApiImplTest.kt @@ -18,6 +18,8 @@ package androidx.xr.scenecore.impl.impress import androidx.xr.scenecore.impl.impress.FakeImpressApiImpl.StereoSurfaceEntityData import androidx.xr.scenecore.impl.impress.FakeImpressApiImpl.StereoSurfaceEntityData.CanvasShape +import androidx.xr.scenecore.impl.impress.ImpressApi.ContentSecurityLevel +import androidx.xr.scenecore.impl.impress.ImpressApi.MediaBlendingMode import androidx.xr.scenecore.impl.impress.ImpressApi.StereoMode import androidx.xr.scenecore.runtime.KhronosPbrMaterialSpec import androidx.xr.scenecore.runtime.TextureSampler @@ -311,6 +313,24 @@ class FakeImpressApiImplTest { assertThat(surface).isNotNull() } + @Test + fun createStereoSurface_withBlendingMode_createsStereoSurface() { + val stereoMode = StereoMode.MONO + val blendingMode = MediaBlendingMode.OPAQUE + val contentSecurityLevel = ContentSecurityLevel.NONE + val stereoSurfaceNode = + fakeImpressApi.createStereoSurface( + stereoMode, + blendingMode, + contentSecurityLevel, + useSuperSampling = false, + ) + val stereoSurface = fakeImpressApi.getStereoSurfaceEntities() + val stereoSurfaceData = stereoSurface[stereoSurfaceNode] + assertNotNull(stereoSurfaceData) + assertThat(stereoSurfaceData.mediaBlendingMode).isEqualTo(blendingMode) + } + @Test fun setStereoSurfaceEntityCanvasShapeQuad_setsCanvasShapeQuad() { val stereoMode = StereoMode.MONO From c3285a6342129b76081541884ac9df8948b65b45 Mon Sep 17 00:00:00 2001 From: Paul Rohde Date: Fri, 16 Jan 2026 10:00:39 -0800 Subject: [PATCH 10/10] Add a single-stream captureWith function for FrameGraph This adds an extension function to make it easier to capture with a single stream into a FrameBuffer, as this is a common use case. Change-Id: Ifc87bebf160dfcb9c840b97ec0611347c6e6498e --- .../androidx/camera/camera2/pipe/FrameGraph.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FrameGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FrameGraph.kt index fdad3a60d64da..8205d383dab7e 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FrameGraph.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FrameGraph.kt @@ -59,7 +59,7 @@ public interface FrameGraph : CameraGraphBase, CameraControl public fun captureWith( streamIds: Set = emptySet(), parameters: Map = emptyMap(), - capacity: Int = 1, + capacity: Int = DEFAULT_FRAME_BUFFER_CAPACITY, ): FrameBuffer /** @@ -74,4 +74,16 @@ public interface FrameGraph : CameraGraphBase, 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 = emptyMap(), + capacity: Int = DEFAULT_FRAME_BUFFER_CAPACITY, + ): FrameBuffer = captureWith(setOf(streamId), parameters, capacity) + } }