1 /*
2  * Copyright (C) 2018 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 package com.android.game.qualification;
17 
18 import org.w3c.dom.Document;
19 import org.w3c.dom.Element;
20 import org.w3c.dom.Node;
21 import org.w3c.dom.NodeList;
22 import org.xml.sax.SAXException;
23 
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.function.Function;
33 
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 
38 /**
39  * Parser to read apk-info.xml.
40  */
41 public class GameCoreConfigurationXmlParser {
42 
43     public enum Field {
44         NAME("name", null),
45 
46         // Certification requirements
47         TARGET_FRAME_TIME("frameTime", "16.666"),
48         MAX_JANK_RATE("jankRate", "0.0"),
49         MAX_LOAD_TIME("loadTime", "-1"),
50 
51         // Apk info
52         FILE_NAME("fileName", null),
53         PACKAGE_NAME("packageName", null),
54         ACTIVITY_NAME("activityName", null),
55         LAYER_NAME("layerName", null),
56         SCRIPT("script", null),
57         ARGS("args", null),
58         LOAD_TIME("loadTime", "10000"),
59         RUN_TIME("runTime", "10000"),
60         EXPECT_INTENTS("expectIntents", "false");
61 
62         private String mTag;
63         private String mDefaultValue;
64 
Field(String tag, String defaultValue)65         Field(String tag, String defaultValue) {
66             mTag = tag;
67             mDefaultValue = defaultValue;
68         }
69 
getTag()70         public String getTag() {
71             return mTag;
72         }
73 
getDefaultValue()74         public String getDefaultValue() {
75             return mDefaultValue;
76         }
77     }
78 
GameCoreConfigurationXmlParser()79     public GameCoreConfigurationXmlParser() {
80     }
81 
parse(File file)82     public GameCoreConfiguration parse(File file)
83             throws IOException, ParserConfigurationException, SAXException {
84         try (InputStream is = new FileInputStream(file)) {
85             return parse(is);
86         }
87     }
88 
parse(InputStream inputStream)89     public GameCoreConfiguration parse(InputStream inputStream)
90             throws ParserConfigurationException, IOException, SAXException {
91         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
92         DocumentBuilder db = dbf.newDocumentBuilder();
93         Document doc = db.parse(inputStream);
94         doc.getDocumentElement().normalize();
95 
96         List<CertificationRequirements> requirements = parseList(
97                 doc.getDocumentElement(),
98                 "certification",
99                 this::createCertificationRequirements);
100         if (requirements == null) {
101             requirements = Collections.emptyList();
102         }
103         List<ApkInfo> apkInfo =
104                 parseList(doc.getDocumentElement(), "apk-info", this::createApkInfo);
105         return new GameCoreConfiguration(requirements, apkInfo);
106     }
107 
parseList(Element element, String tagName, Function<Element, T> parser)108     private <T> List<T> parseList(Element element, String tagName, Function<Element, T> parser) {
109         NodeList nodeList = element.getElementsByTagName(tagName);
110         if (nodeList.getLength() != 1) {
111             return null;
112         }
113         List<T> result = new ArrayList<>();
114         NodeList children = nodeList.item(0).getChildNodes();
115         for (int i = 0; i < children.getLength(); i++) {
116             Node child = children.item(i);
117             if (child.getNodeType() == Node.ELEMENT_NODE) {
118                 result.add(parser.apply((Element) child));
119             }
120         }
121         return result;
122     }
123 
createCertificationRequirements(Element element)124     private CertificationRequirements createCertificationRequirements(Element element) {
125         if (!element.getTagName().equals("apk")) {
126             throw new RuntimeException(
127                     "Unexpected tag <"
128                             + element.getNodeName()
129                             + "> in <certification>.  Only <apk> is allowed." );
130         }
131         return new CertificationRequirements(
132                 getElement(element, Field.NAME),
133                 Float.parseFloat(getElement(element, Field.TARGET_FRAME_TIME)),
134                 Float.parseFloat(getElement(element, Field.MAX_JANK_RATE)),
135                 Integer.parseInt(getElement(element, Field.MAX_LOAD_TIME)));
136     }
137 
createApkInfo(Element element)138     private ApkInfo createApkInfo(Element element) {
139         if (!element.getTagName().equals("apk")) {
140             throw new RuntimeException(
141                     "Unexpected tag <"
142                             + element.getNodeName()
143                             + "> in <apk-info>.  Only <apk> is allowed." );
144         }
145 
146         List<ApkInfo.Argument> args = parseList(element, Field.ARGS.getTag(), this::createArgument);
147         if (args == null) {
148             args = Collections.emptyList();
149         }
150 
151         return new ApkInfo(
152                 getElement(element, Field.NAME),
153                 getElement(element, Field.FILE_NAME),
154                 getElement(element, Field.PACKAGE_NAME),
155                 getElement(element, Field.ACTIVITY_NAME),
156                 getElement(element, Field.LAYER_NAME),
157                 getElement(element, Field.SCRIPT),
158                 args,
159                 Integer.parseInt(getElement(element, Field.LOAD_TIME)),
160                 Integer.parseInt(getElement(element, Field.RUN_TIME)),
161                 Boolean.parseBoolean(getElement(element, Field.EXPECT_INTENTS))
162                 );
163     }
164 
createArgument(Element element)165     private ApkInfo.Argument createArgument(Element element) {
166         String type = element.getAttribute("type");
167         if (type == null || type.isEmpty()) {
168             type = "STRING";
169         }
170         return new ApkInfo.Argument(
171                 element.getTagName(),
172                 element.getTextContent(),
173                 ApkInfo.Argument.Type.valueOf(type.toUpperCase(Locale.US)));
174     }
175 
getElement(Element element, Field field)176     private String getElement(Element element, Field field) {
177         NodeList elements = element.getElementsByTagName(field.getTag());
178         if (elements.getLength() > 0) {
179             return elements.item(0).getTextContent();
180         } else {
181             return field.getDefaultValue();
182         }
183     }
184 }
185