[go: nahoru, domu]

blob: ccbb397e56546e8696db422cab93e745fc31b210 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.glance.appwidget.layoutgenerator
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.KModifier.PRIVATE
import com.squareup.kotlinpoet.KModifier.INTERNAL
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.buildCodeBlock
import com.squareup.kotlinpoet.joinToCode
import java.io.File
/**
* Generate the registry: a mapping from `LayoutSelector` to `Layout`.
*
* For each generated layout, a selector is created describing what the layout can and cannot do.
* It is mapped to a `Layout`, an object describing the parameters needed to act on the layout.
*/
internal fun generateRegistry(
packageName: String,
layouts: Map<File, List<ContainerProperties>>,
boxChildLayouts: Map<File, List<BoxChildProperties>>,
rowColumnChildLayouts: Map<File, List<RowColumnChildProperties>>,
outputSourceDir: File,
) {
outputSourceDir.mkdirs()
val file = FileSpec.builder(packageName, "GeneratedLayouts")
val generatedContainerApi21 = funSpec("registerContainers") {
returns(ContainerMap)
addModifiers(PRIVATE)
addStatement("val result =")
addCode(buildInitializer(layouts))
addStatement("return result")
}
val generatedChildrenApi21 = funSpec("registerChildren") {
returns(ContainerChildrenMap)
addModifiers(PRIVATE)
addStatement("val result =")
addCode(buildChildrenInitializer(layouts, StubSizes))
addStatement("return result")
}
val requireApi31 = AnnotationSpec.builder(RequiresApi).apply {
addMember("%M", VersionCodeS)
}.build()
val generatedContainerApi31 = objectSpec("GeneratedContainersForApi31Impl") {
addModifiers(PRIVATE)
addAnnotation(requireApi31)
addFunction(
funSpec("registerContainers") {
returns(ContainerMap)
addAnnotation(DoNotInline)
addStatement("val result =")
addCode(buildInitializer(layouts))
addStatement("return result")
}
)
addFunction(
funSpec("registerChildren") {
returns(ContainerChildrenMap)
addAnnotation(DoNotInline)
addStatement("val result =")
addCode(buildChildrenInitializer(layouts, listOf(ValidSize.Wrap)))
addStatement("return result")
}
)
}
val generatedLayouts = propertySpec(
"generatedContainers",
ContainerMap,
INTERNAL,
) {
initializer(buildCodeBlock {
addStatement(
"""
|if(%M >= %M) {
| GeneratedContainersForApi31Impl.registerContainers()
|} else {
| registerContainers()
|}""".trimMargin(),
SdkInt, VersionCodeS
)
})
}
file.addProperty(generatedLayouts)
file.addFunction(generatedContainerApi21)
val generatedChildren = propertySpec(
"generatedChildren",
ContainerChildrenMap,
INTERNAL,
) {
initializer(buildCodeBlock {
addStatement(
"""
|if(%M >= %M) {
| GeneratedContainersForApi31Impl.registerChildren()
|} else {
| registerChildren()
|}""".trimMargin(),
SdkInt, VersionCodeS
)
})
}
file.addProperty(generatedChildren)
file.addFunction(generatedChildrenApi21)
file.addType(generatedContainerApi31)
// TODO: only register the box children on T+, since the layouts are in layout-v32
val generatedBoxChildren = propertySpec(
"generatedBoxChildren",
BoxChildrenMap,
INTERNAL,
) {
initializer(buildBoxChildInitializer(boxChildLayouts))
}
file.addProperty(generatedBoxChildren)
val generatedRowColumnChildren = propertySpec(
"generatedRowColumnChildren",
RowColumnChildrenMap,
INTERNAL,
) {
initializer(buildRowColumnChildInitializer(rowColumnChildLayouts))
}
file.addProperty(generatedRowColumnChildren)
val generatedComplexLayouts = propertySpec("generatedComplexLayouts", LayoutsMap, INTERNAL) {
initializer(buildComplexInitializer())
}
file.addProperty(generatedComplexLayouts)
val generatedRoots = propertySpec("generatedRootLayoutShifts", SizeSelectorToIntMap, INTERNAL) {
addKdoc("Shift per root layout before Android S, based on width, height")
initializer(buildRootInitializer())
}
file.addProperty(generatedRoots)
val firstRootAlias = propertySpec("FirstRootAlias", INT, INTERNAL) {
initializer("R.layout.${makeRootAliasResourceName(0)}")
}
val lastRootAlias = propertySpec("LastRootAlias", INT, INTERNAL) {
initializer(
"R.layout.%L",
makeRootAliasResourceName(generatedRootSizePairs.size * RootLayoutAliasCount - 1)
)
}
val rootAliasCount = propertySpec("RootAliasCount", INT, INTERNAL) {
initializer("%L", generatedRootSizePairs.size * RootLayoutAliasCount)
}
file.addProperty(firstRootAlias)
file.addProperty(lastRootAlias)
file.addProperty(rootAliasCount)
file.build().writeTo(outputSourceDir)
}
private fun buildInitializer(layouts: Map<File, List<ContainerProperties>>): CodeBlock =
buildCodeBlock {
withIndent {
addStatement("mapOf(")
withIndent {
add(
layouts.map {
it.key to createFileInitializer(it.key, it.value)
}
.sortedBy { it.first.nameWithoutExtension }
.map { it.second }
.joinToCode("")
)
}
addStatement(")")
}
}
private fun buildChildrenInitializer(
layouts: Map<File, List<ContainerProperties>>,
sizes: List<ValidSize>,
): CodeBlock = buildCodeBlock {
withIndent {
addStatement("mapOf(")
withIndent {
add(
layouts.map {
it.key to createChildrenInitializer(it.key, it.value, sizes)
}
.sortedBy { it.first.nameWithoutExtension }
.map { it.second }
.joinToCode("")
)
}
addStatement(")")
}
}
private fun buildBoxChildInitializer(layouts: Map<File, List<BoxChildProperties>>): CodeBlock =
buildCodeBlock {
withIndent {
addStatement("mapOf(")
withIndent {
add(
layouts.map {
it.key to createBoxChildFileInitializer(it.key, it.value)
}
.sortedBy { it.first.nameWithoutExtension }
.map { it.second }
.joinToCode("")
)
}
addStatement(")")
}
}
private fun buildRowColumnChildInitializer(
layouts: Map<File, List<RowColumnChildProperties>>
): CodeBlock =
buildCodeBlock {
withIndent {
addStatement("mapOf(")
withIndent {
add(
layouts.map {
it.key to createRowColumnChildFileInitializer(it.key, it.value)
}
.sortedBy { it.first.nameWithoutExtension }
.map { it.second }
.joinToCode("")
)
}
addStatement(")")
}
}
private fun buildComplexInitializer(): CodeBlock {
return buildCodeBlock {
addStatement("mapOf(")
withIndent {
forEachConfiguration { width, height ->
addStatement(
"%T(width = %M, height = %M) to ",
SizeSelector,
width.toValue(),
height.toValue(),
)
withIndent {
val resId = makeComplexResourceName(width, height)
addStatement("%T(layoutId = R.layout.$resId),", LayoutInfo)
}
}
}
addStatement(")")
}
}
private fun buildRootInitializer(): CodeBlock {
return buildCodeBlock {
addStatement("mapOf(")
withIndent {
generatedRootSizePairs.forEachIndexed { index, (width, height) ->
addStatement(
"%T(width = %M, height = %M) to %L,",
SizeSelector,
width.toValue(),
height.toValue(),
index,
)
}
}
addStatement(")")
}
}
private fun createFileInitializer(
layout: File,
generated: List<ContainerProperties>
): CodeBlock =
buildCodeBlock {
val viewType = layout.nameWithoutExtension.toLayoutType()
generated.forEach { props ->
addContainer(
resourceName = makeContainerResourceName(
layout,
props.numberChildren,
props.horizontalAlignment,
props.verticalAlignment
),
viewType = viewType,
horizontalAlignment = props.horizontalAlignment,
verticalAlignment = props.verticalAlignment,
numChildren = props.numberChildren,
)
}
}
private fun createBoxChildFileInitializer(
layout: File,
generated: List<BoxChildProperties>
): CodeBlock =
buildCodeBlock {
val viewType = layout.nameWithoutExtension.toLayoutType()
generated.forEach { props ->
addBoxChild(
resourceName = makeBoxChildResourceName(
layout,
props.horizontalAlignment,
props.verticalAlignment
),
viewType = viewType,
horizontalAlignment = props.horizontalAlignment,
verticalAlignment = props.verticalAlignment,
)
}
}
private fun createRowColumnChildFileInitializer(
layout: File,
generated: List<RowColumnChildProperties>
): CodeBlock =
buildCodeBlock {
val viewType = layout.nameWithoutExtension.toLayoutType()
generated.forEach { props ->
addRowColumnChild(
resourceName = makeRowColumnChildResourceName(layout, props.width, props.height),
viewType = viewType,
width = props.width,
height = props.height,
)
}
}
private fun createChildrenInitializer(
layout: File,
generated: List<ContainerProperties>,
sizes: List<ValidSize>,
): CodeBlock =
buildCodeBlock {
val viewType = layout.nameWithoutExtension.toLayoutType()
val orientation = generated.first().containerOrientation
val numChildren =
generated.map { it.numberChildren }.maxOrNull() ?: error("There must be children")
childrenInitializer(viewType, generateChildren(numChildren, orientation, sizes))
}
private fun generateChildren(
numChildren: Int,
containerOrientation: ContainerOrientation,
sizes: List<ValidSize>
) =
(0 until numChildren).associateWith { pos ->
val widths = sizes + containerOrientation.extraWidths
val heights = sizes + containerOrientation.extraHeights
mapInCrossProduct(widths, heights) { width, height ->
ChildProperties(
childId = makeIdName(pos, width, height),
width = width,
height = height,
)
}
}
private fun CodeBlock.Builder.childrenInitializer(
viewType: String,
children: Map<Int, List<ChildProperties>>,
) {
addStatement("%M to mapOf(", makeViewType(viewType))
withIndent {
children.toList()
.sortedBy { it.first }
.forEach { (pos, children) ->
addStatement("$pos to mapOf(")
withIndent {
children.forEach { child ->
addStatement(
"%T(width = %M, height = %M)",
SizeSelector,
child.width.toValue(),
child.height.toValue(),
)
withIndent {
addStatement(
"to R.id.${
makeIdName(
pos,
child.width,
child.height
)
},"
)
}
}
}
addStatement("),")
}
}
addStatement("),")
}
private fun CodeBlock.Builder.addContainer(
resourceName: String,
viewType: String,
horizontalAlignment: HorizontalAlignment?,
verticalAlignment: VerticalAlignment?,
numChildren: Int,
) {
addStatement("%T(", ContainerSelector)
withIndent {
addStatement("type = %M,", makeViewType(viewType))
addStatement("numChildren = %L,", numChildren)
if (horizontalAlignment != null) {
addStatement("horizontalAlignment = %M, ", horizontalAlignment.code)
}
if (verticalAlignment != null) {
addStatement("verticalAlignment = %M, ", verticalAlignment.code)
}
}
addStatement(") to %T(layoutId = R.layout.$resourceName),", ContainerInfo)
}
private fun CodeBlock.Builder.addBoxChild(
resourceName: String,
viewType: String,
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
) {
addStatement("%T(", BoxChildSelector)
withIndent {
addStatement("type = %M,", makeViewType(viewType))
addStatement("horizontalAlignment = %M, ", horizontalAlignment.code)
addStatement("verticalAlignment = %M, ", verticalAlignment.code)
}
addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
}
private fun CodeBlock.Builder.addRowColumnChild(
resourceName: String,
viewType: String,
width: ValidSize,
height: ValidSize,
) {
addStatement("%T(", RowColumnChildSelector)
withIndent {
addStatement("type = %M,", makeViewType(viewType))
addStatement("expandWidth = %L, ", width == ValidSize.Expand)
addStatement("expandHeight = %L, ", height == ValidSize.Expand)
}
addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
}
private val ContainerSelector = ClassName("androidx.glance.appwidget", "ContainerSelector")
private val SizeSelector = ClassName("androidx.glance.appwidget", "SizeSelector")
private val BoxChildSelector = ClassName("androidx.glance.appwidget", "BoxChildSelector")
private val RowColumnChildSelector =
ClassName("androidx.glance.appwidget", "RowColumnChildSelector")
private val LayoutInfo = ClassName("androidx.glance.appwidget", "LayoutInfo")
private val ContainerInfo = ClassName("androidx.glance.appwidget", "ContainerInfo")
private val ContainerMap = Map::class.asTypeName().parameterizedBy(ContainerSelector, ContainerInfo)
private const val LayoutSpecSize = "androidx.glance.appwidget.LayoutSize"
private val WrapValue = MemberName("$LayoutSpecSize", "Wrap")
private val FixedValue = MemberName("$LayoutSpecSize", "Fixed")
private val MatchValue = MemberName("$LayoutSpecSize", "MatchParent")
private val ExpandValue = MemberName("$LayoutSpecSize", "Expand")
private val LayoutsMap = Map::class.asTypeName().parameterizedBy(SizeSelector, LayoutInfo)
private val SizeSelectorToIntMap = Map::class.asTypeName().parameterizedBy(SizeSelector, INT)
private val AndroidBuildVersion = ClassName("android.os", "Build", "VERSION")
private val AndroidBuildVersionCodes = ClassName("android.os", "Build", "VERSION_CODES")
private val SdkInt = AndroidBuildVersion.member("SDK_INT")
private val VersionCodeS = AndroidBuildVersionCodes.member("S")
private val RequiresApi = ClassName("androidx.annotation", "RequiresApi")
private val DoNotInline = ClassName("androidx.annotation", "DoNotInline")
private val HorizontalAlignmentType =
ClassName("androidx.glance.layout", "Alignment", "Horizontal", "Companion")
private val VerticalAlignmentType =
ClassName("androidx.glance.layout", "Alignment", "Vertical", "Companion")
internal val AlignmentStart = MemberName(HorizontalAlignmentType, "Start")
internal val AlignmentCenterHorizontally = MemberName(HorizontalAlignmentType, "CenterHorizontally")
internal val AlignmentEnd = MemberName(HorizontalAlignmentType, "End")
internal val AlignmentTop = MemberName(VerticalAlignmentType, "Top")
internal val AlignmentCenterVertically = MemberName(VerticalAlignmentType, "CenterVertically")
internal val AlignmentBottom = MemberName(VerticalAlignmentType, "Bottom")
private val LayoutType = ClassName("androidx.glance.appwidget", "LayoutType")
private val ChildrenMap = Map::class.asTypeName().parameterizedBy(INT, SizeSelectorToIntMap)
private val ContainerChildrenMap = Map::class.asTypeName().parameterizedBy(LayoutType, ChildrenMap)
private val BoxChildrenMap = Map::class.asTypeName().parameterizedBy(BoxChildSelector, LayoutInfo)
private val RowColumnChildrenMap =
Map::class.asTypeName().parameterizedBy(RowColumnChildSelector, LayoutInfo)
private fun makeViewType(name: String) =
MemberName("androidx.glance.appwidget.LayoutType", name)
private fun String.toLayoutType(): String =
snakeRegex.replace(this.removePrefix("glance_")) {
it.value.replace("_", "").uppercase()
}.replaceFirstChar { it.uppercaseChar() }
private val snakeRegex = "_[a-zA-Z0-9]".toRegex()
private fun ValidSize.toValue() = when (this) {
ValidSize.Wrap -> WrapValue
ValidSize.Fixed -> FixedValue
ValidSize.Expand -> ExpandValue
ValidSize.Match -> MatchValue
}
internal fun makeComplexResourceName(width: ValidSize, height: ValidSize) =
listOf(
"complex",
width.resourceName,
height.resourceName,
).joinToString(separator = "_")
internal fun makeRootResourceName(width: ValidSize, height: ValidSize) =
listOf(
"root",
width.resourceName,
height.resourceName,
).joinToString(separator = "_")
internal fun makeRootAliasResourceName(index: Int) = "root_alias_%03d".format(index)
internal fun makeContainerResourceName(
file: File,
numChildren: Int,
horizontalAlignment: HorizontalAlignment?,
verticalAlignment: VerticalAlignment?
) =
listOf(
file.nameWithoutExtension,
horizontalAlignment?.resourceName,
verticalAlignment?.resourceName,
"${numChildren}children"
).joinToString(separator = "_")
internal fun makeChildResourceName(
pos: Int,
containerOrientation: ContainerOrientation,
horizontalAlignment: HorizontalAlignment?,
verticalAlignment: VerticalAlignment?
) =
listOf(
containerOrientation.resourceName,
"child",
horizontalAlignment?.resourceName,
verticalAlignment?.resourceName,
"group",
pos
).joinToString(separator = "_")
internal fun makeBoxChildResourceName(
file: File,
horizontalAlignment: HorizontalAlignment?,
verticalAlignment: VerticalAlignment?
) =
listOf(
file.nameWithoutExtension,
horizontalAlignment?.resourceName,
verticalAlignment?.resourceName,
).joinToString(separator = "_")
internal fun makeRowColumnChildResourceName(
file: File,
width: ValidSize,
height: ValidSize,
) =
listOf(
file.nameWithoutExtension,
if (width == ValidSize.Expand) "expandwidth" else "wrapwidth",
if (height == ValidSize.Expand) "expandheight" else "wrapheight",
).joinToString(separator = "_")
internal fun makeIdName(pos: Int, width: ValidSize, height: ValidSize) =
listOf(
"childStub$pos",
width.resourceName,
height.resourceName
).joinToString(separator = "_")
internal fun CodeBlock.Builder.withIndent(
builderAction: CodeBlock.Builder.() -> Unit
): CodeBlock.Builder {
indent()
apply(builderAction)
unindent()
return this
}
internal fun funSpec(name: String, builder: FunSpec.Builder.() -> Unit) =
FunSpec.builder(name).apply(builder).build()
internal fun objectSpec(name: String, builder: TypeSpec.Builder.() -> Unit) =
TypeSpec.objectBuilder(name).apply(builder).build()
internal fun propertySpec(
name: String,
type: TypeName,
vararg modifiers: KModifier,
builder: PropertySpec.Builder.() -> Unit
) = PropertySpec.builder(name, type, *modifiers).apply(builder).build()
private val listConfigurations =
crossProduct(ValidSize.values().toList(), ValidSize.values().toList())
private val generatedRootSizePairs = crossProduct(StubSizes, StubSizes)
internal inline fun mapConfiguration(
function: (width: ValidSize, height: ValidSize) -> File
): List<File> =
listConfigurations.map { (a, b) -> function(a, b) }
internal inline fun forEachConfiguration(function: (width: ValidSize, height: ValidSize) -> Unit) {
listConfigurations.forEach { (a, b) -> function(a, b) }
}
internal inline fun <A, B, T> mapInCrossProduct(
first: Iterable<A>,
second: Iterable<B>,
consumer: (A, B) -> T
): List<T> =
first.flatMap { a ->
second.map { b ->
consumer(a, b)
}
}
internal inline fun <A, B, T> forEachInCrossProduct(
first: Iterable<A>,
second: Iterable<B>,
consumer: (A, B) -> T
) {
first.forEach { a ->
second.forEach { b ->
consumer(a, b)
}
}
}
internal fun <A, B> crossProduct(
first: Iterable<A>,
second: Iterable<B>,
): List<Pair<A, B>> =
mapInCrossProduct(first, second) { a, b -> a to b }
internal fun File.resolveRes(resName: String) = resolve("$resName.xml")