1 /*
2  * Copyright (C) 2023 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 com.android.launcher3.responsive
18 
19 import android.content.res.TypedArray
20 import android.content.res.XmlResourceParser
21 import android.util.Xml
22 import com.android.launcher3.R
23 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
24 import com.android.launcher3.util.ResourceHelper
25 import java.io.IOException
26 import org.xmlpull.v1.XmlPullParser
27 import org.xmlpull.v1.XmlPullParserException
28 
29 class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
30 
parseXMLnull31     fun <T : IResponsiveSpec> parseXML(
32         responsiveSpecType: ResponsiveSpecType,
33         map:
34             (
35                 responsiveSpecType: ResponsiveSpecType,
36                 attributes: TypedArray,
37                 sizeSpecs: Map<String, SizeSpec>
38             ) -> T
39     ): List<ResponsiveSpecGroup<T>> {
40         val parser: XmlResourceParser = resourceHelper.getXml()
41 
42         try {
43             val groups = mutableListOf<ResponsiveSpecGroup<T>>()
44             val specs = mutableListOf<T>()
45             var groupAttrs: TypedArray? = null
46 
47             var eventType = parser.eventType
48             while (eventType != XmlPullParser.END_DOCUMENT) {
49                 // Parsing Group
50                 when {
51                     parser starts ResponsiveSpecGroup.XML_GROUP_NAME -> {
52                         groupAttrs =
53                             resourceHelper.obtainStyledAttributes(
54                                 Xml.asAttributeSet(parser),
55                                 R.styleable.ResponsiveSpecGroup
56                             )
57                     }
58                     parser ends ResponsiveSpecGroup.XML_GROUP_NAME -> {
59                         checkNotNull(groupAttrs)
60                         groups += ResponsiveSpecGroup.create(groupAttrs, specs)
61                         specs.clear()
62                         groupAttrs.recycle()
63                         groupAttrs = null
64                     }
65                     // Mapping Spec to WorkspaceSpec, AllAppsSpec, FolderSpecs, HotseatSpec
66                     parser starts responsiveSpecType.xmlTag -> {
67                         val attrs =
68                             resourceHelper.obtainStyledAttributes(
69                                 Xml.asAttributeSet(parser),
70                                 R.styleable.ResponsiveSpec
71                             )
72 
73                         val sizeSpecs = parseSizeSpecs(parser)
74                         specs += map(responsiveSpecType, attrs, sizeSpecs)
75                         attrs.recycle()
76                     }
77                 }
78 
79                 eventType = parser.next()
80             }
81 
82             parser.close()
83 
84             // All the specs should have been linked to a group, otherwise the XML is invalid
85             check(specs.isEmpty()) {
86                 throw InvalidResponsiveGridSpec(
87                     "Invalid XML. ${specs.size} specs not linked to a group."
88                 )
89             }
90 
91             return groups
92         } catch (e: Exception) {
93             when (e) {
94                 is NoSuchFieldException,
95                 is IOException,
96                 is XmlPullParserException ->
97                     throw RuntimeException("Failure parsing specs file.", e)
98                 else -> throw e
99             }
100         } finally {
101             parser.close()
102         }
103     }
104 
parseSizeSpecsnull105     private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
106         val parentName = parser.name
107         parser.next()
108 
109         val result = mutableMapOf<String, SizeSpec>()
110         while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
111             if (parser.eventType == XmlResourceParser.START_TAG) {
112                 result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
113             }
114             parser.next()
115         }
116 
117         return result
118     }
119 
startsnull120     private infix fun XmlResourceParser.starts(tag: String): Boolean =
121         name == tag && eventType == XmlPullParser.START_TAG
122 
123     private infix fun XmlResourceParser.ends(tag: String): Boolean =
124         name == tag && eventType == XmlPullParser.END_TAG
125 }
126 
127 fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
128     return this.getOrElse(key) { error("Attr '$key' must be defined.") }
129 }
130 
131 class InvalidResponsiveGridSpec(message: String) : Exception(message)
132