1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.glance.appwidget.layoutgenerator
18 
19 import com.squareup.kotlinpoet.AnnotationSpec
20 import com.squareup.kotlinpoet.ClassName
21 import com.squareup.kotlinpoet.CodeBlock
22 import com.squareup.kotlinpoet.FileSpec
23 import com.squareup.kotlinpoet.FunSpec
24 import com.squareup.kotlinpoet.INT
25 import com.squareup.kotlinpoet.KModifier
26 import com.squareup.kotlinpoet.KModifier.INTERNAL
27 import com.squareup.kotlinpoet.KModifier.PRIVATE
28 import com.squareup.kotlinpoet.MemberName
29 import com.squareup.kotlinpoet.MemberName.Companion.member
30 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
31 import com.squareup.kotlinpoet.PropertySpec
32 import com.squareup.kotlinpoet.TypeName
33 import com.squareup.kotlinpoet.TypeSpec
34 import com.squareup.kotlinpoet.asTypeName
35 import com.squareup.kotlinpoet.buildCodeBlock
36 import com.squareup.kotlinpoet.joinToCode
37 import java.io.File
38 
39 /**
40  * Generate the registry: a mapping from `LayoutSelector` to `Layout`.
41  *
42  * For each generated layout, a selector is created describing what the layout can and cannot do. It
43  * is mapped to a `Layout`, an object describing the parameters needed to act on the layout.
44  */
45 internal fun generateRegistry(
46     packageName: String,
47     layouts: Map<File, List<ContainerProperties>>,
48     boxChildLayouts: Map<File, List<BoxChildProperties>>,
49     rowColumnChildLayouts: Map<File, List<RowColumnChildProperties>>,
50     outputSourceDir: File,
51 ) {
52     outputSourceDir.mkdirs()
53     val file = FileSpec.builder(packageName, "GeneratedLayouts")
54 
55     val generatedContainerApi21 =
56         funSpec("registerContainers") {
57             returns(ContainerMap)
58             addModifiers(PRIVATE)
59             addStatement("val result =")
60             addCode(buildInitializer(layouts))
61             addStatement("return result")
62         }
63 
64     val generatedChildrenApi21 =
65         funSpec("registerChildren") {
66             returns(ContainerChildrenMap)
67             addModifiers(PRIVATE)
68             addStatement("val result =")
69             addCode(buildChildrenInitializer(layouts, StubSizes))
70             addStatement("return result")
71         }
72 
73     val requireApi31 =
74         AnnotationSpec.builder(RequiresApi).apply { addMember("%M", VersionCodeS) }.build()
75     val generatedContainerApi31 =
76         objectSpec("GeneratedContainersForApi31Impl") {
77             addModifiers(PRIVATE)
78             addAnnotation(requireApi31)
79             addFunction(
80                 funSpec("registerContainers") {
81                     returns(ContainerMap)
82                     addAnnotation(DoNotInline)
83                     addStatement("val result =")
84                     addCode(buildInitializer(layouts))
85                     addStatement("return result")
86                 }
87             )
88             addFunction(
89                 funSpec("registerChildren") {
90                     returns(ContainerChildrenMap)
91                     addAnnotation(DoNotInline)
92                     addStatement("val result =")
93                     addCode(buildChildrenInitializer(layouts, listOf(ValidSize.Wrap)))
94                     addStatement("return result")
95                 }
96             )
97         }
98 
99     val generatedLayouts =
100         propertySpec(
101             "generatedContainers",
102             ContainerMap,
103             INTERNAL,
104         ) {
105             initializer(
106                 buildCodeBlock {
107                     addStatement(
108                         """
109                 |if(%M >= %M) {
110                 |  GeneratedContainersForApi31Impl.registerContainers()
111                 |} else {
112                 |  registerContainers()
113                 |}"""
114                             .trimMargin(),
115                         SdkInt,
116                         VersionCodeS
117                     )
118                 }
119             )
120         }
121     file.addProperty(generatedLayouts)
122     file.addFunction(generatedContainerApi21)
123 
124     val generatedChildren =
125         propertySpec(
126             "generatedChildren",
127             ContainerChildrenMap,
128             INTERNAL,
129         ) {
130             initializer(
131                 buildCodeBlock {
132                     addStatement(
133                         """
134                 |if(%M >= %M) {
135                 |  GeneratedContainersForApi31Impl.registerChildren()
136                 |} else {
137                 |  registerChildren()
138                 |}"""
139                             .trimMargin(),
140                         SdkInt,
141                         VersionCodeS
142                     )
143                 }
144             )
145         }
146     file.addProperty(generatedChildren)
147     file.addFunction(generatedChildrenApi21)
148     file.addType(generatedContainerApi31)
149 
150     // TODO: only register the box children on T+, since the layouts are in layout-v32
151     val generatedBoxChildren =
152         propertySpec(
153             "generatedBoxChildren",
154             BoxChildrenMap,
155             INTERNAL,
156         ) {
157             initializer(buildBoxChildInitializer(boxChildLayouts))
158         }
159     file.addProperty(generatedBoxChildren)
160     val generatedRowColumnChildren =
161         propertySpec(
162             "generatedRowColumnChildren",
163             RowColumnChildrenMap,
164             INTERNAL,
165         ) {
166             initializer(buildRowColumnChildInitializer(rowColumnChildLayouts))
167         }
168     file.addProperty(generatedRowColumnChildren)
169 
170     val generatedComplexLayouts =
171         propertySpec("generatedComplexLayouts", LayoutsMap, INTERNAL) {
172             initializer(buildComplexInitializer())
173         }
174     file.addProperty(generatedComplexLayouts)
175 
176     val generatedRoots =
177         propertySpec("generatedRootLayoutShifts", SizeSelectorToIntMap, INTERNAL) {
178             addKdoc("Shift per root layout before Android S, based on width, height")
179             initializer(buildRootInitializer())
180         }
181     file.addProperty(generatedRoots)
182 
183     val firstRootAlias =
184         propertySpec("FirstRootAlias", INT, INTERNAL) {
185             initializer("R.layout.${makeRootAliasResourceName(0)}")
186         }
187     val lastRootAlias =
188         propertySpec("LastRootAlias", INT, INTERNAL) {
189             initializer(
190                 "R.layout.%L",
191                 makeRootAliasResourceName(generatedRootSizePairs.size * RootLayoutAliasCount - 1)
192             )
193         }
194     val rootAliasCount =
195         propertySpec("RootAliasCount", INT, INTERNAL) {
196             initializer("%L", generatedRootSizePairs.size * RootLayoutAliasCount)
197         }
198     file.addProperty(firstRootAlias)
199     file.addProperty(lastRootAlias)
200     file.addProperty(rootAliasCount)
201 
202     val firstViewId =
203         propertySpec("FirstViewId", INT, INTERNAL) {
204             initializer("R.id.${makeViewIdResourceName(0)}")
205         }
206     val lastViewId =
207         propertySpec("LastViewId", INT, INTERNAL) {
208             initializer("R.id.${makeViewIdResourceName(TotalViewCount - 1)}")
209         }
210     val totalViewCount =
211         propertySpec("TotalViewCount", INT, INTERNAL) { initializer("%L", TotalViewCount) }
212     file.addProperty(firstViewId)
213     file.addProperty(lastViewId)
214     file.addProperty(totalViewCount)
215 
216     file.build().writeTo(outputSourceDir)
217 }
218 
buildInitializernull219 private fun buildInitializer(layouts: Map<File, List<ContainerProperties>>): CodeBlock =
220     buildCodeBlock {
221         withIndent {
222             addStatement("mapOf(")
223             withIndent {
224                 add(
225                     layouts
226                         .map { it.key to createFileInitializer(it.key, it.value) }
227                         .sortedBy { it.first.nameWithoutExtension }
228                         .map { it.second }
229                         .joinToCode("")
230                 )
231             }
232             addStatement(")")
233         }
234     }
235 
buildChildrenInitializernull236 private fun buildChildrenInitializer(
237     layouts: Map<File, List<ContainerProperties>>,
238     sizes: List<ValidSize>,
239 ): CodeBlock = buildCodeBlock {
240     withIndent {
241         addStatement("mapOf(")
242         withIndent {
243             add(
244                 layouts
245                     .map { it.key to createChildrenInitializer(it.key, it.value, sizes) }
246                     .sortedBy { it.first.nameWithoutExtension }
247                     .map { it.second }
248                     .joinToCode("")
249             )
250         }
251         addStatement(")")
252     }
253 }
254 
buildBoxChildInitializernull255 private fun buildBoxChildInitializer(layouts: Map<File, List<BoxChildProperties>>): CodeBlock =
256     buildCodeBlock {
257         withIndent {
258             addStatement("mapOf(")
259             withIndent {
260                 add(
261                     layouts
262                         .map { it.key to createBoxChildFileInitializer(it.key, it.value) }
263                         .sortedBy { it.first.nameWithoutExtension }
264                         .map { it.second }
265                         .joinToCode("")
266                 )
267             }
268             addStatement(")")
269         }
270     }
271 
buildRowColumnChildInitializernull272 private fun buildRowColumnChildInitializer(
273     layouts: Map<File, List<RowColumnChildProperties>>
274 ): CodeBlock = buildCodeBlock {
275     withIndent {
276         addStatement("mapOf(")
277         withIndent {
278             add(
279                 layouts
280                     .map { it.key to createRowColumnChildFileInitializer(it.key, it.value) }
281                     .sortedBy { it.first.nameWithoutExtension }
282                     .map { it.second }
283                     .joinToCode("")
284             )
285         }
286         addStatement(")")
287     }
288 }
289 
buildComplexInitializernull290 private fun buildComplexInitializer(): CodeBlock {
291     return buildCodeBlock {
292         addStatement("mapOf(")
293         withIndent {
294             forEachConfiguration { width, height ->
295                 addStatement(
296                     "%T(width = %M, height = %M) to ",
297                     SizeSelector,
298                     width.toValue(),
299                     height.toValue(),
300                 )
301                 withIndent {
302                     val resId = makeComplexResourceName(width, height)
303                     addStatement("%T(layoutId = R.layout.$resId),", LayoutInfo)
304                 }
305             }
306         }
307         addStatement(")")
308     }
309 }
310 
buildRootInitializernull311 private fun buildRootInitializer(): CodeBlock {
312     return buildCodeBlock {
313         addStatement("mapOf(")
314         withIndent {
315             generatedRootSizePairs.forEachIndexed { index, (width, height) ->
316                 addStatement(
317                     "%T(width = %M, height = %M) to %L,",
318                     SizeSelector,
319                     width.toValue(),
320                     height.toValue(),
321                     index,
322                 )
323             }
324         }
325         addStatement(")")
326     }
327 }
328 
createFileInitializernull329 private fun createFileInitializer(layout: File, generated: List<ContainerProperties>): CodeBlock =
330     buildCodeBlock {
331         val viewType = layout.nameWithoutExtension.toLayoutType()
332         generated.forEach { props ->
333             addContainer(
334                 resourceName =
335                     makeContainerResourceName(
336                         layout,
337                         props.numberChildren,
338                         props.horizontalAlignment,
339                         props.verticalAlignment
340                     ),
341                 viewType = viewType,
342                 horizontalAlignment = props.horizontalAlignment,
343                 verticalAlignment = props.verticalAlignment,
344                 numChildren = props.numberChildren,
345             )
346         }
347     }
348 
createBoxChildFileInitializernull349 private fun createBoxChildFileInitializer(
350     layout: File,
351     generated: List<BoxChildProperties>
352 ): CodeBlock = buildCodeBlock {
353     val viewType = layout.nameWithoutExtension.toLayoutType()
354     generated.forEach { props ->
355         addBoxChild(
356             resourceName =
357                 makeBoxChildResourceName(
358                     layout,
359                     props.horizontalAlignment,
360                     props.verticalAlignment
361                 ),
362             viewType = viewType,
363             horizontalAlignment = props.horizontalAlignment,
364             verticalAlignment = props.verticalAlignment,
365         )
366     }
367 }
368 
createRowColumnChildFileInitializernull369 private fun createRowColumnChildFileInitializer(
370     layout: File,
371     generated: List<RowColumnChildProperties>
372 ): CodeBlock = buildCodeBlock {
373     val viewType = layout.nameWithoutExtension.toLayoutType()
374     generated.forEach { props ->
375         addRowColumnChild(
376             resourceName = makeRowColumnChildResourceName(layout, props.width, props.height),
377             viewType = viewType,
378             width = props.width,
379             height = props.height,
380         )
381     }
382 }
383 
createChildrenInitializernull384 private fun createChildrenInitializer(
385     layout: File,
386     generated: List<ContainerProperties>,
387     sizes: List<ValidSize>,
388 ): CodeBlock = buildCodeBlock {
389     val viewType = layout.nameWithoutExtension.toLayoutType()
390     val orientation = generated.first().containerOrientation
391     val numChildren =
392         generated.map { it.numberChildren }.maxOrNull() ?: error("There must be children")
393     childrenInitializer(viewType, generateChildren(numChildren, orientation, sizes))
394 }
395 
generateChildrennull396 private fun generateChildren(
397     numChildren: Int,
398     containerOrientation: ContainerOrientation,
399     sizes: List<ValidSize>
400 ) =
401     (0 until numChildren).associateWith { pos ->
402         val widths = sizes + containerOrientation.extraWidths
403         val heights = sizes + containerOrientation.extraHeights
404         mapInCrossProduct(widths, heights) { width, height ->
405             ChildProperties(
406                 childId = makeIdName(pos, width, height),
407                 width = width,
408                 height = height,
409             )
410         }
411     }
412 
childrenInitializernull413 private fun CodeBlock.Builder.childrenInitializer(
414     viewType: String,
415     children: Map<Int, List<ChildProperties>>,
416 ) {
417     addStatement("%M to mapOf(", makeViewType(viewType))
418     withIndent {
419         children
420             .toList()
421             .sortedBy { it.first }
422             .forEach { (pos, children) ->
423                 addStatement("$pos to mapOf(")
424                 withIndent {
425                     children.forEach { child ->
426                         addStatement(
427                             "%T(width = %M, height = %M)",
428                             SizeSelector,
429                             child.width.toValue(),
430                             child.height.toValue(),
431                         )
432                         withIndent {
433                             addStatement(
434                                 "to R.id.${
435                                     makeIdName(
436                                         pos,
437                                         child.width,
438                                         child.height
439                                     )
440                                 },"
441                             )
442                         }
443                     }
444                 }
445                 addStatement("),")
446             }
447     }
448     addStatement("),")
449 }
450 
CodeBlocknull451 private fun CodeBlock.Builder.addContainer(
452     resourceName: String,
453     viewType: String,
454     horizontalAlignment: HorizontalAlignment?,
455     verticalAlignment: VerticalAlignment?,
456     numChildren: Int,
457 ) {
458     addStatement("%T(", ContainerSelector)
459     withIndent {
460         addStatement("type = %M,", makeViewType(viewType))
461         addStatement("numChildren = %L,", numChildren)
462         if (horizontalAlignment != null) {
463             addStatement("horizontalAlignment = %M, ", horizontalAlignment.code)
464         }
465         if (verticalAlignment != null) {
466             addStatement("verticalAlignment = %M, ", verticalAlignment.code)
467         }
468     }
469     addStatement(") to %T(layoutId = R.layout.$resourceName),", ContainerInfo)
470 }
471 
CodeBlocknull472 private fun CodeBlock.Builder.addBoxChild(
473     resourceName: String,
474     viewType: String,
475     horizontalAlignment: HorizontalAlignment,
476     verticalAlignment: VerticalAlignment,
477 ) {
478     addStatement("%T(", BoxChildSelector)
479     withIndent {
480         addStatement("type = %M,", makeViewType(viewType))
481         addStatement("horizontalAlignment = %M, ", horizontalAlignment.code)
482         addStatement("verticalAlignment = %M, ", verticalAlignment.code)
483     }
484     addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
485 }
486 
CodeBlocknull487 private fun CodeBlock.Builder.addRowColumnChild(
488     resourceName: String,
489     viewType: String,
490     width: ValidSize,
491     height: ValidSize,
492 ) {
493     addStatement("%T(", RowColumnChildSelector)
494     withIndent {
495         addStatement("type = %M,", makeViewType(viewType))
496         addStatement("expandWidth = %L, ", width == ValidSize.Expand)
497         addStatement("expandHeight = %L, ", height == ValidSize.Expand)
498     }
499     addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
500 }
501 
502 private val ContainerSelector = ClassName("androidx.glance.appwidget", "ContainerSelector")
503 private val SizeSelector = ClassName("androidx.glance.appwidget", "SizeSelector")
504 private val BoxChildSelector = ClassName("androidx.glance.appwidget", "BoxChildSelector")
505 private val RowColumnChildSelector =
506     ClassName("androidx.glance.appwidget", "RowColumnChildSelector")
507 private val LayoutInfo = ClassName("androidx.glance.appwidget", "LayoutInfo")
508 private val ContainerInfo = ClassName("androidx.glance.appwidget", "ContainerInfo")
509 private val ContainerMap = Map::class.asTypeName().parameterizedBy(ContainerSelector, ContainerInfo)
510 private const val LayoutSpecSize = "androidx.glance.appwidget.LayoutSize"
511 private val WrapValue = MemberName("$LayoutSpecSize", "Wrap")
512 private val FixedValue = MemberName("$LayoutSpecSize", "Fixed")
513 private val MatchValue = MemberName("$LayoutSpecSize", "MatchParent")
514 private val ExpandValue = MemberName("$LayoutSpecSize", "Expand")
515 private val LayoutsMap = Map::class.asTypeName().parameterizedBy(SizeSelector, LayoutInfo)
516 private val SizeSelectorToIntMap = Map::class.asTypeName().parameterizedBy(SizeSelector, INT)
517 private val AndroidBuildVersion = ClassName("android.os", "Build", "VERSION")
518 private val AndroidBuildVersionCodes = ClassName("android.os", "Build", "VERSION_CODES")
519 private val SdkInt = AndroidBuildVersion.member("SDK_INT")
520 private val VersionCodeS = AndroidBuildVersionCodes.member("S")
521 private val RequiresApi = ClassName("androidx.annotation", "RequiresApi")
522 private val DoNotInline = ClassName("androidx.annotation", "DoNotInline")
523 private val HorizontalAlignmentType =
524     ClassName("androidx.glance.layout", "Alignment", "Horizontal", "Companion")
525 private val VerticalAlignmentType =
526     ClassName("androidx.glance.layout", "Alignment", "Vertical", "Companion")
527 internal val AlignmentStart = MemberName(HorizontalAlignmentType, "Start")
528 internal val AlignmentCenterHorizontally = MemberName(HorizontalAlignmentType, "CenterHorizontally")
529 internal val AlignmentEnd = MemberName(HorizontalAlignmentType, "End")
530 internal val AlignmentTop = MemberName(VerticalAlignmentType, "Top")
531 internal val AlignmentCenterVertically = MemberName(VerticalAlignmentType, "CenterVertically")
532 internal val AlignmentBottom = MemberName(VerticalAlignmentType, "Bottom")
533 private val LayoutType = ClassName("androidx.glance.appwidget", "LayoutType")
534 private val ChildrenMap = Map::class.asTypeName().parameterizedBy(INT, SizeSelectorToIntMap)
535 private val ContainerChildrenMap = Map::class.asTypeName().parameterizedBy(LayoutType, ChildrenMap)
536 private val BoxChildrenMap = Map::class.asTypeName().parameterizedBy(BoxChildSelector, LayoutInfo)
537 private val RowColumnChildrenMap =
538     Map::class.asTypeName().parameterizedBy(RowColumnChildSelector, LayoutInfo)
539 
makeViewTypenull540 private fun makeViewType(name: String) = MemberName("androidx.glance.appwidget.LayoutType", name)
541 
542 private fun String.toLayoutType(): String =
543     snakeRegex
544         .replace(this.removePrefix("glance_")) { it.value.replace("_", "").uppercase() }
<lambda>null545         .replaceFirstChar { it.uppercaseChar() }
546 
547 private val snakeRegex = "_[a-zA-Z0-9]".toRegex()
548 
toValuenull549 private fun ValidSize.toValue() =
550     when (this) {
551         ValidSize.Wrap -> WrapValue
552         ValidSize.Fixed -> FixedValue
553         ValidSize.Expand -> ExpandValue
554         ValidSize.Match -> MatchValue
555     }
556 
makeComplexResourceNamenull557 internal fun makeComplexResourceName(width: ValidSize, height: ValidSize) =
558     listOf(
559             "complex",
560             width.resourceName,
561             height.resourceName,
562         )
563         .joinToString(separator = "_")
564 
565 internal fun makeRootResourceName(width: ValidSize, height: ValidSize) =
566     listOf(
567             "root",
568             width.resourceName,
569             height.resourceName,
570         )
571         .joinToString(separator = "_")
572 
573 internal fun makeRootAliasResourceName(index: Int) = "root_alias_%03d".format(index)
574 
575 internal fun makeViewIdResourceName(index: Int) = "glance_view_id_%03d".format(index)
576 
577 internal fun makeContainerResourceName(
578     file: File,
579     numChildren: Int,
580     horizontalAlignment: HorizontalAlignment?,
581     verticalAlignment: VerticalAlignment?
582 ) =
583     listOf(
584             file.nameWithoutExtension,
585             horizontalAlignment?.resourceName,
586             verticalAlignment?.resourceName,
587             "${numChildren}children"
588         )
589         .joinToString(separator = "_")
590 
591 internal fun makeChildResourceName(
592     pos: Int,
593     containerOrientation: ContainerOrientation,
594     horizontalAlignment: HorizontalAlignment?,
595     verticalAlignment: VerticalAlignment?
596 ) =
597     listOf(
598             containerOrientation.resourceName,
599             "child",
600             horizontalAlignment?.resourceName,
601             verticalAlignment?.resourceName,
602             "group",
603             pos
604         )
605         .joinToString(separator = "_")
606 
607 internal fun makeBoxChildResourceName(
608     file: File,
609     horizontalAlignment: HorizontalAlignment?,
610     verticalAlignment: VerticalAlignment?
611 ) =
612     listOf(
613             file.nameWithoutExtension,
614             horizontalAlignment?.resourceName,
615             verticalAlignment?.resourceName,
616         )
617         .joinToString(separator = "_")
618 
619 internal fun makeRowColumnChildResourceName(
620     file: File,
621     width: ValidSize,
622     height: ValidSize,
623 ) =
624     listOf(
625             file.nameWithoutExtension,
626             if (width == ValidSize.Expand) "expandwidth" else "wrapwidth",
627             if (height == ValidSize.Expand) "expandheight" else "wrapheight",
628         )
629         .joinToString(separator = "_")
630 
631 internal fun makeIdName(pos: Int, width: ValidSize, height: ValidSize) =
632     listOf("childStub$pos", width.resourceName, height.resourceName).joinToString(separator = "_")
633 
634 internal fun CodeBlock.Builder.withIndent(
635     builderAction: CodeBlock.Builder.() -> Unit
636 ): CodeBlock.Builder {
637     indent()
638     apply(builderAction)
639     unindent()
640     return this
641 }
642 
funSpecnull643 internal fun funSpec(name: String, builder: FunSpec.Builder.() -> Unit) =
644     FunSpec.builder(name).apply(builder).build()
645 
646 internal fun objectSpec(name: String, builder: TypeSpec.Builder.() -> Unit) =
647     TypeSpec.objectBuilder(name).apply(builder).build()
648 
649 internal fun propertySpec(
650     name: String,
651     type: TypeName,
652     vararg modifiers: KModifier,
653     builder: PropertySpec.Builder.() -> Unit
654 ) = PropertySpec.builder(name, type, *modifiers).apply(builder).build()
655 
656 private val listConfigurations =
657     crossProduct(ValidSize.values().toList(), ValidSize.values().toList())
658 
659 private val generatedRootSizePairs = crossProduct(StubSizes, StubSizes)
660 
661 internal inline fun mapConfiguration(
662     function: (width: ValidSize, height: ValidSize) -> File
663 ): List<File> = listConfigurations.map { (a, b) -> function(a, b) }
664 
forEachConfigurationnull665 internal inline fun forEachConfiguration(function: (width: ValidSize, height: ValidSize) -> Unit) {
666     listConfigurations.forEach { (a, b) -> function(a, b) }
667 }
668 
mapInCrossProductnull669 internal inline fun <A, B, T> mapInCrossProduct(
670     first: Iterable<A>,
671     second: Iterable<B>,
672     consumer: (A, B) -> T
673 ): List<T> = first.flatMap { a -> second.map { b -> consumer(a, b) } }
674 
forEachInCrossProductnull675 internal inline fun <A, B, T> forEachInCrossProduct(
676     first: Iterable<A>,
677     second: Iterable<B>,
678     consumer: (A, B) -> T
679 ) {
680     first.forEach { a -> second.forEach { b -> consumer(a, b) } }
681 }
682 
crossProductnull683 internal fun <A, B> crossProduct(
684     first: Iterable<A>,
685     second: Iterable<B>,
686 ): List<Pair<A, B>> = mapInCrossProduct(first, second) { a, b -> a to b }
687 
resolveResnull688 internal fun File.resolveRes(resName: String) = resolve("$resName.xml")
689