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.MemberName
20 import java.io.File
21 import javax.xml.parsers.DocumentBuilderFactory
22 import javax.xml.transform.OutputKeys
23 import javax.xml.transform.TransformerFactory
24 import javax.xml.transform.dom.DOMSource
25 import javax.xml.transform.stream.StreamResult
26 import org.w3c.dom.Document
27 import org.w3c.dom.Node
28 
29 /**
30  * Generate the layouts from the templates provided to the task.
31  *
32  * For each layout template, 18 layouts are created: 9 simple and 9 complex. The simple layouts are
33  * there to create non-resizable views, while complex layouts are there to create resizable layouts
34  * (i.e. layout with at least one dimension sets explicitly in dip).
35  *
36  * A layout should be of the form:
37  * ```
38  * <TargetView prop1="" ... />
39  * ```
40  *
41  * For example, for the row:
42  * ```
43  * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
44  *     android:orientation="horizontal" />
45  * ```
46  *
47  * The template should not define the view id, or the desired width and height of the view.
48  */
49 internal class LayoutGenerator {
50 
51     private val documentBuilderFactory by lazy {
52         DocumentBuilderFactory.newInstance().apply { isNamespaceAware = true }
53     }
54 
55     private val documentBuilder by lazy { documentBuilderFactory.newDocumentBuilder()!! }
56 
57     private val transformerFactory by lazy { TransformerFactory.newInstance() }
58 
59     fun parseLayoutTemplate(input: File): Document = documentBuilder.parse(input)
60 
61     fun writeGeneratedLayout(document: Document, output: File) {
62         transformerFactory.newTransformer().apply {
63             setOutputProperty(OutputKeys.INDENT, "yes")
64             transform(DOMSource(document), StreamResult(output))
65         }
66     }
67 
68     /**
69      * Generate files and return a mapping from File object to a structure defining useful
70      * information extracted from the input.
71      */
72     fun generateAllFiles(
73         containerFiles: List<File>,
74         childrenFiles: List<File>,
75         outputResourcesDir: File
76     ): GeneratedFiles {
77         val outputLayoutDir = outputResourcesDir.resolve("layout")
78         val outputLayoutDirS = outputResourcesDir.resolve("layout-v31")
79         val outputLayoutDirT = outputResourcesDir.resolve("layout-v33")
80         val outputValueDir = outputResourcesDir.resolve("values")
81         outputLayoutDir.mkdirs()
82         outputLayoutDirS.mkdirs()
83         outputLayoutDirT.mkdirs()
84         outputValueDir.mkdirs()
85         val generatedFiles =
86             generateSizeLayouts(outputLayoutDir) +
87                 generateComplexLayouts(outputLayoutDir) +
88                 generateChildIds(outputValueDir) +
89                 generateContainersChildrenForS(outputLayoutDirS) +
90                 generateContainersChildrenBeforeS(outputLayoutDir) +
91                 generateRootElements(outputLayoutDir) +
92                 generateRootAliases(outputValueDir) +
93                 generateViewIds(outputValueDir)
94         val topLevelLayouts = containerFiles + childrenFiles.filter { isTopLevelLayout(it) }
95         return GeneratedFiles(
96             generatedContainers =
97                 containerFiles.associateWith { generateContainers(it, outputLayoutDir) },
98             generatedBoxChildren =
99                 topLevelLayouts.associateWith { generateBoxChildrenForT(it, outputLayoutDirT) },
100             generatedRowColumnChildren =
101                 topLevelLayouts.associateWith {
102                     generateRowColumnChildrenForT(it, outputLayoutDirT)
103                 },
104             extraFiles = generatedFiles,
105         )
106     }
107 
108     private fun generateChildIds(outputValuesDir: File) =
109         generateRes(outputValuesDir, "child_ids") {
110             val containerSizes = listOf(ValidSize.Match, ValidSize.Wrap, ValidSize.Expand)
111             val root = createElement("resources")
112             appendChild(root)
113             repeat(MaxChildCount) { pos ->
114                 forEachInCrossProduct(containerSizes, containerSizes) { width, height ->
115                     val id = createElement("id")
116                     root.appendChild(id)
117                     id.attributes.apply {
118                         setNamedItem(attribute("name", makeIdName(pos, width, height)))
119                     }
120                 }
121             }
122         }
123 
124     private fun generateViewIds(outputValueDir: File) =
125         generateRes(outputValueDir, "view_ids") {
126             val root = createElement("resources")
127             appendChild(root)
128             repeat(TotalViewCount) {
129                 val id =
130                     createElement("id").apply {
131                         attributes.setNamedItem(attribute("name", makeViewIdResourceName(it)))
132                     }
133                 root.appendChild(id)
134             }
135         }
136 
137     private fun generateContainersChildrenForS(outputLayoutDir: File) =
138         generateContainersChildren(outputLayoutDir, listOf(ValidSize.Wrap))
139 
140     private fun generateContainersChildrenBeforeS(outputLayoutDir: File) =
141         generateContainersChildren(outputLayoutDir, StubSizes)
142 
143     private fun generateContainersChildren(
144         outputLayoutDir: File,
145         containerSizes: List<ValidSize>,
146     ) =
147         ContainerOrientation.values()
148             .flatMap { orientation ->
149                 generateContainersChildren(
150                     outputLayoutDir,
151                     containerSizes,
152                     orientation,
153                 )
154             }
155             .toSet()
156 
157     private fun generateContainersChildren(
158         outputLayoutDir: File,
159         sizes: List<ValidSize>,
160         containerOrientation: ContainerOrientation
161     ): Set<File> {
162         val widths = sizes + containerOrientation.extraWidths
163         val heights = sizes + containerOrientation.extraHeights
164         val alignments = containerOrientation.alignments
165         return (0 until MaxChildCount)
166             .flatMap { pos ->
167                 alignments.map { (horizontalAlignment, verticalAlignment) ->
168                     generateRes(
169                         outputLayoutDir,
170                         makeChildResourceName(
171                             pos,
172                             containerOrientation,
173                             horizontalAlignment,
174                             verticalAlignment
175                         )
176                     ) {
177                         val root = createElement("merge")
178                         appendChild(root)
179                         forEachInCrossProduct(widths, heights) { width, height ->
180                             val childId = makeIdName(pos, width, height)
181                             root.appendChild(
182                                 makeStub(
183                                     childId,
184                                     width,
185                                     height,
186                                     horizontalAlignment,
187                                     verticalAlignment,
188                                 )
189                             )
190                         }
191                     }
192                 }
193             }
194             .toSet()
195     }
196 
197     private fun Document.makeStub(
198         name: String,
199         width: ValidSize,
200         height: ValidSize,
201         horizontalAlignment: HorizontalAlignment?,
202         verticalAlignment: VerticalAlignment?
203     ) =
204         createElement("ViewStub").apply {
205             attributes.apply {
206                 setNamedItemNS(androidId("@id/$name"))
207                 setNamedItemNS(androidWidth(width))
208                 setNamedItemNS(androidHeight(height))
209                 if (width == ValidSize.Expand || height == ValidSize.Expand) {
210                     setNamedItemNS(androidWeight(1))
211                 }
212                 setNamedItemNS(
213                     androidGravity(
214                         listOfNotNull(
215                                 horizontalAlignment?.resourceName,
216                                 verticalAlignment?.resourceName
217                             )
218                             .joinToString(separator = "|")
219                     )
220                 )
221             }
222         }
223 
224     private fun generateSizeLayouts(outputLayoutDir: File): Set<File> {
225         val stubSizes = listOf(ValidSize.Wrap, ValidSize.Match)
226         return mapInCrossProduct(stubSizes, stubSizes) { width, height ->
227                 generateRes(outputLayoutDir, "size_${width.resourceName}_${height.resourceName}") {
228                     val root = createElement("TextView")
229                     appendChild(root)
230                     root.attributes.apply {
231                         setNamedItem(androidNamespace)
232                         setNamedItemNS(androidWidth(width))
233                         setNamedItemNS(androidHeight(height))
234                     }
235                 }
236             }
237             .toSet()
238     }
239 
240     /**
241      * Generate the various layouts needed for complex layouts.
242      *
243      * These layouts can be used with any view when it is not enough to have the naked view.
244      * Currently, it only creates layouts to allow resizing views on API 30-. In the generated
245      * layout, there is always a `ViewStub` with id `@id/glanceViewStub`, which needs to be replaced
246      * with the actual view.
247      *
248      * The skeleton is:
249      *
250      * <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
251      * android:id="@id/relativeLayout" android:layout_height="wrap_content"
252      * android:layout_width="wrap_content"> <FrameLayout android:id="@id/sizeView"
253      * android:layout_height="wrap_content" android:layout_width="wrap_content"> <ViewStub
254      * android:id="@id/sizeViewStub" android:layout_height="wrap_content"
255      * android:layout_width="wrap_content"/> </FrameLayout> <ViewStub
256      * android:id="@id/glanceViewStub" android:layout_height="wrap_content"
257      * android:layout_width="wrap_content"/> </RelativeLayout>
258      *
259      * With the `sizeView` frame layout only present if either dimension needs to be fixed.
260      */
261     private fun generateComplexLayouts(outputLayoutDir: File): Set<File> =
262         mapConfiguration { width, height ->
263                 generateRes(outputLayoutDir, makeComplexResourceName(width, height)) {
264                     val root = createElement("RelativeLayout")
265                     appendChild(root)
266                     root.attributes.apply {
267                         setNamedItemNS(androidId("@id/relativeLayout"))
268                         setNamedItemNS(androidAttr("tag", "glanceComplexLayout"))
269                         setNamedItemNS(androidWidth(width))
270                         setNamedItemNS(androidHeight(height))
271                         if (width == ValidSize.Expand || height == ValidSize.Expand) {
272                             setNamedItemNS(androidWeight(1))
273                         }
274                     }
275 
276                     if (width == ValidSize.Fixed || height == ValidSize.Fixed) {
277                         // A sizing view is only required if the width or height are fixed.
278                         val sizeView = createElement("FrameLayout")
279                         root.appendChild(sizeView)
280                         sizeView.attributes.apply {
281                             setNamedItemNS(androidId("@id/sizeView"))
282                             setNamedItemNS(androidAttr("visibility", "invisible"))
283                             setNamedItemNS(androidWidth(ValidSize.Wrap))
284                             setNamedItemNS(androidHeight(ValidSize.Wrap))
285                         }
286                         val sizeViewStub = createElement("ViewStub")
287                         sizeView.appendChild(sizeViewStub)
288                         sizeViewStub.attributes.apply {
289                             setNamedItemNS(androidId("@id/sizeViewStub"))
290                             setNamedItemNS(androidWidth(ValidSize.Wrap))
291                             setNamedItemNS(androidHeight(ValidSize.Wrap))
292                         }
293                     }
294 
295                     val glanceViewStub = createElement("ViewStub")
296                     root.appendChild(glanceViewStub)
297                     glanceViewStub.attributes.apply {
298                         setNamedItemNS(androidId("@id/glanceViewStub"))
299                         when (width) {
300                             ValidSize.Wrap -> setNamedItemNS(androidWidth(ValidSize.Wrap))
301                             ValidSize.Match,
302                             ValidSize.Expand -> {
303                                 setNamedItemNS(androidWidth(ValidSize.Match))
304                             }
305                             ValidSize.Fixed -> {
306                                 // If the view's height is fixed, its height is determined by
307                                 // sizeView.
308                                 // Use 0dp width for efficiency.
309                                 setNamedItemNS(androidWidth(ValidSize.Expand))
310                                 setNamedItemNS(androidAttr("layout_alignLeft", "@id/sizeView"))
311                                 setNamedItemNS(androidAttr("layout_alignRight", "@id/sizeView"))
312                             }
313                         }
314                         when (height) {
315                             ValidSize.Wrap -> setNamedItemNS(androidHeight(ValidSize.Wrap))
316                             ValidSize.Match,
317                             ValidSize.Expand -> {
318                                 setNamedItemNS(androidHeight(ValidSize.Match))
319                             }
320                             ValidSize.Fixed -> {
321                                 // If the view's height is fixed, its height is determined by
322                                 // sizeView.
323                                 // Use 0dp width for efficiency.
324                                 setNamedItemNS(androidHeight(ValidSize.Expand))
325                                 setNamedItemNS(androidAttr("layout_alignTop", "@id/sizeView"))
326                                 setNamedItemNS(androidAttr("layout_alignBottom", "@id/sizeView"))
327                             }
328                         }
329                     }
330                 }
331             }
332             .toSet()
333 
334     private fun generateRootElements(outputLayoutDir: File): Set<File> =
335         mapInCrossProduct(StubSizes, StubSizes) { width, height ->
336                 outputLayoutDir.resolveRes(makeRootResourceName(width, height)).also { output ->
337                     writeGeneratedLayout(createRootElement(width, height), output)
338                 }
339             }
340             .toSet()
341 
342     private fun createRootElement(width: ValidSize, height: ValidSize) =
343         documentBuilder.newDocument().apply {
344             val root = createElement("FrameLayout")
345             appendChild(root)
346             root.attributes.apply {
347                 setNamedItemNS(androidId("@id/rootView"))
348                 setNamedItemNS(androidWidth(width))
349                 setNamedItemNS(androidHeight(height))
350                 setNamedItemNS(androidAttr("theme", "@style/Glance.AppWidget.Theme"))
351             }
352             val stub = createElement("ViewStub")
353             root.appendChild(stub)
354             stub.attributes.apply {
355                 setNamedItemNS(androidId("@id/rootStubId"))
356                 setNamedItemNS(androidWidth(width))
357                 setNamedItemNS(androidHeight(height))
358             }
359         }
360 
361     private fun generateContainers(
362         file: File,
363         outputLayoutDir: File,
364     ): List<ContainerProperties> {
365         val document = parseLayoutTemplate(file)
366         val orientation =
367             when (document.documentElement.androidAttr("orientation")?.nodeValue) {
368                 "horizontal" -> ContainerOrientation.Horizontal
369                 "vertical" -> ContainerOrientation.Vertical
370                 null -> ContainerOrientation.None
371                 else -> throw IllegalStateException("Unknown orientation in $file")
372             }
373         val alignments = orientation.alignments
374         return (0..MaxChildCount).flatMap { numChildren ->
375             alignments.map { (horizontalAlignment, verticalAlignment) ->
376                 val generated =
377                     generateContainer(
378                         document,
379                         numChildren,
380                         orientation,
381                         horizontalAlignment,
382                         verticalAlignment,
383                     )
384                 val output =
385                     outputLayoutDir.resolveRes(
386                         makeContainerResourceName(
387                             file,
388                             numChildren,
389                             horizontalAlignment,
390                             verticalAlignment
391                         )
392                     )
393                 writeGeneratedLayout(generated, output)
394                 ContainerProperties(
395                     output,
396                     numChildren,
397                     orientation,
398                     horizontalAlignment,
399                     verticalAlignment,
400                 )
401             }
402         }
403     }
404 
405     private fun generateContainer(
406         inputDoc: Document,
407         numberChildren: Int,
408         containerOrientation: ContainerOrientation,
409         horizontalAlignment: HorizontalAlignment?,
410         verticalAlignment: VerticalAlignment?,
411     ) =
412         documentBuilder.newDocument().apply {
413             val root = importNode(inputDoc.documentElement, true)
414             appendChild(root)
415             root.attributes.apply {
416                 setNamedItemNS(androidWidth(ValidSize.Wrap))
417                 setNamedItemNS(androidHeight(ValidSize.Wrap))
418             }
419             for (pos in 0 until numberChildren) {
420                 root.appendChild(
421                     createElement("include").apply {
422                         attributes.apply {
423                             setNamedItem(
424                                 attribute(
425                                     "layout",
426                                     "@layout/${
427                                     makeChildResourceName(
428                                         pos,
429                                         containerOrientation,
430                                         horizontalAlignment,
431                                         verticalAlignment
432                                     )
433                                 }"
434                                 )
435                             )
436                         }
437                     }
438                 )
439             }
440         }
441 
442     private fun generateBoxChildrenForT(
443         file: File,
444         outputLayoutDir: File,
445     ): List<BoxChildProperties> =
446         crossProduct(HorizontalAlignment.values().toList(), VerticalAlignment.values().toList())
447             .map { (horizontalAlignment, verticalAlignment) ->
448                 val generated =
449                     generateAlignedLayout(
450                         parseLayoutTemplate(file),
451                         horizontalAlignment,
452                         verticalAlignment,
453                     )
454                 val output =
455                     outputLayoutDir.resolveRes(
456                         makeBoxChildResourceName(file, horizontalAlignment, verticalAlignment)
457                     )
458                 writeGeneratedLayout(generated, output)
459                 BoxChildProperties(output, horizontalAlignment, verticalAlignment)
460             }
461 
462     private fun generateRowColumnChildrenForT(
463         file: File,
464         outputLayoutDir: File,
465     ): List<RowColumnChildProperties> =
466         listOf(
467                 Pair(ValidSize.Expand, ValidSize.Wrap),
468                 Pair(ValidSize.Wrap, ValidSize.Expand),
469             )
470             .map { (width, height) ->
471                 val generated = generateSimpleLayout(parseLayoutTemplate(file), width, height)
472                 val output =
473                     outputLayoutDir.resolveRes(makeRowColumnChildResourceName(file, width, height))
474                 writeGeneratedLayout(generated, output)
475                 RowColumnChildProperties(output, width, height)
476             }
477 
478     /**
479      * Generate a simple layout.
480      *
481      * A simple layout only contains the view itself, set up for a given width and height. On
482      * Android R-, simple layouts are non-resizable.
483      */
484     private fun generateSimpleLayout(
485         document: Document,
486         width: ValidSize,
487         height: ValidSize,
488     ): Document {
489         val generated = documentBuilder.newDocument()
490         val root = generated.importNode(document.documentElement, true)
491         generated.appendChild(root)
492         root.attributes.apply {
493             setNamedItem(generated.androidNamespace)
494             if (root.androidId == null) {
495                 setNamedItemNS(generated.androidId("@id/glanceView"))
496             }
497             setNamedItemNS(generated.androidWidth(width))
498             setNamedItemNS(generated.androidHeight(height))
499             if (width == ValidSize.Expand || height == ValidSize.Expand) {
500                 setNamedItemNS(generated.androidWeight(1))
501             }
502             setNamedItemNS(generated.androidLayoutDirection("locale"))
503         }
504         return generated
505     }
506 
507     /**
508      * This function is used to generate FrameLayout children with "layout_gravity" set for Android
509      * T+. We can ignore size here since it is set programmatically for T+.
510      */
511     private fun generateAlignedLayout(
512         document: Document,
513         horizontalAlignment: HorizontalAlignment,
514         verticalAlignment: VerticalAlignment,
515     ) =
516         generateSimpleLayout(document, ValidSize.Wrap, ValidSize.Wrap).apply {
517             documentElement.attributes.setNamedItemNS(
518                 androidGravity(
519                     listOfNotNull(horizontalAlignment.resourceName, verticalAlignment.resourceName)
520                         .joinToString(separator = "|")
521                 )
522             )
523         }
524 
525     private fun generateRootAliases(outputValueDir: File) =
526         generateRes(outputValueDir, "layouts") {
527             val root = createElement("resources")
528             appendChild(root)
529             val sizes = crossProduct(StubSizes, StubSizes)
530             val numStubs = sizes.size
531             repeat(RootLayoutAliasCount) { aliasIndex ->
532                 sizes.forEachIndexed() { index, (width, height) ->
533                     val fullIndex = aliasIndex * numStubs + index
534                     val alias =
535                         createElement("item").apply {
536                             attributes.apply {
537                                 setNamedItem(
538                                     attribute("name", makeRootAliasResourceName(fullIndex))
539                                 )
540                                 setNamedItem(attribute("type", "layout"))
541                             }
542                             textContent = "@layout/${makeRootResourceName(width, height)}"
543                         }
544                     root.appendChild(alias)
545                 }
546             }
547         }
548 
549     private inline fun generateRes(
550         outputDir: File,
551         resName: String,
552         builder: Document.() -> Unit
553     ): File {
554         val document = documentBuilder.newDocument()
555         val file = outputDir.resolveRes(resName)
556         builder(document)
557         writeGeneratedLayout(document, file)
558         return file
559     }
560 
561     private fun isTopLevelLayout(file: File) =
562         parseLayoutTemplate(file).run {
563             documentElement.appAttr("glance_isTopLevelLayout")?.nodeValue == "true"
564         }
565 }
566 
567 /** Maximum number of children generated in containers. */
568 private const val MaxChildCount = 10
569 
570 /**
571  * Number of aliases for the root view. As pre-S we need four aliases per position, effectively 4
572  * times that number will be generated.
573  */
574 internal const val RootLayoutAliasCount = 100
575 
576 /**
577  * Number of View IDs that will be generated for use throughout the UI layout. This number
578  * determines the maximum number of total views a layout may contain.
579  */
580 internal const val TotalViewCount = 500
581 
582 internal data class GeneratedFiles(
583     val generatedContainers: Map<File, List<ContainerProperties>>,
584     val generatedBoxChildren: Map<File, List<BoxChildProperties>>,
585     val generatedRowColumnChildren: Map<File, List<RowColumnChildProperties>>,
586     val extraFiles: Set<File>
587 )
588 
589 internal data class ChildProperties(
590     val childId: String,
591     val width: ValidSize,
592     val height: ValidSize,
593 )
594 
595 internal data class ContainerProperties(
596     val generatedFile: File,
597     val numberChildren: Int,
598     val containerOrientation: ContainerOrientation,
599     val horizontalAlignment: HorizontalAlignment?,
600     val verticalAlignment: VerticalAlignment?,
601 )
602 
603 internal data class BoxChildProperties(
604     val generatedFile: File,
605     val horizontalAlignment: HorizontalAlignment,
606     val verticalAlignment: VerticalAlignment,
607 )
608 
609 internal data class RowColumnChildProperties(
610     val generatedFile: File,
611     val width: ValidSize,
612     val height: ValidSize,
613 )
614 
615 internal enum class ValidSize(val androidValue: String, val resourceName: String) {
616     Wrap("wrap_content", "wrap"),
617     Fixed("wrap_content", "fixed"),
618     Match("match_parent", "match"),
619     Expand("0dp", "expand"),
620 }
621 
622 internal enum class ContainerOrientation(
623     val resourceName: String,
624     val extraWidths: List<ValidSize>,
625     val extraHeights: List<ValidSize>
626 ) {
627     None("box", emptyList(), emptyList()),
628     Horizontal("row", listOf(ValidSize.Expand), emptyList()),
629     Vertical("column", emptyList(), listOf(ValidSize.Expand))
630 }
631 
632 internal val ContainerOrientation.alignments: List<Pair<HorizontalAlignment?, VerticalAlignment?>>
633     get() =
634         when (this) {
635             ContainerOrientation.None -> {
636                 crossProduct(
637                     HorizontalAlignment.values().toList(),
638                     VerticalAlignment.values().toList()
639                 )
640             }
<lambda>null641             ContainerOrientation.Horizontal -> VerticalAlignment.values().map { null to it }
<lambda>null642             ContainerOrientation.Vertical -> HorizontalAlignment.values().map { it to null }
643         }
644 
645 internal enum class HorizontalAlignment(
646     val resourceName: String,
647     val code: MemberName,
648 ) {
649     Start("start", AlignmentStart),
650     Center("center_horizontal", AlignmentCenterHorizontally),
651     End("end", AlignmentEnd),
652 }
653 
654 internal enum class VerticalAlignment(
655     val resourceName: String,
656     val code: MemberName,
657 ) {
658     Top("top", AlignmentTop),
659     Center("center_vertical", AlignmentCenterVertically),
660     Bottom("bottom", AlignmentBottom),
661 }
662 
663 /** Sizes a ViewStub can meaningfully have, if expand is not an option. */
664 internal val StubSizes = listOf(ValidSize.Wrap, ValidSize.Match)
665 
getChildMergeFilenameWithoutExtensionnull666 internal fun getChildMergeFilenameWithoutExtension(childCount: Int) = "merge_${childCount}child"
667 
668 private val AndroidNS = "http://schemas.android.com/apk/res/android"
669 private val AppNS = "http://schemas.android.com/apk/res-auto"
670 
671 internal fun Document.androidAttr(name: String, value: String) =
672     createAttributeNS(AndroidNS, "android:$name").apply { textContent = value }
673 
androidAttrnull674 internal fun Node.androidAttr(name: String): Node? = attributes.getNamedItemNS(AndroidNS, name)
675 
676 internal fun Node.appAttr(name: String): Node? = attributes.getNamedItemNS(AppNS, name)
677 
678 internal fun Document.attribute(name: String, value: String): Node? =
679     createAttribute(name).apply { textContent = value }
680 
androidIdnull681 internal fun Document.androidId(value: String) = androidAttr("id", value)
682 
683 internal val Node.androidId: Node?
684     get() = androidAttr("id")
685 
686 internal fun Document.androidWidth(value: ValidSize) =
687     androidAttr("layout_width", value.androidValue)
688 
689 internal fun Document.androidHeight(value: ValidSize) =
690     androidAttr("layout_height", value.androidValue)
691 
692 internal fun Document.androidWeight(value: Int) = androidAttr("layout_weight", value.toString())
693 
694 internal fun Document.androidGravity(value: String) = androidAttr("layout_gravity", value)
695 
696 internal fun Document.androidLayoutDirection(value: String) = androidAttr("layoutDirection", value)
697 
698 internal val Document.androidNamespace
699     get() = createAttribute("xmlns:android").apply { textContent = AndroidNS }
700