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