xref: /aosp_15_r20/system/media/camera/docs/metadata_helpers.py (revision b9df5ad1c9ac98a7fefaac271a55f7ae3db05414)
1#
2# Copyright (C) 2012 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"""
18A set of helpers for rendering Mako templates with a Metadata model.
19"""
20
21import metadata_model
22import re
23import markdown
24import textwrap
25import sys
26import bs4
27# Monkey-patch BS4. WBR element must not have an end tag.
28bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr")
29
30from collections import OrderedDict, defaultdict
31from operator import itemgetter
32from os import path
33
34# Relative path from HTML file to the base directory used by <img> tags
35IMAGE_SRC_METADATA="images/camera2/metadata/"
36
37# Prepend this path to each <img src="foo"> in javadocs
38JAVADOC_IMAGE_SRC_METADATA="/reference/" + IMAGE_SRC_METADATA
39NDKDOC_IMAGE_SRC_METADATA="../" + IMAGE_SRC_METADATA
40
41#Corresponds to Android Q, where the camera VNDK was added (minor version 4 and vndk version 29).
42# Minor version and vndk version must correspond to the same release
43FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION = 4
44FRAMEWORK_CAMERA_VNDK_STARTING_VERSION =  29
45
46_context_buf = None
47_enum = None
48
49def _is_sec_or_ins(x):
50  return isinstance(x, metadata_model.Section) or    \
51         isinstance(x, metadata_model.InnerNamespace)
52
53##
54## Metadata Helpers
55##
56
57def find_all_sections(root):
58  """
59  Find all descendants that are Section or InnerNamespace instances.
60
61  Args:
62    root: a Metadata instance
63
64  Returns:
65    A list of Section/InnerNamespace instances
66
67  Remarks:
68    These are known as "sections" in the generated C code.
69  """
70  return root.find_all(_is_sec_or_ins)
71
72def find_all_sections_filtered(root, visibility):
73  """
74  Find all descendants that are Section or InnerNamespace instances that do not
75  contain entries of the supplied visibility
76
77  Args:
78    root: a Metadata instance
79    visibilities: An iterable of visibilities to filter against
80
81  Returns:
82    A list of Section/InnerNamespace instances
83
84  Remarks:
85    These are known as "sections" in the generated C code.
86  """
87  sections = root.find_all(_is_sec_or_ins)
88
89  filtered_sections = []
90  for sec in sections:
91    if not any(filter_visibility(find_unique_entries(sec), visibility)):
92      filtered_sections.append(sec)
93
94  return filtered_sections
95
96
97def find_parent_section(entry):
98  """
99  Find the closest ancestor that is either a Section or InnerNamespace.
100
101  Args:
102    entry: an Entry or Clone node
103
104  Returns:
105    An instance of Section or InnerNamespace
106  """
107  return entry.find_parent_first(_is_sec_or_ins)
108
109# find uniquely named entries (w/o recursing through inner namespaces)
110def find_unique_entries(node):
111  """
112  Find all uniquely named entries, without recursing through inner namespaces.
113
114  Args:
115    node: a Section or InnerNamespace instance
116
117  Yields:
118    A sequence of MergedEntry nodes representing an entry
119
120  Remarks:
121    This collapses multiple entries with the same fully qualified name into
122    one entry (e.g. if there are multiple entries in different kinds).
123  """
124  if not isinstance(node, metadata_model.Section) and    \
125     not isinstance(node, metadata_model.InnerNamespace):
126      raise TypeError("expected node to be a Section or InnerNamespace")
127
128  d = OrderedDict()
129  # remove the 'kinds' from the path between sec and the closest entries
130  # then search the immediate children of the search path
131  search_path = isinstance(node, metadata_model.Section) and node.kinds \
132                or [node]
133  for i in search_path:
134      for entry in i.entries:
135          d[entry.name] = entry
136
137  for k,v in d.items():
138      yield v.merge()
139
140def path_name(node):
141  """
142  Calculate a period-separated string path from the root to this element,
143  by joining the names of each node and excluding the Metadata/Kind nodes
144  from the path.
145
146  Args:
147    node: a Node instance
148
149  Returns:
150    A string path
151  """
152
153  isa = lambda x,y: isinstance(x, y)
154  fltr = lambda x: not isa(x, metadata_model.Metadata) and \
155                   not isa(x, metadata_model.Kind)
156
157  path = node.find_parents(fltr)
158  path = list(path)
159  path.reverse()
160  path.append(node)
161
162  return ".".join((i.name for i in path))
163
164def ndk(name):
165  """
166  Return the NDK version of given name, which replace
167  the leading "android" to "acamera"
168
169  Args:
170    name: name string of an entry
171
172  Returns:
173    A NDK version name string of the input name
174  """
175  name_list = name.split(".")
176  if name_list[0] == "android":
177    name_list[0] = "acamera"
178  return ".".join(name_list)
179
180def protobuf_type(entry):
181  """
182  Return the protocol buffer message type for input metadata entry.
183  Only support types used by static metadata right now
184
185  Returns:
186    A string of protocol buffer type. Ex: "optional int32" or "repeated RangeInt"
187  """
188  typeName = None
189  if entry.typedef is None:
190    typeName = entry.type
191  else:
192    typeName = entry.typedef.name
193
194  typename_to_protobuftype = {
195    "rational"               : "Rational",
196    "size"                   : "Size",
197    "sizeF"                  : "SizeF",
198    "rectangle"              : "Rect",
199    "streamConfigurationMap" : "StreamConfigurations",
200    "mandatoryStreamCombination" : "MandatoryStreamCombination",
201    "rangeInt"               : "RangeInt",
202    "rangeLong"              : "RangeLong",
203    "rangeFloat"             : "RangeFloat",
204    "colorSpaceTransform"    : "ColorSpaceTransform",
205    "blackLevelPattern"      : "BlackLevelPattern",
206    "byte"                   : "int32", # protocol buffer don't support byte
207    "boolean"                : "bool",
208    "float"                  : "float",
209    "double"                 : "double",
210    "int32"                  : "int32",
211    "int64"                  : "int64",
212    "enumList"               : "int32",
213    "string"                 : "string",
214    "capability"             : "Capability",
215    "multiResolutionStreamConfigurationMap" : "MultiResolutionStreamConfigurations",
216    "deviceStateSensorOrientationMap"  : "DeviceStateSensorOrientationMap",
217    "dynamicRangeProfiles"   : "DynamicRangeProfiles",
218    "colorSpaceProfiles"     : "ColorSpaceProfiles",
219    "versionCode"            : "int32",
220    "sharedSessionConfiguration"  : "SharedSessionConfiguration",
221  }
222
223  if typeName not in typename_to_protobuftype:
224    print("  ERROR: Could not find protocol buffer type for {%s} type {%s} typedef {%s}" % \
225          (entry.name, entry.type, entry.typedef), file=sys.stderr)
226
227  proto_type = typename_to_protobuftype[typeName]
228
229  prefix = "optional"
230  if entry.container == 'array':
231    has_variable_size = False
232    for size in entry.container_sizes:
233      try:
234        size_int = int(size)
235      except ValueError:
236        has_variable_size = True
237
238    if has_variable_size:
239      prefix = "repeated"
240
241  return "%s %s" %(prefix, proto_type)
242
243
244def protobuf_name(entry):
245  """
246  Return the protocol buffer field name for input metadata entry
247
248  Returns:
249    A string. Ex: "android_colorCorrection_availableAberrationModes"
250  """
251  return entry.name.replace(".", "_")
252
253def has_descendants_with_enums(node):
254  """
255  Determine whether or not the current node is or has any descendants with an
256  Enum node.
257
258  Args:
259    node: a Node instance
260
261  Returns:
262    True if it finds an Enum node in the subtree, False otherwise
263  """
264  return bool(node.find_first(lambda x: isinstance(x, metadata_model.Enum)))
265
266def get_children_by_throwing_away_kind(node, member='entries'):
267  """
268  Get the children of this node by compressing the subtree together by removing
269  the kind and then combining any children nodes with the same name together.
270
271  Args:
272    node: An instance of Section, InnerNamespace, or Kind
273
274  Returns:
275    An iterable over the combined children of the subtree of node,
276    as if the Kinds never existed.
277
278  Remarks:
279    Not recursive. Call this function repeatedly on each child.
280  """
281
282  if isinstance(node, metadata_model.Section):
283    # Note that this makes jump from Section to Kind,
284    # skipping the Kind entirely in the tree.
285    node_to_combine = node.combine_kinds_into_single_node()
286  else:
287    node_to_combine = node
288
289  combined_kind = node_to_combine.combine_children_by_name()
290
291  return (i for i in getattr(combined_kind, member))
292
293def get_children_by_filtering_kind(section, kind_name, member='entries'):
294  """
295  Takes a section and yields the children of the merged kind under this section.
296
297  Args:
298    section: An instance of Section
299    kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
300
301  Returns:
302    An iterable over the children of the specified merged kind.
303  """
304
305  matched_kind = next((i for i in section.merged_kinds if i.name == kind_name), None)
306
307  if matched_kind:
308    return getattr(matched_kind, member)
309  else:
310    return ()
311
312##
313## Filters
314##
315
316# abcDef.xyz -> ABC_DEF_XYZ
317def csym(name):
318  """
319  Convert an entry name string into an uppercase C symbol.
320
321  Returns:
322    A string
323
324  Example:
325    csym('abcDef.xyz') == 'ABC_DEF_XYZ'
326  """
327  newstr = name
328  newstr = "".join([i.isupper() and ("_" + i) or i for i in newstr]).upper()
329  newstr = newstr.replace(".", "_")
330  return newstr
331
332# abcDef.xyz -> abc_def_xyz
333def csyml(name):
334  """
335  Convert an entry name string into a lowercase C symbol.
336
337  Returns:
338    A string
339
340  Example:
341    csyml('abcDef.xyz') == 'abc_def_xyz'
342  """
343  return csym(name).lower()
344
345# pad with spaces to make string len == size. add new line if too big
346def ljust(size, indent=4):
347  """
348  Creates a function that given a string will pad it with spaces to make
349  the string length == size. Adds a new line if the string was too big.
350
351  Args:
352    size: an integer representing how much spacing should be added
353    indent: an integer representing the initial indendation level
354
355  Returns:
356    A function that takes a string and returns a string.
357
358  Example:
359    ljust(8)("hello") == 'hello   '
360
361  Remarks:
362    Deprecated. Use pad instead since it works for non-first items in a
363    Mako template.
364  """
365  def inner(what):
366    newstr = what.ljust(size)
367    if len(newstr) > size:
368      return what + "\n" + "".ljust(indent + size)
369    else:
370      return newstr
371  return inner
372
373def _find_new_line():
374
375  if _context_buf is None:
376    raise ValueError("Context buffer was not set")
377
378  buf = _context_buf
379  x = -1 # since the first read is always ''
380  cur_pos = buf.tell()
381  while buf.tell() > 0 and buf.read(1) != '\n':
382    buf.seek(cur_pos - x)
383    x = x + 1
384
385  buf.seek(cur_pos)
386
387  return int(x)
388
389# Pad the string until the buffer reaches the desired column.
390# If string is too long, insert a new line with 'col' spaces instead
391def pad(col):
392  """
393  Create a function that given a string will pad it to the specified column col.
394  If the string overflows the column, put the string on a new line and pad it.
395
396  Args:
397    col: an integer specifying the column number
398
399  Returns:
400    A function that given a string will produce a padded string.
401
402  Example:
403    pad(8)("hello") == 'hello   '
404
405  Remarks:
406    This keeps track of the line written by Mako so far, so it will always
407    align to the column number correctly.
408  """
409  def inner(what):
410    wut = int(col)
411    current_col = _find_new_line()
412
413    if len(what) > wut - current_col:
414      return what + "\n".ljust(col)
415    else:
416      return what.ljust(wut - current_col)
417  return inner
418
419# int32 -> TYPE_INT32, byte -> TYPE_BYTE, etc. note that enum -> TYPE_INT32
420def ctype_enum(what):
421  """
422  Generate a camera_metadata_type_t symbol from a type string.
423
424  Args:
425    what: a type string
426
427  Returns:
428    A string representing the camera_metadata_type_t
429
430  Example:
431    ctype_enum('int32') == 'TYPE_INT32'
432    ctype_enum('int64') == 'TYPE_INT64'
433    ctype_enum('float') == 'TYPE_FLOAT'
434
435  Remarks:
436    An enum is coerced to a byte since the rest of the camera_metadata
437    code doesn't support enums directly yet.
438  """
439  return 'TYPE_%s' %(what.upper())
440
441
442# Calculate a java type name from an entry with a Typedef node
443def _jtypedef_type(entry):
444  typedef = entry.typedef
445  additional = ''
446
447  # Hacky way to deal with arrays. Assume that if we have
448  # size 'Constant x N' the Constant is part of the Typedef size.
449  # So something sized just 'Constant', 'Constant1 x Constant2', etc
450  # is not treated as a real java array.
451  if entry.container == 'array':
452    has_variable_size = False
453    for size in entry.container_sizes:
454      try:
455        size_int = int(size)
456      except ValueError:
457        has_variable_size = True
458
459    if has_variable_size:
460      additional = '[]'
461
462  try:
463    name = typedef.languages['java']
464
465    return "%s%s" %(name, additional)
466  except KeyError:
467    return None
468
469# Box if primitive. Otherwise leave unboxed.
470def _jtype_box(type_name):
471  mapping = {
472    'boolean': 'Boolean',
473    'byte': 'Byte',
474    'int': 'Integer',
475    'float': 'Float',
476    'double': 'Double',
477    'long': 'Long'
478  }
479
480  return mapping.get(type_name, type_name)
481
482def jtype_unboxed(entry):
483  """
484  Calculate the Java type from an entry type string, to be used whenever we
485  need the regular type in Java. It's not boxed, so it can't be used as a
486  generic type argument when the entry type happens to resolve to a primitive.
487
488  Remarks:
489    Since Java generics cannot be instantiated with primitives, this version
490    is not applicable in that case. Use jtype_boxed instead for that.
491
492  Returns:
493    The string representing the Java type.
494  """
495  if not isinstance(entry, metadata_model.Entry):
496    raise ValueError("Expected entry to be an instance of Entry")
497
498  metadata_type = entry.type
499
500  java_type = None
501
502  if entry.typedef:
503    typedef_name = _jtypedef_type(entry)
504    if typedef_name:
505      java_type = typedef_name # already takes into account arrays
506
507  if not java_type:
508    if not java_type and entry.enum and metadata_type == 'byte':
509      # Always map byte enums to Java ints, unless there's a typedef override
510      base_type = 'int'
511
512    else:
513      mapping = {
514        'int32': 'int',
515        'int64': 'long',
516        'float': 'float',
517        'double': 'double',
518        'byte': 'byte',
519        'rational': 'Rational'
520      }
521
522      base_type = mapping[metadata_type]
523
524    # Convert to array (enums, basic types)
525    if entry.container == 'array':
526      additional = '[]'
527    else:
528      additional = ''
529
530    java_type = '%s%s' %(base_type, additional)
531
532  # Now box this sucker.
533  return java_type
534
535def jtype_boxed(entry):
536  """
537  Calculate the Java type from an entry type string, to be used as a generic
538  type argument in Java. The type is guaranteed to inherit from Object.
539
540  It will only box when absolutely necessary, i.e. int -> Integer[], but
541  int[] -> int[].
542
543  Remarks:
544    Since Java generics cannot be instantiated with primitives, this version
545    will use boxed types when absolutely required.
546
547  Returns:
548    The string representing the boxed Java type.
549  """
550  unboxed_type = jtype_unboxed(entry)
551  return _jtype_box(unboxed_type)
552
553def _is_jtype_generic(entry):
554  """
555  Determine whether or not the Java type represented by the entry type
556  string and/or typedef is a Java generic.
557
558  For example, "Range<Integer>" would be considered a generic, whereas
559  a "MeteringRectangle" or a plain "Integer" would not be considered a generic.
560
561  Args:
562    entry: An instance of an Entry node
563
564  Returns:
565    True if it's a java generic, False otherwise.
566  """
567  if entry.typedef:
568    local_typedef = _jtypedef_type(entry)
569    if local_typedef:
570      match = re.search(r'<.*>', local_typedef)
571      return bool(match)
572  return False
573
574def _jtype_primitive(what):
575  """
576  Calculate the Java type from an entry type string.
577
578  Remarks:
579    Makes a special exception for Rational, since it's a primitive in terms of
580    the C-library camera_metadata type system.
581
582  Returns:
583    The string representing the primitive type
584  """
585  mapping = {
586    'int32': 'int',
587    'int64': 'long',
588    'float': 'float',
589    'double': 'double',
590    'byte': 'byte',
591    'rational': 'Rational'
592  }
593
594  try:
595    return mapping[what]
596  except KeyError as e:
597    raise ValueError("Can't map '%s' to a primitive, not supported" %what)
598
599def jclass(entry):
600  """
601  Calculate the java Class reference string for an entry.
602
603  Args:
604    entry: an Entry node
605
606  Example:
607    <entry name="some_int" type="int32"/>
608    <entry name="some_int_array" type="int32" container='array'/>
609
610    jclass(some_int) == 'int.class'
611    jclass(some_int_array) == 'int[].class'
612
613  Returns:
614    The ClassName.class string
615  """
616
617  return "%s.class" %jtype_unboxed(entry)
618
619def jkey_type_token(entry):
620  """
621  Calculate the java type token compatible with a Key constructor.
622  This will be the Java Class<T> for non-generic classes, and a
623  TypeReference<T> for generic classes.
624
625  Args:
626    entry: An entry node
627
628  Returns:
629    The ClassName.class string, or 'new TypeReference<ClassName>() {{ }}' string
630  """
631  if _is_jtype_generic(entry):
632    return "new TypeReference<%s>() {{ }}" %(jtype_boxed(entry))
633  else:
634    return jclass(entry)
635
636def jidentifier(what):
637  """
638  Convert the input string into a valid Java identifier.
639
640  Args:
641    what: any identifier string
642
643  Returns:
644    String with added underscores if necessary.
645  """
646  if re.match("\d", what):
647    return "_%s" %what
648  else:
649    return what
650
651def enum_calculate_value_string(enum_value):
652  """
653  Calculate the value of the enum, even if it does not have one explicitly
654  defined.
655
656  This looks back for the first enum value that has a predefined value and then
657  applies addition until we get the right value, using C-enum semantics.
658
659  Args:
660    enum_value: an EnumValue node with a valid Enum parent
661
662  Example:
663    <enum>
664      <value>X</value>
665      <value id="5">Y</value>
666      <value>Z</value>
667    </enum>
668
669    enum_calculate_value_string(X) == '0'
670    enum_calculate_Value_string(Y) == '5'
671    enum_calculate_value_string(Z) == '6'
672
673  Returns:
674    String that represents the enum value as an integer literal.
675  """
676
677  enum_value_siblings = list(enum_value.parent.values)
678  this_index = enum_value_siblings.index(enum_value)
679
680  def is_hex_string(instr):
681    return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE))
682
683  base_value = 0
684  base_offset = 0
685  emit_as_hex = False
686
687  this_id = enum_value_siblings[this_index].id
688  while this_index != 0 and not this_id:
689    this_index -= 1
690    base_offset += 1
691    this_id = enum_value_siblings[this_index].id
692
693  if this_id:
694    base_value = int(this_id, 0)  # guess base
695    emit_as_hex = is_hex_string(this_id)
696
697  if emit_as_hex:
698    return "0x%X" %(base_value + base_offset)
699  else:
700    return "%d" %(base_value + base_offset)
701
702def enumerate_with_last(iterable):
703  """
704  Enumerate a sequence of iterable, while knowing if this element is the last in
705  the sequence or not.
706
707  Args:
708    iterable: an Iterable of some sequence
709
710  Yields:
711    (element, bool) where the bool is True iff the element is last in the seq.
712  """
713  it = (i for i in iterable)
714
715  try:
716    first = next(it)  # OK: raises exception if it is empty
717  except StopIteration:
718    return
719
720  second = first  # for when we have only 1 element in iterable
721
722  try:
723    while True:
724      second = next(it)
725      # more elements remaining.
726      yield (first, False)
727      first = second
728  except StopIteration:
729    # last element. no more elements left
730    yield (second, True)
731
732def pascal_case(what):
733  """
734  Convert the first letter of a string to uppercase, to make the identifier
735  conform to PascalCase.
736
737  If there are dots, remove the dots, and capitalize the letter following
738  where the dot was. Letters that weren't following dots are left unchanged,
739  except for the first letter of the string (which is made upper-case).
740
741  Args:
742    what: a string representing some identifier
743
744  Returns:
745    String with first letter capitalized
746
747  Example:
748    pascal_case("helloWorld") == "HelloWorld"
749    pascal_case("foo") == "Foo"
750    pascal_case("hello.world") = "HelloWorld"
751    pascal_case("fooBar.fooBar") = "FooBarFooBar"
752  """
753  return "".join([s[0:1].upper() + s[1:] for s in what.split('.')])
754
755def jkey_identifier(what):
756  """
757  Return a Java identifier from a property name.
758
759  Args:
760    what: a string representing a property name.
761
762  Returns:
763    Java identifier corresponding to the property name. May need to be
764    prepended with the appropriate Java class name by the caller of this
765    function. Note that the outer namespace is stripped from the property
766    name.
767
768  Example:
769    jkey_identifier("android.lens.facing") == "LENS_FACING"
770  """
771  return csym(what[what.find('.') + 1:])
772
773def jenum_value(enum_entry, enum_value):
774  """
775  Calculate the Java name for an integer enum value
776
777  Args:
778    enum: An enum-typed Entry node
779    value: An EnumValue node for the enum
780
781  Returns:
782    String representing the Java symbol
783  """
784
785  cname = csym(enum_entry.name)
786  return cname[cname.find('_') + 1:] + '_' + enum_value.name
787
788def generate_extra_javadoc_detail(entry):
789  """
790  Returns a function to add extra details for an entry into a string for inclusion into
791  javadoc. Adds information about units, the list of enum values for this key, and the valid
792  range.
793  """
794  def inner(text):
795    if entry.units and not (entry.typedef and entry.typedef.name == 'string'):
796      text += '\n\n<b>Units</b>: %s\n' % (dedent(entry.units))
797    if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
798      text += '\n\n<b>Possible values:</b>\n<ul>\n'
799      for value in entry.enum.values:
800        if not value.hidden and (value.aconfig_flag == entry.aconfig_flag):
801          text += '  <li>{@link #%s %s}</li>\n' % ( jenum_value(entry, value ), value.name )
802      text += '</ul>\n'
803    if entry.range:
804      if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
805        text += '\n\n<b>Available values for this device:</b><br>\n'
806      else:
807        text += '\n\n<b>Range of valid values:</b><br>\n'
808      text += '%s\n' % (dedent(entry.range))
809    if entry.hwlevel != 'legacy': # covers any of (None, 'limited', 'full')
810      text += '\n\n<b>Optional</b> - The value for this key may be {@code null} on some devices.\n'
811    if entry.hwlevel == 'full':
812      text += \
813        '\n<b>Full capability</b> - \n' + \
814        'Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the\n' + \
815        'android.info.supportedHardwareLevel key\n'
816    if entry.hwlevel == 'limited':
817      text += \
818        '\n<b>Limited capability</b> - \n' + \
819        'Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the\n' + \
820        'android.info.supportedHardwareLevel key\n'
821    if entry.hwlevel == 'legacy':
822      text += "\nThis key is available on all devices."
823    if entry.permission_needed == "true":
824      text += "\n\n<b>Permission {@link android.Manifest.permission#CAMERA} is needed to access this property</b>\n\n"
825
826    return text
827  return inner
828
829
830def javadoc(metadata, indent = 4):
831  """
832  Returns a function to format a markdown syntax text block as a
833  javadoc comment section, given a set of metadata
834
835  Args:
836    metadata: A Metadata instance, representing the top-level root
837      of the metadata for cross-referencing
838    indent: baseline level of indentation for javadoc block
839  Returns:
840    A function that transforms a String text block as follows:
841    - Indent and * for insertion into a Javadoc comment block
842    - Trailing whitespace removed
843    - Entire body rendered via markdown to generate HTML
844    - All tag names converted to appropriate Javadoc {@link} with @see
845      for each tag
846
847  Example:
848    "This is a comment for Javadoc\n" +
849    "     with multiple lines, that should be   \n" +
850    "     formatted better\n" +
851    "\n" +
852    "    That covers multiple lines as well\n"
853    "    And references android.control.mode\n"
854
855    transforms to
856    "    * <p>This is a comment for Javadoc\n" +
857    "    * with multiple lines, that should be\n" +
858    "    * formatted better</p>\n" +
859    "    * <p>That covers multiple lines as well</p>\n" +
860    "    * and references {@link CaptureRequest#CONTROL_MODE android.control.mode}\n" +
861    "    *\n" +
862    "    * @see CaptureRequest#CONTROL_MODE\n"
863  """
864  def javadoc_formatter(text):
865    comment_prefix = " " * indent + " * "
866
867    # render with markdown => HTML
868    javatext = md(text, JAVADOC_IMAGE_SRC_METADATA)
869
870    # Identity transform for javadoc links
871    def javadoc_link_filter(target, target_ndk, shortname):
872      return '{@link %s %s}' % (target, shortname)
873
874    javatext = filter_links(javatext, javadoc_link_filter)
875
876    # Crossref tag names
877    kind_mapping = {
878        'static': 'CameraCharacteristics',
879        'dynamic': 'CaptureResult',
880        'controls': 'CaptureRequest' }
881
882    # Convert metadata entry "android.x.y.z" to form
883    # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
884    def javadoc_crossref_filter(node):
885      if node.applied_visibility in ('public', 'java_public', 'fwk_java_public', 'fwk_public',\
886                                     'fwk_system_public'):
887        return '{@link %s#%s %s}' % (kind_mapping[node.kind],
888                                     jkey_identifier(node.name),
889                                     node.name)
890      else:
891        return node.name
892
893    # For each public tag "android.x.y.z" referenced, add a
894    # "@see CaptureRequest#X_Y_Z"
895    def javadoc_crossref_see_filter(node_set):
896      node_set = (x for x in node_set if x.applied_visibility in \
897                  ('public', 'java_public', 'fwk_java_public', 'fwk_public', 'fwk_system_public'))
898
899      text = '\n'
900      for node in node_set:
901        text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
902                                      jkey_identifier(node.name))
903
904      return text if text != '\n' else ''
905
906    javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_crossref_see_filter)
907
908    def line_filter(line):
909      # Indent each line
910      # Add ' * ' to it for stylistic reasons
911      # Strip right side of trailing whitespace
912      return (comment_prefix + line).rstrip()
913
914    # Process each line with above filter
915    javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
916
917    return javatext
918
919  return javadoc_formatter
920
921def ndkdoc(metadata, indent = 4):
922  """
923  Returns a function to format a markdown syntax text block as a
924  NDK camera API C/C++ comment section, given a set of metadata
925
926  Args:
927    metadata: A Metadata instance, representing the top-level root
928      of the metadata for cross-referencing
929    indent: baseline level of indentation for comment block
930  Returns:
931    A function that transforms a String text block as follows:
932    - Indent and * for insertion into a comment block
933    - Trailing whitespace removed
934    - Entire body rendered via markdown
935    - All tag names converted to appropriate NDK tag name for each tag
936
937  Example:
938    "This is a comment for NDK\n" +
939    "     with multiple lines, that should be   \n" +
940    "     formatted better\n" +
941    "\n" +
942    "    That covers multiple lines as well\n"
943    "    And references android.control.mode\n"
944
945    transforms to
946    "    * This is a comment for NDK\n" +
947    "    * with multiple lines, that should be\n" +
948    "    * formatted better\n" +
949    "    * That covers multiple lines as well\n" +
950    "    * and references ACAMERA_CONTROL_MODE\n" +
951    "    *\n" +
952    "    * @see ACAMERA_CONTROL_MODE\n"
953  """
954  def ndkdoc_formatter(text):
955    # render with markdown => HTML
956    # Turn off the table plugin since doxygen doesn't recognize generated <thead> <tbody> tags
957    ndktext = md(text, NDKDOC_IMAGE_SRC_METADATA, False)
958
959    # Simple transform for ndk doc links
960    def ndkdoc_link_filter(target, target_ndk, shortname):
961      if target_ndk is not None:
962        return '{@link %s %s}' % (target_ndk, shortname)
963
964      # Create HTML link to Javadoc
965      if shortname == '':
966        lastdot = target.rfind('.')
967        if lastdot == -1:
968          shortname = target
969        else:
970          shortname = target[lastdot + 1:]
971
972      target = target.replace('.','/')
973      if target.find('#') != -1:
974        target = target.replace('#','.html#')
975      else:
976        target = target + '.html'
977
978      # Work around html links with inner classes.
979      target = target.replace('CaptureRequest/Builder', 'CaptureRequest.Builder')
980      target = target.replace('Build/VERSION', 'Build.VERSION')
981
982      return '<a href="https://developer.android.com/reference/%s">%s</a>' % (target, shortname)
983
984    ndktext = filter_links(ndktext, ndkdoc_link_filter)
985
986    # Convert metadata entry "android.x.y.z" to form
987    # NDK tag format of "ACAMERA_X_Y_Z"
988    def ndkdoc_crossref_filter(node):
989      if node.applied_ndk_visible == 'true':
990        return csym(ndk(node.name))
991      else:
992        return node.name
993
994    # For each public tag "android.x.y.z" referenced, add a
995    # "@see ACAMERA_X_Y_Z"
996    def ndkdoc_crossref_see_filter(node_set):
997      node_set = (x for x in node_set if x.applied_ndk_visible == 'true')
998
999      text = '\n'
1000      for node in node_set:
1001        text = text + '\n@see %s' % (csym(ndk(node.name)))
1002
1003      return text if text != '\n' else ''
1004
1005    ndktext = filter_tags(ndktext, metadata, ndkdoc_crossref_filter, ndkdoc_crossref_see_filter)
1006
1007    ndktext = ndk_replace_tag_wildcards(ndktext, metadata)
1008
1009    comment_prefix = " " * indent + " * ";
1010
1011    def line_filter(line):
1012      # Indent each line
1013      # Add ' * ' to it for stylistic reasons
1014      # Strip right side of trailing whitespace
1015      return (comment_prefix + line).rstrip()
1016
1017    # Process each line with above filter
1018    ndktext = "\n".join(line_filter(i) for i in ndktext.split("\n")) + "\n"
1019
1020    return ndktext
1021
1022  return ndkdoc_formatter
1023
1024def hidldoc(metadata, indent = 4):
1025  """
1026  Returns a function to format a markdown syntax text block as a
1027  HIDL camera HAL module C/C++ comment section, given a set of metadata
1028
1029  Args:
1030    metadata: A Metadata instance, representing the top-level root
1031      of the metadata for cross-referencing
1032    indent: baseline level of indentation for comment block
1033  Returns:
1034    A function that transforms a String text block as follows:
1035    - Indent and * for insertion into a comment block
1036    - Trailing whitespace removed
1037    - Entire body rendered via markdown
1038    - All tag names converted to appropriate HIDL tag name for each tag
1039
1040  Example:
1041    "This is a comment for NDK\n" +
1042    "     with multiple lines, that should be   \n" +
1043    "     formatted better\n" +
1044    "\n" +
1045    "    That covers multiple lines as well\n"
1046    "    And references android.control.mode\n"
1047
1048    transforms to
1049    "    * This is a comment for NDK\n" +
1050    "    * with multiple lines, that should be\n" +
1051    "    * formatted better\n" +
1052    "    * That covers multiple lines as well\n" +
1053    "    * and references ANDROID_CONTROL_MODE\n" +
1054    "    *\n" +
1055    "    * @see ANDROID_CONTROL_MODE\n"
1056  """
1057  def hidldoc_formatter(text):
1058    # render with markdown => HTML
1059    # Turn off the table plugin since doxygen doesn't recognize generated <thead> <tbody> tags
1060    hidltext = md(text, NDKDOC_IMAGE_SRC_METADATA, False)
1061
1062    # Simple transform for hidl doc links
1063    def hidldoc_link_filter(target, target_ndk, shortname):
1064      if target_ndk is not None:
1065        return '{@link %s %s}' % (target_ndk, shortname)
1066
1067      # Create HTML link to Javadoc
1068      if shortname == '':
1069        lastdot = target.rfind('.')
1070        if lastdot == -1:
1071          shortname = target
1072        else:
1073          shortname = target[lastdot + 1:]
1074
1075      target = target.replace('.','/')
1076      if target.find('#') != -1:
1077        target = target.replace('#','.html#')
1078      else:
1079        target = target + '.html'
1080
1081      return '<a href="https://developer.android.com/reference/%s">%s</a>' % (target, shortname)
1082
1083    hidltext = filter_links(hidltext, hidldoc_link_filter)
1084
1085    # Convert metadata entry "android.x.y.z" to form
1086    # HIDL tag format of "ANDROID_X_Y_Z"
1087    def hidldoc_crossref_filter(node):
1088      return csym(node.name)
1089
1090    # For each public tag "android.x.y.z" referenced, add a
1091    # "@see ANDROID_X_Y_Z"
1092    def hidldoc_crossref_see_filter(node_set):
1093      text = '\n'
1094      for node in node_set:
1095        text = text + '\n@see %s' % (csym(node.name))
1096
1097      return text if text != '\n' else ''
1098
1099    hidltext = filter_tags(hidltext, metadata, hidldoc_crossref_filter, hidldoc_crossref_see_filter)
1100
1101    comment_prefix = " " * indent + " * ";
1102
1103    def line_filter(line):
1104      # Indent each line
1105      # Add ' * ' to it for stylistic reasons
1106      # Strip right side of trailing whitespace
1107      return (comment_prefix + line).rstrip()
1108
1109    # Process each line with above filter
1110    hidltext = "\n".join(line_filter(i) for i in hidltext.split("\n")) + "\n"
1111
1112    return hidltext
1113
1114  return hidldoc_formatter
1115
1116def dedent(text):
1117  """
1118  Remove all common indentation from every line but the 0th.
1119  This will avoid getting <code> blocks when rendering text via markdown.
1120  Ignoring the 0th line will also allow the 0th line not to be aligned.
1121
1122  Args:
1123    text: A string of text to dedent.
1124
1125  Returns:
1126    String dedented by above rules.
1127
1128  For example:
1129    assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
1130    assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
1131    assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
1132  """
1133  text = textwrap.dedent(text)
1134  text_lines = text.split('\n')
1135  text_not_first = "\n".join(text_lines[1:])
1136  text_not_first = textwrap.dedent(text_not_first)
1137  text = text_lines[0] + "\n" + text_not_first
1138
1139  return text
1140
1141def md(text, img_src_prefix="", table_ext=True):
1142    """
1143    Run text through markdown to produce HTML.
1144
1145    This also removes all common indentation from every line but the 0th.
1146    This will avoid getting <code> blocks in markdown.
1147    Ignoring the 0th line will also allow the 0th line not to be aligned.
1148
1149    Args:
1150      text: A markdown-syntax using block of text to format.
1151      img_src_prefix: An optional string to prepend to each <img src="target"/>
1152
1153    Returns:
1154      String rendered by markdown and other rules applied (see above).
1155
1156    For example, this avoids the following situation:
1157
1158      <!-- Input -->
1159
1160      <!--- can't use dedent directly since 'foo' has no indent -->
1161      <notes>foo
1162          bar
1163          bar
1164      </notes>
1165
1166      <!-- Bad Output -- >
1167      <!-- if no dedent is done generated code looks like -->
1168      <p>foo
1169        <code><pre>
1170          bar
1171          bar</pre></code>
1172      </p>
1173
1174    Instead we get the more natural expected result:
1175
1176      <!-- Good Output -->
1177      <p>foo
1178      bar
1179      bar</p>
1180
1181    """
1182    text = dedent(text)
1183
1184    # full list of extensions at http://pythonhosted.org/Markdown/extensions/
1185    md_extensions = ['tables'] if table_ext else []# make <table> with ASCII |_| tables
1186    # render with markdown
1187    text = markdown.markdown(text, extensions=md_extensions)
1188
1189    # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
1190    text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
1191    return text
1192
1193def filter_tags(text, metadata, filter_function, summary_function = None):
1194    """
1195    Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
1196    the provided text, and pass them through filter_function and summary_function.
1197
1198    Used to linkify entry names in HMTL, javadoc output.
1199
1200    Args:
1201      text: A string representing a block of text destined for output
1202      metadata: A Metadata instance, the root of the metadata properties tree
1203      filter_function: A Node->string function to apply to each node
1204        when found in text; the string returned replaces the tag name in text.
1205      summary_function: A Node list->string function that is provided the list of
1206        unique tag nodes found in text, and which must return a string that is
1207        then appended to the end of the text. The list is sorted alphabetically
1208        by node name.
1209    """
1210
1211    tag_set = set()
1212    def name_match(name):
1213      return lambda node: node.name == name
1214
1215    # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
1216    # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
1217    # check for validity, a few false positives don't hurt).
1218    # Try to ignore items of the form {@link <outer_namespace>...
1219    for outer_namespace in metadata.outer_namespaces:
1220
1221      tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \
1222        r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
1223
1224      def filter_sub(match):
1225        whole_match = match.group(0)
1226        section1 = match.group(1)
1227        section2 = match.group(2)
1228        section3 = match.group(3)
1229        end_slash = match.group(4)
1230
1231        # Don't linkify things ending in slash (urls, for example)
1232        if end_slash:
1233          return whole_match
1234
1235        candidate = ""
1236
1237        # First try a two-level match
1238        candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
1239        got_two_level = False
1240
1241        node = metadata.find_first(name_match(candidate2.replace('\n','')))
1242        if not node and '\n' in section2:
1243          # Linefeeds are ambiguous - was the intent to add a space,
1244          # or continue a lengthy name? Try the former now.
1245          candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
1246          node = metadata.find_first(name_match(candidate2b))
1247          if node:
1248            candidate2 = candidate2b
1249
1250        if node:
1251          # Have two-level match
1252          got_two_level = True
1253          candidate = candidate2
1254        elif section3:
1255          # Try three-level match
1256          candidate3 = "%s%s" % (candidate2, section3)
1257          node = metadata.find_first(name_match(candidate3.replace('\n','')))
1258
1259          if not node and '\n' in section3:
1260            # Linefeeds are ambiguous - was the intent to add a space,
1261            # or continue a lengthy name? Try the former now.
1262            candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
1263            node = metadata.find_first(name_match(candidate3b))
1264            if node:
1265              candidate3 = candidate3b
1266
1267          if node:
1268            # Have 3-level match
1269            candidate = candidate3
1270
1271        # Replace match with crossref or complain if a likely match couldn't be matched
1272
1273        if node:
1274          tag_set.add(node)
1275          return whole_match.replace(candidate,filter_function(node))
1276        else:
1277          print("  WARNING: Could not crossref likely reference {%s}" % (match.group(0)),
1278                file=sys.stderr)
1279          return whole_match
1280
1281      text = re.sub(tag_match, filter_sub, text)
1282
1283    if summary_function is not None:
1284      return text + summary_function(sorted(tag_set, key=lambda x: x.name))
1285    else:
1286      return text
1287
1288def ndk_replace_tag_wildcards(text, metadata):
1289    """
1290    Find all references to tags in the form android.xxx.* or android.xxx.yyy.*
1291    in the provided text, and replace them by NDK format of "ACAMERA_XXX_*" or
1292    "ACAMERA_XXX_YYY_*"
1293
1294    Args:
1295      text: A string representing a block of text destined for output
1296      metadata: A Metadata instance, the root of the metadata properties tree
1297    """
1298    tag_match = r"android\.([a-zA-Z0-9\n]+)\.\*"
1299    tag_match_2 = r"android\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)\*"
1300
1301    def filter_sub(match):
1302      return "ACAMERA_" + match.group(1).upper() + "_*"
1303    def filter_sub_2(match):
1304      return "ACAMERA_" + match.group(1).upper() + match.group(2).upper() + "_*"
1305
1306    text = re.sub(tag_match, filter_sub, text)
1307    text = re.sub(tag_match_2, filter_sub_2, text)
1308    return text
1309
1310def filter_links(text, filter_function, summary_function = None):
1311    """
1312    Find all references to tags in the form {@link xxx#yyy [zzz]} in the
1313    provided text, and pass them through filter_function and
1314    summary_function.
1315
1316    Used to linkify documentation cross-references in HMTL, javadoc output.
1317
1318    Args:
1319      text: A string representing a block of text destined for output
1320      metadata: A Metadata instance, the root of the metadata properties tree
1321      filter_function: A (string, string)->string function to apply to each 'xxx#yyy',
1322        zzz pair when found in text; the string returned replaces the tag name in text.
1323      summary_function: A string list->string function that is provided the list of
1324        unique targets found in text, and which must return a string that is
1325        then appended to the end of the text. The list is sorted alphabetically
1326        by node name.
1327
1328    """
1329
1330    target_set = set()
1331    def name_match(name):
1332      return lambda node: node.name == name
1333
1334    tag_match = r"\{@link\s+([^\s\}\|]+)(?:\|([^\s\}]+))*([^\}]*)\}"
1335
1336    def filter_sub(match):
1337      whole_match = match.group(0)
1338      target = match.group(1)
1339      target_ndk = match.group(2)
1340      shortname = match.group(3).strip()
1341
1342      #print("Found link '%s' ndk '%s' as '%s' -> '%s'" % (target, target_ndk, shortname, filter_function(target, target_ndk, shortname)))
1343
1344      # Replace match with crossref
1345      target_set.add(target)
1346      return filter_function(target, target_ndk, shortname)
1347
1348    text = re.sub(tag_match, filter_sub, text)
1349
1350    if summary_function is not None:
1351      return text + summary_function(sorted(target_set))
1352    else:
1353      return text
1354
1355def any_visible(section, kind_name, visibilities):
1356  """
1357  Determine if entries in this section have an applied visibility that's in
1358  the list of given visibilities.
1359
1360  Args:
1361    section: A section of metadata
1362    kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
1363    visibilities: An iterable of visibilities to match against
1364
1365  Returns:
1366    True if the section has any entries with any of the given visibilities. False otherwise.
1367  """
1368
1369  for inner_namespace in get_children_by_filtering_kind(section, kind_name,
1370                                                        'namespaces'):
1371    if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
1372      return True
1373
1374  return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
1375                                                              'merged_entries'),
1376                               visibilities))
1377
1378def filter_visibility(entries, visibilities):
1379  """
1380  Remove entries whose applied visibility is not in the supplied visibilities.
1381
1382  Args:
1383    entries: An iterable of Entry nodes
1384    visibilities: An iterable of visibilities to filter against
1385
1386  Yields:
1387    An iterable of Entry nodes
1388  """
1389  return (e for e in entries if e.applied_visibility in visibilities)
1390
1391def is_not_hal_visible(e):
1392  """
1393  Determine that the entry being passed in is not visible to HAL.
1394
1395  Args:
1396    e: An entry node
1397
1398  Returns:
1399    True if the entry is not visible to HAL
1400  """
1401  return (e.visibility == 'fwk_only' or
1402          e.visibility == 'fwk_java_public' or
1403          e.visibility == 'fwk_public' or
1404          e.visibility == 'fwk_system_public' or
1405          e.visibility == 'fwk_ndk_public' or
1406          e.visibility == 'extension')
1407
1408def remove_hal_non_visible(entries):
1409  """
1410  Filter the given entries by removing those that are not HAL visible:
1411  synthetic, fwk_only, extension, fwk_java_public, fwk_system_public, fwk_ndk_public,
1412  or fwk_public.
1413
1414  Args:
1415    entries: An iterable of Entry nodes
1416
1417  Yields:
1418    An iterable of Entry nodes
1419  """
1420  return (e for e in entries if not (e.synthetic or is_not_hal_visible(e)))
1421
1422def remove_ndk_non_visible(entries):
1423  """
1424  Filter the given entries by removing those that are not NDK visible:
1425  synthetic, fwk_only, extension, or fwk_java_public.
1426
1427  Args:
1428    entries: An iterable of Entry nodes
1429
1430  Yields:
1431    An iterable of Entry nodes
1432  """
1433  return (e for e in entries if not (e.synthetic or e.visibility == 'fwk_only'
1434                                     or e.visibility == 'fwk_java_public' or
1435                                     e.visibility == 'extension'))
1436
1437"""
1438  Return the vndk version for a given hal minor version. The major version is assumed to be 3
1439
1440  Args:
1441    hal_minor_version : minor version to retrieve the vndk version for
1442
1443  Yields:
1444    int representing the vndk version
1445  """
1446def get_vndk_version(hal_minor_version):
1447  if hal_minor_version <= FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION:
1448    return 0
1449  return hal_minor_version - FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION \
1450        + FRAMEWORK_CAMERA_VNDK_STARTING_VERSION
1451
1452"""
1453  Returns an api level -> dict of metadata tags corresponding to the api level
1454
1455  Args:
1456    sections : metadata sections to create the mapping for
1457    metadata: the metadata structure to be used to create the mapping
1458    kind : kind of entries to create a mapping for : 'static' or 'dynamic'
1459
1460  Yields:
1461    A dictionary mapping api level to a dictionary of metadata tags for the particular key (api level)
1462  """
1463def get_api_level_to_keys(sections, metadata, kind):
1464  api_level_to_keys = {}
1465  for sec in sections:
1466    for idx,entry in enumerate(remove_synthetic(find_unique_entries(sec))):
1467      if entry._hal_minor_version > FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION and \
1468          metadata.is_entry_this_kind(entry, kind):
1469        api_level = get_vndk_version(entry._hal_minor_version)
1470        try:
1471          api_level_to_keys[api_level].add(entry.name)
1472        except KeyError:
1473          api_level_to_keys[api_level] = {entry.name}
1474  #Insert the keys in sorted order since dicts in python (< 3.7, even OrderedDicts don't actually
1475  # sort keys)
1476  api_level_to_keys_ordered = OrderedDict()
1477  for api_level_ordered in sorted(api_level_to_keys.keys()):
1478    api_level_to_keys_ordered[api_level_ordered] = sorted(api_level_to_keys[api_level_ordered])
1479  return api_level_to_keys_ordered
1480
1481
1482def get_api_level_to_session_characteristic_keys(sections):
1483  """
1484  Returns a mapping of api_level -> list session characteristics tag keys where api_level
1485  is the level at which they became a part of getSessionCharacteristics call.
1486
1487  Args:
1488    sections : metadata sections to create the mapping for
1489
1490  Returns:
1491    A dictionary mapping api level to a list of metadata tags.
1492  """
1493  api_level_to_keys = defaultdict(list)
1494  for sec in sections:
1495    for entry in remove_synthetic(find_unique_entries(sec)):
1496      if entry.session_characteristics_key_since is None:
1497        continue
1498
1499      api_level = entry.session_characteristics_key_since
1500      api_level_to_keys[api_level].append(entry.name)
1501
1502  # sort dictionary on its key (api_level)
1503  api_level_to_keys = OrderedDict(sorted(api_level_to_keys.items(), key=itemgetter(0)))
1504  # sort the keys for each api_level
1505  for api_level, keys in api_level_to_keys.items():
1506    api_level_to_keys[api_level] = sorted(keys)
1507
1508  return api_level_to_keys
1509
1510
1511def remove_synthetic(entries):
1512  """
1513  Filter the given entries by removing those that are synthetic.
1514
1515  Args:
1516    entries: An iterable of Entry nodes
1517
1518  Yields:
1519    An iterable of Entry nodes
1520  """
1521  return (e for e in entries if not e.synthetic)
1522
1523def filter_added_in_hal_version(entries, hal_major_version, hal_minor_version):
1524  """
1525  Filter the given entries to those added in the given HIDL HAL version
1526
1527  Args:
1528    entries: An iterable of Entry nodes
1529    hal_major_version: Major HIDL version to filter for
1530    hal_minor_version: Minor HIDL version to filter for
1531
1532  Yields:
1533    An iterable of Entry nodes
1534  """
1535  return (e for e in entries if e.hal_major_version == hal_major_version and e.hal_minor_version == hal_minor_version)
1536
1537def filter_has_enum_values_added_in_hal_version(entries, hal_major_version, hal_minor_version):
1538  """
1539  Filter the given entries to those that have a new enum value added in the given HIDL HAL version
1540
1541  Args:
1542    entries: An iterable of Entry nodes
1543    hal_major_version: Major HIDL version to filter for
1544    hal_minor_version: Minor HIDL version to filter for
1545
1546  Yields:
1547    An iterable of Entry nodes
1548  """
1549  return (e for e in entries if e.has_new_values_added_in_hal_version(hal_major_version, hal_minor_version))
1550
1551def permission_needed_count(root):
1552  """
1553  Return the number entries that need camera permission.
1554
1555  Args:
1556    root: a Metadata instance
1557
1558  Returns:
1559    The number of entires that need camera permission.
1560
1561  """
1562  ret = 0
1563  for sec in find_all_sections(root):
1564      ret += len(list(filter_has_permission_needed(remove_hal_non_visible(find_unique_entries(sec)))))
1565
1566  return ret
1567
1568def filter_has_permission_needed(entries):
1569  """
1570    Filter the given entries by removing those that don't need camera permission.
1571
1572    Args:
1573      entries: An iterable of Entry nodes
1574
1575    Yields:
1576      An iterable of Entry nodes
1577  """
1578  return (e for e in entries if e.permission_needed == 'true')
1579
1580def filter_ndk_visible(entries):
1581  """
1582  Filter the given entries by removing those that are not NDK visible.
1583
1584  Args:
1585    entries: An iterable of Entry nodes
1586
1587  Yields:
1588    An iterable of Entry nodes
1589  """
1590  return (e for e in entries if e.applied_ndk_visible == 'true')
1591
1592def wbr(text):
1593  """
1594  Insert word break hints for the browser in the form of <wbr> HTML tags.
1595
1596  Word breaks are inserted inside an HTML node only, so the nodes themselves
1597  will not be changed. Attributes are also left unchanged.
1598
1599  The following rules apply to insert word breaks:
1600  - For characters in [ '.', '/', '_' ]
1601  - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
1602
1603  Args:
1604    text: A string of text containing HTML content.
1605
1606  Returns:
1607    A string with <wbr> inserted by the above rules.
1608  """
1609  SPLIT_CHARS_LIST = ['.', '_', '/']
1610  SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
1611  CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
1612  def wbr_filter(text):
1613      new_txt = text
1614
1615      # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
1616      # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
1617      for words in text.split(" "):
1618        for char in SPLIT_CHARS_LIST:
1619          # match at least x.y.z, don't match x or x.y
1620          if len(words.split(char)) >= CAP_LETTER_MIN:
1621            new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
1622            new_txt = new_txt.replace(words, new_word)
1623
1624      # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
1625      new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
1626
1627      return new_txt
1628
1629  # Do not mangle HTML when doing the replace by using BeatifulSoup
1630  # - Use the 'html.parser' to avoid inserting <html><body> when decoding
1631  soup = bs4.BeautifulSoup(text, features='html.parser')
1632  wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
1633
1634  for navigable_string in soup.findAll(text=True):
1635      parent = navigable_string.parent
1636
1637      # Insert each '$text<wbr>$foo' before the old '$text$foo'
1638      split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
1639      for (split_string, last) in enumerate_with_last(split_by_wbr_list):
1640          navigable_string.insert_before(split_string)
1641
1642          if not last:
1643            # Note that 'insert' will move existing tags to this spot
1644            # so make a new tag instead
1645            navigable_string.insert_before(wbr_tag())
1646
1647      # Remove the old unmodified text
1648      navigable_string.extract()
1649
1650  return soup.decode()
1651
1652def copyright_year():
1653  return _copyright_year
1654
1655def infer_copyright_year_from_source(src_file, default_copyright_year):
1656  """
1657  Opens src_file and tries to infer the copyright year from the file
1658  if it exists. Returns default_copyright_year if src_file is None, doesn't
1659  exist, or the copyright year cannot be parsed from the first 15 lines.
1660
1661  Assumption:
1662    - Copyright text must be in the first 15 lines of the src_file.
1663      This should almost always be true.
1664  """
1665  if src_file is None:
1666    return default_copyright_year
1667
1668  if not path.isfile(src_file):
1669    return default_copyright_year
1670
1671  copyright_pattern = r"^.*Copyright \([Cc]\) (20\d\d) The Android Open Source Project$"
1672  num_max_lines = 15
1673
1674  with open(src_file, "r") as f:
1675    for i, line in enumerate(f):
1676      if i >= num_max_lines:
1677        break
1678
1679      years = re.findall(copyright_pattern, line.strip())
1680      if len(years) > 0:
1681        return years[0]
1682
1683  return default_copyright_year
1684
1685def enum():
1686  return _enum
1687
1688def first_hal_minor_version(hal_major_version):
1689  return 2 if hal_major_version == 3 else 0
1690
1691def find_all_sections_added_in_hal(root, hal_major_version, hal_minor_version):
1692  """
1693  Find all descendants that are Section or InnerNamespace instances, which
1694  were added in HIDL HAL version major.minor. The section is defined to be
1695  added in a HAL version iff the lowest HAL version number of its entries is
1696  that HAL version.
1697
1698  Args:
1699    root: a Metadata instance
1700    hal_major/minor_version: HAL version numbers
1701
1702  Returns:
1703    A list of Section/InnerNamespace instances
1704
1705  Remarks:
1706    These are known as "sections" in the generated C code.
1707  """
1708  all_sections = find_all_sections(root)
1709  new_sections = []
1710  for section in all_sections:
1711    min_major_version = None
1712    min_minor_version = None
1713    for entry in remove_hal_non_visible(find_unique_entries(section)):
1714      min_major_version = (min_major_version or entry.hal_major_version)
1715      min_minor_version = (min_minor_version or entry.hal_minor_version)
1716      if entry.hal_major_version < min_major_version or \
1717          (entry.hal_major_version == min_major_version and entry.hal_minor_version < min_minor_version):
1718        min_minor_version = entry.hal_minor_version
1719        min_major_version = entry.hal_major_version
1720    if min_major_version == hal_major_version and min_minor_version == hal_minor_version:
1721      new_sections.append(section)
1722  return new_sections
1723
1724def find_first_older_used_hal_version(section, hal_major_version, hal_minor_version):
1725  hal_version = (0, 0)
1726  for v in section.hal_versions:
1727    if (v[0] > hal_version[0] or (v[0] == hal_version[0] and v[1] > hal_version[1])) and \
1728        (v[0] < hal_major_version or (v[0] == hal_major_version and v[1] < hal_minor_version)):
1729      hal_version = v
1730  return hal_version
1731
1732# Some exceptions need to be made regarding enum value identifiers in AIDL.
1733# Process them here.
1734def aidl_enum_value_name(name):
1735  if name == 'ANDROID_INFO_SUPPORTED_BUFFER_MANAGEMENT_VERSION_HIDL_DEVICE_3_5':
1736    name = 'ANDROID_INFO_SUPPORTED_BUFFER_MANAGEMENT_VERSION_AIDL_DEVICE'
1737  return name
1738
1739def aidl_enum_values(entry):
1740  ignoreList = [
1741    'ANDROID_SCALER_AVAILABLE_RECOMMENDED_STREAM_CONFIGURATIONS_PUBLIC_END',
1742    'ANDROID_SCALER_AVAILABLE_RECOMMENDED_STREAM_CONFIGURATIONS_PUBLIC_END_3_8'
1743  ]
1744  return [
1745    val for val in entry.enum.values if '%s_%s'%(csym(entry.name), val.name) not in ignoreList
1746  ]
1747
1748def java_symbol_for_aconfig_flag(flag_name):
1749  """
1750  Returns the java symbol for a give aconfig flag. This means converting
1751  snake_case to lower camelCase. For example: The aconfig flag
1752  'camera_ae_mode_low_light_boost' becomes 'cameraAeModeLowLightBoost'.
1753
1754  Args:
1755    flag_name: str. aconfig flag in snake_case
1756
1757  Return:
1758    Java symbol for the a config flag.
1759  """
1760  camel_case = "".join([t.capitalize() for t in flag_name.split("_")])
1761  # first character should be lowercase
1762  return camel_case[0].lower() + camel_case[1:]
1763