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