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