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('--androidaudiocommonbaseheader',
59                           help="Android Audio CommonBase C header file, Mandatory.",
60                           metavar="ANDROID_AUDIO_COMMON_BASE_HEADER",
61                           type=argparse.FileType('r'),
62                           required=True)
63    argparser.add_argument('--audiopolicyconfigurationfile',
64                           help="Android Audio Policy Configuration file, Mandatory.",
65                           metavar="(AUDIO_POLICY_CONFIGURATION_FILE)",
66                           type=argparse.FileType('r'),
67                           required=True)
68    argparser.add_argument('--criteriontypes',
69                           help="Criterion types XML base file, in \
70                           '<criterion_types> \
71                               <criterion_type name="" type=<inclusive|exclusive> \
72                               values=<value1,value2,...>/>' \
73                           format. Mandatory.",
74                           metavar="CRITERION_TYPE_FILE",
75                           type=argparse.FileType('r'),
76                           required=True)
77    argparser.add_argument('--outputfile',
78                           help="Criterion types outputfile file. Mandatory.",
79                           metavar="CRITERION_TYPE_OUTPUT_FILE",
80                           type=argparse.FileType('w'),
81                           required=True)
82    argparser.add_argument('--verbose',
83                           action='store_true')
84
85    return argparser.parse_args()
86
87
88output_devices_type_value = {}
89input_devices_type_value = {}
90
91def generateXmlCriterionTypesFile(criterionTypes, addressCriteria, criterionTypesFile, outputFile):
92
93    logging.info("Importing criterionTypesFile {}".format(criterionTypesFile))
94    criterion_types_in_tree = ET.parse(criterionTypesFile)
95
96    criterion_types_root = criterion_types_in_tree.getroot()
97
98    for criterion_name, values_dict in criterionTypes.items():
99        for criterion_type in criterion_types_root.findall('criterion_type'):
100            if criterion_type.get('name') == criterion_name:
101                values_node = ET.SubElement(criterion_type, "values")
102                ordered_values = OrderedDict(sorted(values_dict.items(), key=lambda x: x[1]))
103                for key, value in ordered_values.items():
104                    value_node = ET.SubElement(values_node, "value")
105                    value_node.set('numerical', str(value))
106                    value_node.set('literal', key)
107
108                    if criterion_type.get('name') == "OutputDevicesMaskType":
109                        value_node.set('android_type', output_devices_type_value[key])
110                    if criterion_type.get('name') == "InputDevicesMaskType":
111                        value_node.set('android_type', input_devices_type_value[key])
112
113    if addressCriteria:
114        for criterion_name, values_list in addressCriteria.items():
115            for criterion_type in criterion_types_root.findall('criterion_type'):
116                if criterion_type.get('name') == criterion_name:
117                    index = 0
118                    existing_values_node = criterion_type.find("values")
119                    if existing_values_node is not None:
120                        for existing_value in existing_values_node.findall('value'):
121                            if existing_value.get('numerical') == str(1 << index):
122                                index += 1
123                        values_node = existing_values_node
124                    else:
125                        values_node = ET.SubElement(criterion_type, "values")
126
127                    for value in values_list:
128                        value_node = ET.SubElement(values_node, "value", literal=value)
129                        value_node.set('numerical', str(1 << index))
130                        index += 1
131
132    xmlstr = ET.tostring(criterion_types_root, encoding='utf8', method='xml')
133    reparsed = MINIDOM.parseString(xmlstr)
134    prettyXmlStr = reparsed.toprettyxml(newl='\r\n')
135    prettyXmlStr = os.linesep.join([s for s in prettyXmlStr.splitlines() if s.strip()])
136    outputFile.write(prettyXmlStr)
137
138def capitalizeLine(line):
139    return ' '.join((w.capitalize() for w in line.split(' ')))
140
141
142#
143# Parse the audio policy configuration file and output a dictionary of device criteria addresses
144#
145def parseAndroidAudioPolicyConfigurationFile(audiopolicyconfigurationfile):
146
147    logging.info("Checking Audio Policy Configuration file {}".format(audiopolicyconfigurationfile))
148    #
149    # extract all devices addresses from audio policy configuration file
150    #
151    address_criteria_mapping_table = {
152        'sink' : "OutputDevicesAddressesType",
153        'source' : "InputDevicesAddressesType"}
154
155    address_criteria = {
156        'OutputDevicesAddressesType' : [],
157        'InputDevicesAddressesType' : []}
158
159    old_working_dir = os.getcwd()
160    print("Current working directory %s" % old_working_dir)
161
162    new_dir = os.path.join(old_working_dir, audiopolicyconfigurationfile.name)
163
164    policy_in_tree = ET.parse(audiopolicyconfigurationfile)
165    os.chdir(os.path.dirname(os.path.normpath(new_dir)))
166
167    print("new working directory %s" % os.getcwd())
168
169    policy_root = policy_in_tree.getroot()
170    EI.include(policy_root)
171
172    os.chdir(old_working_dir)
173
174    for device in policy_root.iter('devicePort'):
175        for key in address_criteria_mapping_table.keys():
176            if device.get('role') == key and device.get('address'):
177                logging.info("{}: <{}>".format(key, device.get('address')))
178                address_criteria[address_criteria_mapping_table[key]].append(device.get('address'))
179
180    for criteria in address_criteria:
181        values = ','.join(address_criteria[criteria])
182        logging.info("{}: <{}>".format(criteria, values))
183
184    return address_criteria
185
186#
187# Parse the audio-base.h file and output a dictionary of android dependent criterion types:
188#   -Android Mode
189#   -Output devices type
190#   -Input devices type
191#
192def parseAndroidAudioFile(androidaudiobaseheaderFile, androidaudiocommonbaseheaderFile):
193    #
194    # Adaptation table between Android Enumeration prefix and Audio PFW Criterion type names
195    #
196    criterion_mapping_table = {
197        'HAL_AUDIO_MODE' : "AndroidModeType",
198        'AUDIO_DEVICE_OUT' : "OutputDevicesMaskType",
199        'AUDIO_DEVICE_IN' : "InputDevicesMaskType"}
200
201    all_criteria = {
202        'AndroidModeType' : {},
203        'OutputDevicesMaskType' : {},
204        'InputDevicesMaskType' : {}}
205
206    #
207    # _CNT, _MAX, _ALL and _NONE are prohibited values as ther are just helpers for enum users.
208    #
209    ignored_values = ['CNT', 'MAX', 'ALL', 'NONE']
210
211    multi_bit_outputdevice_shift = 32
212    multi_bit_inputdevice_shift = 32
213
214    criteria_pattern = re.compile(
215        r"\s*V\((?P<type>(?:"+'|'.join(criterion_mapping_table.keys()) + "))_" \
216        r"(?P<literal>(?!" + '|'.join(ignored_values) + ")\w*)\s*,\s*" \
217        r"(?:AUDIO_DEVICE_BIT_IN \| )?(?P<values>(?:0[xX])?[0-9a-fA-F]+|[0-9]+)")
218
219    logging.info("Checking Android Header file {}".format(androidaudiobaseheaderFile))
220
221    for line_number, line in enumerate(androidaudiobaseheaderFile):
222        match = criteria_pattern.match(line)
223        if match:
224            logging.debug("The following line is VALID: {}:{}\n{}".format(
225                androidaudiobaseheaderFile.name, line_number, line))
226
227            criterion_name = criterion_mapping_table[match.groupdict()['type']]
228            criterion_literal = \
229                ''.join((w.capitalize() for w in match.groupdict()['literal'].split('_')))
230            criterion_numerical_value = match.groupdict()['values']
231
232            # for AUDIO_DEVICE_IN: rename default to stub
233            if criterion_name == "InputDevicesMaskType":
234                if criterion_literal == "Default":
235                    criterion_numerical_value = str(int("0x40000000", 0))
236                    input_devices_type_value[criterion_literal] = "0xC0000000"
237                else:
238                    try:
239                        string_int = int(criterion_numerical_value, 0)
240                        # Append AUDIO_DEVICE_IN for android type tag
241                        input_devices_type_value[criterion_literal] = hex(string_int | 2147483648)
242
243                        num_bits = bin(string_int).count("1")
244                        if num_bits > 1:
245                            logging.info("The value {}:{} is for criterion {} binary rep {} has {} bits sets"
246                                .format(criterion_numerical_value, criterion_literal, criterion_name, bin(string_int), num_bits))
247                            string_int = 2**multi_bit_inputdevice_shift
248                            logging.info("new val assigned is {} {}" .format(string_int, bin(string_int)))
249                            multi_bit_inputdevice_shift += 1
250                            criterion_numerical_value = str(string_int)
251
252                    except ValueError:
253                        # Handle the exception
254                        logging.info("value {}:{} for criterion {} is not a number, ignoring"
255                            .format(criterion_numerical_value, criterion_literal, criterion_name))
256                        continue
257
258            if criterion_name == "OutputDevicesMaskType":
259                if criterion_literal == "Default":
260                    criterion_numerical_value = str(int("0x40000000", 0))
261                    output_devices_type_value[criterion_literal] = "0x40000000"
262                else:
263                    try:
264                        string_int = int(criterion_numerical_value, 0)
265                        output_devices_type_value[criterion_literal] = criterion_numerical_value
266
267                        num_bits = bin(string_int).count("1")
268                        if num_bits > 1:
269                            logging.info("The value {}:{} is for criterion {} binary rep {} has {} bits sets"
270                                .format(criterion_numerical_value, criterion_literal, criterion_name, bin(string_int), num_bits))
271                            string_int = 2**multi_bit_outputdevice_shift
272                            logging.info("new val assigned is {} {}" .format(string_int, bin(string_int)))
273                            multi_bit_outputdevice_shift += 1
274                            criterion_numerical_value = str(string_int)
275
276                    except ValueError:
277                        # Handle the exception
278                        logging.info("The value {}:{} is for criterion {} is not a number, ignoring"
279                            .format(criterion_numerical_value, criterion_literal, criterion_name))
280                        continue
281
282            try:
283                string_int = int(criterion_numerical_value, 0)
284
285            except ValueError:
286                # Handle the exception
287                logging.info("The value {}:{} is for criterion {} is not a number, ignoring"
288                    .format(criterion_numerical_value, criterion_literal, criterion_name))
289                continue
290
291            # Remove duplicated numerical values
292            if int(criterion_numerical_value, 0) in all_criteria[criterion_name].values():
293                logging.info("criterion {} duplicated values:".format(criterion_name))
294                logging.info("{}:{}".format(criterion_numerical_value, criterion_literal))
295                logging.info("KEEPING LATEST")
296                for key in list(all_criteria[criterion_name]):
297                    if all_criteria[criterion_name][key] == int(criterion_numerical_value, 0):
298                        del all_criteria[criterion_name][key]
299
300            all_criteria[criterion_name][criterion_literal] = int(criterion_numerical_value, 0)
301
302            logging.debug("type:{},".format(criterion_name))
303            logging.debug("iteral:{},".format(criterion_literal))
304            logging.debug("values:{}.".format(criterion_numerical_value))
305
306    logging.info("Checking Android Common Header file {}".format(androidaudiocommonbaseheaderFile))
307
308    criteria_pattern = re.compile(
309        r"\s*(?P<type>(?:"+'|'.join(criterion_mapping_table.keys()) + "))_" \
310        r"(?P<literal>(?!" + '|'.join(ignored_values) + ")\w*)\s*=\s*" \
311        r"(?:AUDIO_DEVICE_BIT_IN \| )?(?P<values>(?:0[xX])?[0-9a-fA-F]+|[0-9]+)")
312
313    for line_number, line in enumerate(androidaudiocommonbaseheaderFile):
314        match = criteria_pattern.match(line)
315        if match:
316            logging.debug("The following line is VALID: {}:{}\n{}".format(
317                androidaudiocommonbaseheaderFile.name, line_number, line))
318
319            criterion_name = criterion_mapping_table[match.groupdict()['type']]
320            criterion_literal = \
321                ''.join((w.capitalize() for w in match.groupdict()['literal'].split('_')))
322            criterion_numerical_value = match.groupdict()['values']
323
324            try:
325                string_int = int(criterion_numerical_value, 0)
326            except ValueError:
327                # Handle the exception
328                logging.info("The value {}:{} is for criterion {} is not a number, ignoring"
329                    .format(criterion_numerical_value, criterion_literal, criterion_name))
330                continue
331
332            # Remove duplicated numerical values
333            if int(criterion_numerical_value, 0) in all_criteria[criterion_name].values():
334                logging.info("criterion {} duplicated values:".format(criterion_name))
335                logging.info("{}:{}".format(criterion_numerical_value, criterion_literal))
336                logging.info("KEEPING LATEST")
337                for key in list(all_criteria[criterion_name]):
338                    if all_criteria[criterion_name][key] == int(criterion_numerical_value, 0):
339                        del all_criteria[criterion_name][key]
340
341            all_criteria[criterion_name][criterion_literal] = int(criterion_numerical_value, 0)
342
343            logging.debug("type:{},".format(criterion_name))
344            logging.debug("iteral:{},".format(criterion_literal))
345            logging.debug("values:{}.".format(criterion_numerical_value))
346
347    return all_criteria
348
349
350def main():
351    logging.root.setLevel(logging.INFO)
352    args = parseArgs()
353
354    all_criteria = parseAndroidAudioFile(args.androidaudiobaseheader,
355                                         args.androidaudiocommonbaseheader)
356
357    address_criteria = parseAndroidAudioPolicyConfigurationFile(args.audiopolicyconfigurationfile)
358
359    criterion_types = args.criteriontypes
360
361    generateXmlCriterionTypesFile(all_criteria, address_criteria, criterion_types, args.outputfile)
362
363# If this file is directly executed
364if __name__ == "__main__":
365    sys.exit(main())
366