1#!/usr/bin/python3
2
3#
4# Copyright 2018, The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19import argparse
20import re
21import sys
22import os
23import logging
24import xml.etree.ElementTree as ET
25import xml.etree.ElementInclude as EI
26import xml.dom.minidom as MINIDOM
27from collections import OrderedDict
28
29#
30# Helper script that helps to feed at build time the XML criterion types file used by
31# the engineconfigurable to start the parameter-framework.
32# It prevents to fill them manually and avoid divergences with android.
33#
34# The Device Types criterion types are fed from audio-base.h file with the option
35#           --androidaudiobaseheader <path/to/android/audio/base/file/audio-base.h>
36#
37# The Device Addresses criterion types are fed from the audio policy configuration file
38# in order to discover all the devices for which the address matter.
39#           --audiopolicyconfigurationfile <path/to/audio_policy_configuration.xml>
40#
41# The reference file of criterion types must also be set as an input of the script:
42#           --criteriontypes <path/to/criterion/file/audio_criterion_types.xml.in>
43#
44# At last, the output of the script shall be set also:
45#           --outputfile <path/to/out/vendor/etc/audio_criterion_types.xml>
46#
47
48def parseArgs():
49    argparser = argparse.ArgumentParser(description="Parameter-Framework XML \
50                                        audio criterion type file generator.\n\
51                                        Exit with the number of (recoverable or not) \
52                                        error that occured.")
53    argparser.add_argument('--androidaudiobaseheader',
54                           help="Android Audio Base C header file, Mandatory.",
55                           metavar="ANDROID_AUDIO_BASE_HEADER",
56                           type=argparse.FileType('r'),
57                           required=True)
58    argparser.add_argument('--audiopolicyconfigurationfile',
59                           help="Android Audio Policy Configuration file, Mandatory.",
60                           metavar="(AUDIO_POLICY_CONFIGURATION_FILE)",
61                           type=argparse.FileType('r'),
62                           required=True)
63    argparser.add_argument('--criteriontypes',
64                           help="Criterion types XML base file, in \
65                           '<criterion_types> \
66                               <criterion_type name="" type=<inclusive|exclusive> \
67                               values=<value1,value2,...>/>' \
68                           format. Mandatory.",
69                           metavar="CRITERION_TYPE_FILE",
70                           type=argparse.FileType('r'),
71                           required=True)
72    argparser.add_argument('--outputfile',
73                           help="Criterion types outputfile file. Mandatory.",
74                           metavar="CRITERION_TYPE_OUTPUT_FILE",
75                           type=argparse.FileType('w'),
76                           required=True)
77    argparser.add_argument('--verbose',
78                           action='store_true')
79
80    return argparser.parse_args()
81
82
83def generateXmlCriterionTypesFile(criterionTypes, addressCriteria, criterionTypesFile, outputFile):
84
85    logging.info("Importing criterionTypesFile {}".format(criterionTypesFile))
86    criterion_types_in_tree = ET.parse(criterionTypesFile)
87
88    criterion_types_root = criterion_types_in_tree.getroot()
89
90    for criterion_name, values_dict in criterionTypes.items():
91        for criterion_type in criterion_types_root.findall('criterion_type'):
92            if criterion_type.get('name') == criterion_name:
93                values_node = ET.SubElement(criterion_type, "values")
94                ordered_values = OrderedDict(sorted(values_dict.items(), key=lambda x: x[1]))
95                for key, value in ordered_values.items():
96                    value_node = ET.SubElement(values_node, "value")
97                    value_node.set('numerical', str(value))
98                    value_node.set('literal', key)
99
100    if addressCriteria:
101        for criterion_name, values_list in addressCriteria.items():
102            for criterion_type in criterion_types_root.findall('criterion_type'):
103                if criterion_type.get('name') == criterion_name:
104                    index = 0
105                    existing_values_node = criterion_type.find("values")
106                    if existing_values_node is not None:
107                        for existing_value in existing_values_node.findall('value'):
108                            if existing_value.get('numerical') == str(1 << index):
109                                index += 1
110                        values_node = existing_values_node
111                    else:
112                        values_node = ET.SubElement(criterion_type, "values")
113
114                    for value in values_list:
115                        value_node = ET.SubElement(values_node, "value", literal=value)
116                        value_node.set('numerical', str(1 << index))
117                        index += 1
118
119    xmlstr = ET.tostring(criterion_types_root, encoding='utf8', method='xml')
120    reparsed = MINIDOM.parseString(xmlstr)
121    prettyXmlStr = reparsed.toprettyxml(newl='\r\n')
122    prettyXmlStr = os.linesep.join([s for s in prettyXmlStr.splitlines() if s.strip()])
123    outputFile.write(prettyXmlStr)
124
125def capitalizeLine(line):
126    return ' '.join((w.capitalize() for w in line.split(' ')))
127
128
129#
130# Parse the audio policy configuration file and output a dictionary of device criteria addresses
131#
132def parseAndroidAudioPolicyConfigurationFile(audiopolicyconfigurationfile):
133
134    logging.info("Checking Audio Policy Configuration file {}".format(audiopolicyconfigurationfile))
135    #
136    # extract all devices addresses from audio policy configuration file
137    #
138    address_criteria_mapping_table = {
139        'sink' : "OutputDevicesAddressesType",
140        'source' : "InputDevicesAddressesType"}
141
142    address_criteria = {
143        'OutputDevicesAddressesType' : [],
144        'InputDevicesAddressesType' : []}
145
146    old_working_dir = os.getcwd()
147    print("Current working directory %s" % old_working_dir)
148
149    new_dir = os.path.join(old_working_dir, audiopolicyconfigurationfile.name)
150
151    policy_in_tree = ET.parse(audiopolicyconfigurationfile)
152    os.chdir(os.path.dirname(os.path.normpath(new_dir)))
153
154    print("new working directory %s" % os.getcwd())
155
156    policy_root = policy_in_tree.getroot()
157    EI.include(policy_root)
158
159    os.chdir(old_working_dir)
160
161    for device in policy_root.iter('devicePort'):
162        for key in address_criteria_mapping_table.keys():
163            if device.get('role') == key and device.get('address'):
164                logging.info("{}: <{}>".format(key, device.get('address')))
165                address_criteria[address_criteria_mapping_table[key]].append(device.get('address'))
166
167    for criteria in address_criteria:
168        values = ','.join(address_criteria[criteria])
169        logging.info("{}: <{}>".format(criteria, values))
170
171    return address_criteria
172
173#
174# Parse the audio-base.h file and output a dictionary of android dependent criterion types:
175#   -Android Mode
176#   -Output devices type
177#   -Input devices type
178#
179def parseAndroidAudioFile(androidaudiobaseheaderFile):
180    #
181    # Adaptation table between Android Enumeration prefix and Audio PFW Criterion type names
182    #
183    criterion_mapping_table = {
184        'AUDIO_MODE' : "AndroidModeType",
185        'AUDIO_DEVICE_OUT' : "OutputDevicesMaskType",
186        'AUDIO_DEVICE_IN' : "InputDevicesMaskType"}
187
188    all_criteria = {
189        'AndroidModeType' : {},
190        'OutputDevicesMaskType' : {},
191        'InputDevicesMaskType' : {}}
192
193    #
194    # _CNT, _MAX, _ALL and _NONE are prohibited values as ther are just helpers for enum users.
195    #
196    ignored_values = ['CNT', 'MAX', 'ALL', 'NONE']
197
198    criteria_pattern = re.compile(
199        r"\s*(?P<type>(?:"+'|'.join(criterion_mapping_table.keys()) + "))_" \
200        r"(?P<literal>(?!" + '|'.join(ignored_values) + ")\w*)\s*=\s*" \
201        r"(?P<values>(?:0[xX])?[0-9a-fA-F]+)")
202
203    logging.info("Checking Android Header file {}".format(androidaudiobaseheaderFile))
204
205    for line_number, line in enumerate(androidaudiobaseheaderFile):
206        match = criteria_pattern.match(line)
207        if match:
208            logging.debug("The following line is VALID: {}:{}\n{}".format(
209                androidaudiobaseheaderFile.name, line_number, line))
210
211            criterion_name = criterion_mapping_table[match.groupdict()['type']]
212            literal = ''.join((w.capitalize() for w in match.groupdict()['literal'].split('_')))
213            numerical_value = match.groupdict()['values']
214
215            # for AUDIO_DEVICE_IN: need to remove sign bit
216            if criterion_name == "InputDevicesMaskType":
217                numerical_value = str(int(numerical_value, 0) & ~2147483648)
218
219            # Remove duplicated numerical values
220            if int(numerical_value, 0) in all_criteria[criterion_name].values():
221                logging.info("criterion {} duplicated values:".format(criterion_name))
222                logging.info("{}:{}".format(numerical_value, literal))
223                logging.info("KEEPING LATEST")
224                for key in list(all_criteria[criterion_name]):
225                    if all_criteria[criterion_name][key] == int(numerical_value, 0):
226                        del all_criteria[criterion_name][key]
227
228            all_criteria[criterion_name][literal] = int(numerical_value, 0)
229
230            logging.debug("type:{},".format(criterion_name))
231            logging.debug("iteral:{},".format(literal))
232            logging.debug("values:{}.".format(numerical_value))
233
234    return all_criteria
235
236
237def main():
238    logging.root.setLevel(logging.INFO)
239    args = parseArgs()
240
241    all_criteria = parseAndroidAudioFile(args.androidaudiobaseheader)
242
243    address_criteria = parseAndroidAudioPolicyConfigurationFile(args.audiopolicyconfigurationfile)
244
245    criterion_types = args.criteriontypes
246
247    generateXmlCriterionTypesFile(all_criteria, address_criteria, criterion_types, args.outputfile)
248
249# If this file is directly executed
250if __name__ == "__main__":
251    sys.exit(main())
252