• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    assertEqual("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
1130    assertEqual("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
1131    assertEqual("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 e.visibility == 'extension_passthrough' or
1421                                     is_not_hal_visible(e)))
1422
1423def remove_ndk_non_visible(entries):
1424  """
1425  Filter the given entries by removing those that are not NDK visible:
1426  synthetic, fwk_only, extension, or fwk_java_public.
1427
1428  Args:
1429    entries: An iterable of Entry nodes
1430
1431  Yields:
1432    An iterable of Entry nodes
1433  """
1434  return (e for e in entries if not (e.synthetic or e.visibility == 'fwk_only'
1435                                     or e.visibility == 'fwk_java_public' or
1436                                     e.visibility == 'extension'))
1437
1438"""
1439  Return the vndk version for a given hal minor version. The major version is assumed to be 3
1440
1441  Args:
1442    hal_minor_version : minor version to retrieve the vndk version for
1443
1444  Yields:
1445    int representing the vndk version
1446  """
1447def get_vndk_version(hal_minor_version):
1448  if hal_minor_version <= FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION:
1449    return 0
1450  return hal_minor_version - FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION \
1451        + FRAMEWORK_CAMERA_VNDK_STARTING_VERSION
1452
1453"""
1454  Returns an api level -> dict of metadata tags corresponding to the api level
1455
1456  Args:
1457    sections : metadata sections to create the mapping for
1458    metadata: the metadata structure to be used to create the mapping
1459    kind : kind of entries to create a mapping for : 'static' or 'dynamic'
1460
1461  Yields:
1462    A dictionary mapping api level to a dictionary of metadata tags for the particular key (api level)
1463  """
1464def get_api_level_to_keys(sections, metadata, kind):
1465  api_level_to_keys = {}
1466  for sec in sections:
1467    for idx,entry in enumerate(remove_synthetic(find_unique_entries(sec))):
1468      if entry._hal_minor_version > FRAMEWORK_CAMERA_VNDK_HAL_MINOR_VERSION and \
1469          metadata.is_entry_this_kind(entry, kind):
1470        api_level = get_vndk_version(entry._hal_minor_version)
1471        try:
1472          api_level_to_keys[api_level].add(entry.name)
1473        except KeyError:
1474          api_level_to_keys[api_level] = {entry.name}
1475  #Insert the keys in sorted order since dicts in python (< 3.7, even OrderedDicts don't actually
1476  # sort keys)
1477  api_level_to_keys_ordered = OrderedDict()
1478  for api_level_ordered in sorted(api_level_to_keys.keys()):
1479    api_level_to_keys_ordered[api_level_ordered] = sorted(api_level_to_keys[api_level_ordered])
1480  return api_level_to_keys_ordered
1481
1482
1483def get_api_level_to_session_characteristic_keys(sections):
1484  """
1485  Returns a mapping of api_level -> list session characteristics tag keys where api_level
1486  is the level at which they became a part of getSessionCharacteristics call.
1487
1488  Args:
1489    sections : metadata sections to create the mapping for
1490
1491  Returns:
1492    A dictionary mapping api level to a list of metadata tags.
1493  """
1494  api_level_to_keys = defaultdict(list)
1495  for sec in sections:
1496    for entry in remove_synthetic(find_unique_entries(sec)):
1497      if entry.session_characteristics_key_since is None:
1498        continue
1499
1500      api_level = entry.session_characteristics_key_since
1501      api_level_to_keys[api_level].append(entry.name)
1502
1503  # sort dictionary on its key (api_level)
1504  api_level_to_keys = OrderedDict(sorted(api_level_to_keys.items(), key=itemgetter(0)))
1505  # sort the keys for each api_level
1506  for api_level, keys in api_level_to_keys.items():
1507    api_level_to_keys[api_level] = sorted(keys)
1508
1509  return api_level_to_keys
1510
1511
1512def remove_synthetic(entries):
1513  """
1514  Filter the given entries by removing those that are synthetic.
1515
1516  Args:
1517    entries: An iterable of Entry nodes
1518
1519  Yields:
1520    An iterable of Entry nodes
1521  """
1522  return (e for e in entries if not e.synthetic)
1523
1524def filter_added_in_hal_version(entries, hal_major_version, hal_minor_version):
1525  """
1526  Filter the given entries to those added in the given HIDL HAL version
1527
1528  Args:
1529    entries: An iterable of Entry nodes
1530    hal_major_version: Major HIDL version to filter for
1531    hal_minor_version: Minor HIDL version to filter for
1532
1533  Yields:
1534    An iterable of Entry nodes
1535  """
1536  return (e for e in entries if e.hal_major_version == hal_major_version and e.hal_minor_version == hal_minor_version)
1537
1538def filter_has_enum_values_added_in_hal_version(entries, hal_major_version, hal_minor_version):
1539  """
1540  Filter the given entries to those that have a new enum value added in the given HIDL HAL version
1541
1542  Args:
1543    entries: An iterable of Entry nodes
1544    hal_major_version: Major HIDL version to filter for
1545    hal_minor_version: Minor HIDL version to filter for
1546
1547  Yields:
1548    An iterable of Entry nodes
1549  """
1550  return (e for e in entries if e.has_new_values_added_in_hal_version(hal_major_version, hal_minor_version))
1551
1552def permission_needed_count(root):
1553  """
1554  Return the number entries that need camera permission.
1555
1556  Args:
1557    root: a Metadata instance
1558
1559  Returns:
1560    The number of entires that need camera permission.
1561
1562  """
1563  ret = 0
1564  for sec in find_all_sections(root):
1565      ret += len(list(filter_has_permission_needed(remove_hal_non_visible(find_unique_entries(sec)))))
1566
1567  return ret
1568
1569def filter_has_permission_needed(entries):
1570  """
1571    Filter the given entries by removing those that don't need camera permission.
1572
1573    Args:
1574      entries: An iterable of Entry nodes
1575
1576    Yields:
1577      An iterable of Entry nodes
1578  """
1579  return (e for e in entries if e.permission_needed == 'true')
1580
1581def filter_ndk_visible(entries):
1582  """
1583  Filter the given entries by removing those that are not NDK visible.
1584
1585  Args:
1586    entries: An iterable of Entry nodes
1587
1588  Yields:
1589    An iterable of Entry nodes
1590  """
1591  return (e for e in entries if e.applied_ndk_visible == 'true')
1592
1593def wbr(text):
1594  """
1595  Insert word break hints for the browser in the form of <wbr> HTML tags.
1596
1597  Word breaks are inserted inside an HTML node only, so the nodes themselves
1598  will not be changed. Attributes are also left unchanged.
1599
1600  The following rules apply to insert word breaks:
1601  - For characters in [ '.', '/', '_' ]
1602  - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
1603
1604  Args:
1605    text: A string of text containing HTML content.
1606
1607  Returns:
1608    A string with <wbr> inserted by the above rules.
1609  """
1610  SPLIT_CHARS_LIST = ['.', '_', '/']
1611  SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
1612  CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
1613  def wbr_filter(text):
1614      new_txt = text
1615
1616      # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
1617      # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
1618      for words in text.split(" "):
1619        for char in SPLIT_CHARS_LIST:
1620          # match at least x.y.z, don't match x or x.y
1621          if len(words.split(char)) >= CAP_LETTER_MIN:
1622            new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
1623            new_txt = new_txt.replace(words, new_word)
1624
1625      # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
1626      new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
1627
1628      return new_txt
1629
1630  # Do not mangle HTML when doing the replace by using BeatifulSoup
1631  # - Use the 'html.parser' to avoid inserting <html><body> when decoding
1632  soup = bs4.BeautifulSoup(text, features='html.parser')
1633  wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
1634
1635  for navigable_string in soup.findAll(text=True):
1636      parent = navigable_string.parent
1637
1638      # Insert each '$text<wbr>$foo' before the old '$text$foo'
1639      split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
1640      for (split_string, last) in enumerate_with_last(split_by_wbr_list):
1641          navigable_string.insert_before(split_string)
1642
1643          if not last:
1644            # Note that 'insert' will move existing tags to this spot
1645            # so make a new tag instead
1646            navigable_string.insert_before(wbr_tag())
1647
1648      # Remove the old unmodified text
1649      navigable_string.extract()
1650
1651  return soup.decode()
1652
1653def copyright_year():
1654  return _copyright_year
1655
1656def infer_copyright_year_from_source(src_file, default_copyright_year):
1657  """
1658  Opens src_file and tries to infer the copyright year from the file
1659  if it exists. Returns default_copyright_year if src_file is None, doesn't
1660  exist, or the copyright year cannot be parsed from the first 15 lines.
1661
1662  Assumption:
1663    - Copyright text must be in the first 15 lines of the src_file.
1664      This should almost always be true.
1665  """
1666  if src_file is None:
1667    return default_copyright_year
1668
1669  if not path.isfile(src_file):
1670    return default_copyright_year
1671
1672  copyright_pattern = r"^.*Copyright \([Cc]\) (20\d\d) The Android Open Source Project$"
1673  num_max_lines = 15
1674
1675  with open(src_file, "r") as f:
1676    for i, line in enumerate(f):
1677      if i >= num_max_lines:
1678        break
1679
1680      years = re.findall(copyright_pattern, line.strip())
1681      if len(years) > 0:
1682        return years[0]
1683
1684  return default_copyright_year
1685
1686def enum():
1687  return _enum
1688
1689def first_hal_minor_version(hal_major_version):
1690  return 2 if hal_major_version == 3 else 0
1691
1692def find_all_sections_added_in_hal(root, hal_major_version, hal_minor_version):
1693  """
1694  Find all descendants that are Section or InnerNamespace instances, which
1695  were added in HIDL HAL version major.minor. The section is defined to be
1696  added in a HAL version iff the lowest HAL version number of its entries is
1697  that HAL version.
1698
1699  Args:
1700    root: a Metadata instance
1701    hal_major/minor_version: HAL version numbers
1702
1703  Returns:
1704    A list of Section/InnerNamespace instances
1705
1706  Remarks:
1707    These are known as "sections" in the generated C code.
1708  """
1709  all_sections = find_all_sections(root)
1710  new_sections = []
1711  for section in all_sections:
1712    min_major_version = None
1713    min_minor_version = None
1714    for entry in remove_hal_non_visible(find_unique_entries(section)):
1715      min_major_version = (min_major_version or entry.hal_major_version)
1716      min_minor_version = (min_minor_version or entry.hal_minor_version)
1717      if entry.hal_major_version < min_major_version or \
1718          (entry.hal_major_version == min_major_version and entry.hal_minor_version < min_minor_version):
1719        min_minor_version = entry.hal_minor_version
1720        min_major_version = entry.hal_major_version
1721    if min_major_version == hal_major_version and min_minor_version == hal_minor_version:
1722      new_sections.append(section)
1723  return new_sections
1724
1725def find_first_older_used_hal_version(section, hal_major_version, hal_minor_version):
1726  hal_version = (0, 0)
1727  for v in section.hal_versions:
1728    if (v[0] > hal_version[0] or (v[0] == hal_version[0] and v[1] > hal_version[1])) and \
1729        (v[0] < hal_major_version or (v[0] == hal_major_version and v[1] < hal_minor_version)):
1730      hal_version = v
1731  return hal_version
1732
1733# Some exceptions need to be made regarding enum value identifiers in AIDL.
1734# Process them here.
1735def aidl_enum_value_name(name):
1736  if name == 'ANDROID_INFO_SUPPORTED_BUFFER_MANAGEMENT_VERSION_HIDL_DEVICE_3_5':
1737    name = 'ANDROID_INFO_SUPPORTED_BUFFER_MANAGEMENT_VERSION_AIDL_DEVICE'
1738  return name
1739
1740def aidl_enum_values(entry):
1741  ignoreList = [
1742    'ANDROID_SCALER_AVAILABLE_RECOMMENDED_STREAM_CONFIGURATIONS_PUBLIC_END',
1743    'ANDROID_SCALER_AVAILABLE_RECOMMENDED_STREAM_CONFIGURATIONS_PUBLIC_END_3_8'
1744  ]
1745  return [
1746    val for val in entry.enum.values if '%s_%s'%(csym(entry.name), val.name) not in ignoreList
1747  ]
1748
1749def java_symbol_for_aconfig_flag(flag_name):
1750  """
1751  Returns the java symbol for a give aconfig flag. This means converting
1752  snake_case to lower camelCase. For example: The aconfig flag
1753  'camera_ae_mode_low_light_boost' becomes 'cameraAeModeLowLightBoost'.
1754
1755  Args:
1756    flag_name: str. aconfig flag in snake_case
1757
1758  Return:
1759    Java symbol for the a config flag.
1760  """
1761  camel_case = "".join([t.capitalize() for t in flag_name.split("_")])
1762  # first character should be lowercase
1763  return camel_case[0].lower() + camel_case[1:]
1764