# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import collections
import contextlib
import itertools
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile

from util import build_utils
from util import parallel
import action_helpers
import zip_helpers

_SOURCE_ROOT = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')
)
# Import jinja2 from third_party/jinja2
sys.path.insert(1, os.path.join(_SOURCE_ROOT, 'third_party'))
from jinja2 import Template  # pylint: disable=F0401


# A variation of these maps also exists in:
# //base/android/java/src/org/chromium/base/LocaleUtils.java
# //ui/android/java/src/org/chromium/base/LocalizationUtils.java
_CHROME_TO_ANDROID_LOCALE_MAP = {
    'es-419': 'es-rUS',
    'sr-Latn': 'b+sr+Latn',
    'fil': 'tl',
    'he': 'iw',
    'id': 'in',
    'yi': 'ji',
}
_ANDROID_TO_CHROMIUM_LANGUAGE_MAP = {
    'tl': 'fil',
    'iw': 'he',
    'in': 'id',
    'ji': 'yi',
    'no': 'nb',  # 'no' is not a real language. http://crbug.com/920960
}

ALL_RESOURCE_TYPES = {
    'anim',
    'animator',
    'array',
    'attr',
    'bool',
    'color',
    'dimen',
    'drawable',
    'font',
    'fraction',
    'id',
    'integer',
    'interpolator',
    'layout',
    'macro',
    'menu',
    'mipmap',
    'overlayable',
    'plurals',
    'raw',
    'string',
    'style',
    'styleable',
    'transition',
    'xml',
}

AAPT_IGNORE_PATTERN = ':'.join(
    [
        '*OWNERS',  # Allow OWNERS files within res/
        'DIR_METADATA',  # Allow DIR_METADATA files within res/
        '*.py',  # PRESUBMIT.py sometimes exist.
        '*.pyc',
        '*~',  # Some editors create these as temp files.
        '.*',  # Never makes sense to include dot(files/dirs).
        '*.d.stamp',  # Ignore stamp files
        '*.backup',  # Some tools create temporary backup files.
    ]
)

MULTIPLE_RES_MAGIC_STRING = b'magic'


def ToAndroidLocaleName(chromium_locale):
    """Convert a Chromium locale name into a corresponding Android one."""
    # Should be in sync with build/config/locales.gni.
    # First handle the special cases, these are needed to deal with Android
    # releases *before* 5.0/Lollipop.
    android_locale = _CHROME_TO_ANDROID_LOCALE_MAP.get(chromium_locale)
    if android_locale:
        return android_locale

    # Format of Chromium locale name is '<lang>' or '<lang>-<region>'
    # where <lang> is a 2 or 3 letter language code (ISO 639-1 or 639-2)
    # and region is a capitalized locale region name.
    lang, _, region = chromium_locale.partition('-')
    if not region:
        return lang

    # Translate newer language tags into obsolete ones. Only necessary if
    #  region is not None (e.g. 'he-IL' -> 'iw-rIL')
    lang = _CHROME_TO_ANDROID_LOCALE_MAP.get(lang, lang)

    # Using '<lang>-r<region>' is now acceptable as a locale name for all
    # versions of Android.
    return '%s-r%s' % (lang, region)


# ISO 639 language code + optional ("-r" + capitalized region code).
# Note that before Android 5.0/Lollipop, only 2-letter ISO 639-1 codes
# are supported.
_RE_ANDROID_LOCALE_QUALIFIER_1 = re.compile(r'^([a-z]{2,3})(\-r([A-Z]+))?$')

# Starting with Android 7.0/Nougat, BCP 47 codes are supported but must
# be prefixed with 'b+', and may include optional tags.
#  e.g. 'b+en+US', 'b+ja+Latn', 'b+ja+Latn+JP'
_RE_ANDROID_LOCALE_QUALIFIER_2 = re.compile(r'^b\+([a-z]{2,3})(\+.+)?$')


def ToChromiumLocaleName(android_locale):
    """Convert an Android locale name into a Chromium one."""
    lang = None
    region = None
    script = None
    m = _RE_ANDROID_LOCALE_QUALIFIER_1.match(android_locale)
    if m:
        lang = m.group(1)
        if m.group(2):
            region = m.group(3)
    elif _RE_ANDROID_LOCALE_QUALIFIER_2.match(android_locale):
        # Split an Android BCP-47 locale (e.g. b+sr+Latn+RS)
        tags = android_locale.split('+')

        # The Lang tag is always the first tag.
        lang = tags[1]

        # The optional region tag is 2ALPHA or 3DIGIT tag in pos 1 or 2.
        # The optional script tag is 4ALPHA and always in pos 1.
        optional_tags = iter(tags[2:])

        next_tag = next(optional_tags, None)
        if next_tag and len(next_tag) == 4:
            script = next_tag
            next_tag = next(optional_tags, None)
        if next_tag and len(next_tag) < 4:
            region = next_tag

    if not lang:
        return None

    # Special case for es-rUS -> es-419
    if lang == 'es' and region == 'US':
        return 'es-419'

    lang = _ANDROID_TO_CHROMIUM_LANGUAGE_MAP.get(lang, lang)

    if script:
        lang = '%s-%s' % (lang, script)

    if not region:
        return lang

    return '%s-%s' % (lang, region)


def IsAndroidLocaleQualifier(string):
    """Returns true if |string| is a valid Android resource locale qualifier."""
    return _RE_ANDROID_LOCALE_QUALIFIER_1.match(
        string
    ) or _RE_ANDROID_LOCALE_QUALIFIER_2.match(string)


def FindLocaleInStringResourceFilePath(file_path):
    """Return Android locale name of a string resource file path.

    Args:
      file_path: A file path.
    Returns:
      If |file_path| is of the format '.../values-<locale>/<name>.xml', return
      the value of <locale> (and Android locale qualifier). Otherwise return None.
    """
    if not file_path.endswith('.xml'):
        return None
    prefix = 'values-'
    dir_name = os.path.basename(os.path.dirname(file_path))
    if not dir_name.startswith(prefix):
        return None
    qualifier = dir_name[len(prefix) :]
    return qualifier if IsAndroidLocaleQualifier(qualifier) else None


def ToAndroidLocaleList(locale_list):
    """Convert a list of Chromium locales into the corresponding Android list."""
    return sorted(ToAndroidLocaleName(locale) for locale in locale_list)


# Represents a line from a R.txt file.
_TextSymbolEntry = collections.namedtuple(
    'RTextEntry', ('java_type', 'resource_type', 'name', 'value')
)


def GenerateGlobs(pattern):
    # This function processes the aapt ignore assets pattern into a list of globs
    # to be used to exclude files using build_utils.MatchesGlob. It removes the
    # '!', which is used by aapt to mean 'not chatty' so it does not output if the
    # file is ignored (we dont output anyways, so it is not required). This
    # function does not handle the <dir> and <file> prefixes used by aapt and are
    # assumed not to be included in the pattern string.
    return pattern.replace('!', '').split(':')


def DeduceResourceDirsFromFileList(resource_files, validate=True):
    """Return a list of resource directories from a list of resource files."""
    # Directory list order is important, cannot use set or other data structures
    # that change order. This is because resource files of the same name in
    # multiple res/ directories ellide one another (the last one passed is used).
    # Thus the order must be maintained to prevent non-deterministic and possibly
    # flakey builds.
    resource_dirs = {}
    for resource_path in resource_files:
        # Resources are always 1 directory deep under res/.
        res_dir = os.path.dirname(os.path.dirname(resource_path))
        resource_dirs[res_dir] = 1

    # Check if any resource_dirs are children of other ones. This indicates that a
    # file was listed that is not exactly 1 directory deep under res/.
    # E.g.:
    # sources = ["java/res/values/foo.xml", "java/res/README.md"]
    # ^^ This will cause "java" to be detected as resource directory.
    if validate:
        for a, b in itertools.permutations(resource_dirs, 2):
            if not os.path.relpath(a, b).startswith('..'):
                bad_sources = (
                    s
                    for s in resource_files
                    if os.path.dirname(os.path.dirname(s)) == b
                )
                msg = """\
Resource(s) found that are not in a proper directory structure:
  {}
All resource files must follow a structure of "$ROOT/$SUBDIR/$FILE"."""
                raise Exception(msg.format('\n  '.join(bad_sources)))

    return list(resource_dirs)


def IterResourceFilesInDirectories(
    directories, ignore_pattern=AAPT_IGNORE_PATTERN
):
    globs = GenerateGlobs(ignore_pattern)
    for d in directories:
        for root, _, files in os.walk(d):
            for f in files:
                archive_path = f
                parent_dir = os.path.relpath(root, d)
                if parent_dir != '.':
                    archive_path = os.path.join(parent_dir, f)
                path = os.path.join(root, f)
                if build_utils.MatchesGlob(archive_path, globs):
                    continue
                yield path, archive_path


class ResourceInfoFile:
    """Helper for building up .res.info files."""

    def __init__(self):
        # Dict of archive_path -> source_path for the current target.
        self._entries = {}
        # List of (old_archive_path, new_archive_path) tuples.
        self._renames = []
        # We don't currently support using both AddMapping and MergeInfoFile.
        self._add_mapping_was_called = False

    def AddMapping(self, archive_path, source_path):
        """Adds a single |archive_path| -> |source_path| entry."""
        self._add_mapping_was_called = True
        # "values/" files do not end up in the apk except through resources.arsc.
        if archive_path.startswith('values'):
            return
        source_path = os.path.normpath(source_path)
        new_value = self._entries.setdefault(archive_path, source_path)
        if new_value != source_path:
            raise Exception(
                'Duplicate AddMapping for "{}". old={} new={}'.format(
                    archive_path, new_value, source_path
                )
            )

    def RegisterRename(self, old_archive_path, new_archive_path):
        """Records an archive_path rename.

        |old_archive_path| does not need to currently exist in the mappings. Renames
        are buffered and replayed only when Write() is called.
        """
        if not old_archive_path.startswith('values'):
            self._renames.append((old_archive_path, new_archive_path))

    def MergeInfoFile(self, info_file_path):
        """Merges the mappings from |info_file_path| into this object.

        Any existing entries are overridden.
        """
        assert not self._add_mapping_was_called
        # Allows clobbering, which is used when overriding resources.
        with open(info_file_path, encoding='utf-8') as f:
            self._entries.update(l.rstrip().split('\t') for l in f)

    def _ApplyRenames(self):
        applied_renames = set()
        ret = self._entries
        for rename_tup in self._renames:
            # Duplicate entries happen for resource overrides.
            # Use a "seen" set to ensure we still error out if multiple renames
            # happen for the same old_archive_path with different new_archive_paths.
            if rename_tup in applied_renames:
                continue
            applied_renames.add(rename_tup)
            old_archive_path, new_archive_path = rename_tup
            ret[new_archive_path] = ret[old_archive_path]
            del ret[old_archive_path]

        self._entries = None
        self._renames = None
        return ret

    def Write(self, info_file_path):
        """Applies renames and writes out the file.

        No other methods may be called after this.
        """
        entries = self._ApplyRenames()
        lines = []
        for archive_path, source_path in entries.items():
            lines.append('{}\t{}\n'.format(archive_path, source_path))
        with open(info_file_path, 'w', encoding='utf-8') as info_file:
            info_file.writelines(sorted(lines))


def _ParseTextSymbolsFile(path, fix_package_ids=False):
    """Given an R.txt file, returns a list of _TextSymbolEntry.

    Args:
      path: Path to R.txt file.
      fix_package_ids: If True, package IDs in the resources will be replaced
        with 0x00. This is to make it easy to compare resources from different
        split APKs which might have different package IDs assigned.

    Returns:
      A list of _TextSymbolEntry.

    Raises:
      Exception: An unexpected line was detected in the input.
    """
    ret = []
    with open(path, encoding='utf-8') as f:
        for line in f:
            m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line)
            if not m:
                raise Exception('Unexpected line in R.txt: %s' % line)
            java_type, resource_type, name, value = m.groups()
            if fix_package_ids:
                value = _FixPackageIds(value)
            ret.append(_TextSymbolEntry(java_type, resource_type, name, value))
    return ret


def _FixPackageIds(resource_value):
    # Resource IDs for resources belonging to regular APKs have their first byte
    # as 0x7f (package id). However with webview, since it is not a regular apk
    # but used as a shared library, aapt is passed the --shared-resources flag
    # which changes some of the package ids to 0x00.  This function normalises
    # these (0x00) package ids to 0x7f, which the generated code in R.java changes
    # to the correct package id at runtime.  resource_value is a string with
    # either, a single value '0x12345678', or an array of values like '{
    # 0xfedcba98, 0x01234567, 0x56789abc }'
    return resource_value.replace('0x00', '0x7f')


def ResolveStyleableReferences(r_txt_path):
    # Convert lines like:
    # int[] styleable ViewBack { 0x010100d4, com.android.webview.R.attr.backTint }
    # to:
    # int[] styleable ViewBack { 0x010100d4, 0xREALVALUE }
    entries = _ParseTextSymbolsFile(r_txt_path)
    lookup_table = {(e.resource_type, e.name): e.value for e in entries}

    sb = []
    with open(r_txt_path, encoding='utf-8') as f:
        for l in f:
            if l.startswith('int[] styleable'):
                brace_start = l.index('{') + 2
                brace_end = l.index('}') - 1
                values = [x for x in l[brace_start:brace_end].split(', ') if x]
                new_values = []
                for v in values:
                    try:
                        if not v.startswith('0x'):
                            resource_type, name = v.split('.')[-2:]
                            new_values.append(
                                lookup_table[(resource_type, name)]
                            )
                        else:
                            new_values.append(v)
                    except:
                        logging.warning('Failed line: %r %r', l, v)
                        raise
                l = l[:brace_start] + ', '.join(new_values) + l[brace_end:]
            sb.append(l)

    with open(r_txt_path, 'w', encoding='utf-8') as f:
        f.writelines(sb)


def _GetRTxtResourceNames(r_txt_path):
    """Parse an R.txt file and extract the set of resource names from it."""
    return {entry.name for entry in _ParseTextSymbolsFile(r_txt_path)}


def GetRTxtStringResourceNames(r_txt_path):
    """Parse an R.txt file and the list of its string resource names."""
    return sorted(
        {
            entry.name
            for entry in _ParseTextSymbolsFile(r_txt_path)
            if entry.resource_type == 'string'
        }
    )


def GenerateStringResourcesAllowList(module_r_txt_path, allowlist_r_txt_path):
    """Generate a allowlist of string resource IDs.

    Args:
      module_r_txt_path: Input base module R.txt path.
      allowlist_r_txt_path: Input allowlist R.txt path.
    Returns:
      A dictionary mapping numerical resource IDs to the corresponding
      string resource names. The ID values are taken from string resources in
      |module_r_txt_path| that are also listed by name in |allowlist_r_txt_path|.
    """
    allowlisted_names = {
        entry.name
        for entry in _ParseTextSymbolsFile(allowlist_r_txt_path)
        if entry.resource_type == 'string'
    }
    return {
        int(entry.value, 0): entry.name
        for entry in _ParseTextSymbolsFile(module_r_txt_path)
        if entry.resource_type == 'string' and entry.name in allowlisted_names
    }


class RJavaBuildOptions:
    """A class used to model the various ways to build an R.java file.

    This is used to control which resource ID variables will be final or
    non-final, and whether an onResourcesLoaded() method will be generated
    to adjust the non-final ones, when the corresponding library is loaded
    at runtime.

    Note that by default, all resources are final, and there is no
    method generated, which corresponds to calling ExportNoResources().
    """

    def __init__(self):
        self.has_constant_ids = True
        self.resources_allowlist = None
        self.has_on_resources_loaded = False
        self.export_const_styleable = False
        self.final_package_id = None
        self.fake_on_resources_loaded = False

    def ExportNoResources(self):
        """Make all resource IDs final, and don't generate a method."""
        self.has_constant_ids = True
        self.resources_allowlist = None
        self.has_on_resources_loaded = False
        self.export_const_styleable = False

    def ExportAllResources(self):
        """Make all resource IDs non-final in the R.java file."""
        self.has_constant_ids = False
        self.resources_allowlist = None

    def ExportSomeResources(self, r_txt_file_path):
        """Only select specific resource IDs to be non-final.

        Args:
          r_txt_file_path: The path to an R.txt file. All resources named
            int it will be non-final in the generated R.java file, all others
            will be final.
        """
        self.has_constant_ids = True
        self.resources_allowlist = _GetRTxtResourceNames(r_txt_file_path)

    def ExportAllStyleables(self):
        """Make all styleable constants non-final, even non-resources ones.

        Resources that are styleable but not of int[] type are not actually
        resource IDs but constants. By default they are always final. Call this
        method to make them non-final anyway in the final R.java file.
        """
        self.export_const_styleable = True

    def GenerateOnResourcesLoaded(self, fake=False):
        """Generate an onResourcesLoaded() method.

        This Java method will be called at runtime by the framework when
        the corresponding library (which includes the R.java source file)
        will be loaded at runtime. This corresponds to the --shared-resources
        or --app-as-shared-lib flags of 'aapt package'.

        if |fake|, then the method will be empty bodied to compile faster. This
        useful for dummy R.java files that will eventually be replaced by real
        ones.
        """
        self.has_on_resources_loaded = True
        self.fake_on_resources_loaded = fake

    def SetFinalPackageId(self, package_id):
        """Sets a package ID to be used for resources marked final."""
        self.final_package_id = package_id

    def MaybeRewriteRTxtPackageIds(self, r_txt_path):
        """Rewrites package IDs in the R.txt file if necessary.

        If SetFinalPackageId() was called, some of the resource IDs may have had
        their package ID changed. This function rewrites the R.txt file to match
        those changes.
        """
        if self.final_package_id is None:
            return

        entries = _ParseTextSymbolsFile(r_txt_path)
        with open(r_txt_path, 'w', encoding='utf-8') as f:
            for entry in entries:
                value = entry.value
                if self.IsResourceFinal(entry):
                    value = re.sub(
                        r'0x(?:00|7f)',
                        '0x{:02x}'.format(self.final_package_id),
                        value,
                    )
                f.write(
                    '{} {} {} {}\n'.format(
                        entry.java_type, entry.resource_type, entry.name, value
                    )
                )

    def IsResourceFinal(self, entry):
        """Determines whether a resource should be final or not.

        Args:
          entry: A _TextSymbolEntry instance.
        Returns:
          True iff the corresponding entry should be final.
        """
        if entry.resource_type == 'styleable' and entry.java_type != 'int[]':
            # A styleable constant may be exported as non-final after all.
            return not self.export_const_styleable
        if not self.has_constant_ids:
            # Every resource is non-final
            return False
        if not self.resources_allowlist:
            # No allowlist means all IDs are non-final.
            return True
        # Otherwise, only those in the
        return entry.name not in self.resources_allowlist


def CreateRJavaFiles(
    output_srcjar_path,
    canonical_srcjar_path,
    package,
    main_r_txt_file,
    extra_res_packages,
    rjava_build_options,
    custom_root_package_name=None,
    grandparent_custom_package_name=None,
    ignore_mismatched_values=False,
    apk_under_test_rtxt=None,
):
    """Create all R.java files for a set of packages and R.txt files.

    Args:
      output_srcjar_path: Path of the srcjar zip file to write. This may be a
        temporary file path (e.g. in an atomic build context).
      canonical_srcjar_path: Canonical path of the final output srcjar file.
        Used to derive a deterministic default package name when
        custom_root_package_name is not provided.
      package: Package name for R java source files which will inherit
        from the root R java file.
      main_r_txt_file: The main R.txt file containing the valid values
        of _all_ resource IDs.
      extra_res_packages: A list of extra package names.
      rjava_build_options: An RJavaBuildOptions instance that controls how
        exactly the R.java file is generated.
      custom_root_package_name: Custom package name for module root R.java file,
        (eg. vr for gen.vr package).
      grandparent_custom_package_name: Custom root package name for the root
        R.java file to inherit from. DFM root R.java files will have "base"
        as the grandparent_custom_package_name. The format of this package name
        is identical to custom_root_package_name.
        (eg. for vr grandparent_custom_package_name would be "base")
      ignore_mismatched_values: If True, ignores if a resource appears multiple
        times with different entry values (useful when all the values are
        dummy anyways).
      apk_under_test_rtxt: R.txt of the apk_under_test.
    Raises:
      Exception if a package name appears several times in |extra_res_packages|
    """
    rjava_build_options.MaybeRewriteRTxtPackageIds(main_r_txt_file)

    packages = list(extra_res_packages)

    if package and package not in packages:
        # Sometimes, an apk target and a resources target share the same
        # AndroidManifest.xml and thus |package| will already be in |packages|.
        packages.append(package)

    # Map of (resource_type, name) -> Entry.
    # Contains the correct values for resources.
    all_resources = {}
    under_test_resources = set()

    if apk_under_test_rtxt:
        for entry in _ParseTextSymbolsFile(
            apk_under_test_rtxt, fix_package_ids=True
        ):
            entry_key = (entry.resource_type, entry.name)
            all_resources[entry_key] = entry
            under_test_resources.add(entry_key)

    for entry in _ParseTextSymbolsFile(main_r_txt_file, fix_package_ids=True):
        entry_key = (entry.resource_type, entry.name)
        if not all_resources.setdefault(entry_key, entry):
            # Resource not seen yet.
            continue
        if entry_key in under_test_resources:
            # Overwrite from apk_under_test is allowed.
            all_resources[entry_key] = entry
        else:
            # Duplicate within main_r_txt_file.
            if not ignore_mismatched_values:
                assert entry == all_resources[entry_key], (
                    'Input R.txt %s provided a duplicate resource with a different '
                    'entry value. Got %s, expected %s.'
                    % (main_r_txt_file, entry, all_resources[entry_key])
                )

    all_resources_by_type = collections.defaultdict(list)
    for entry in all_resources.values():
        all_resources_by_type[entry.resource_type].append(entry)
        assert entry.resource_type in ALL_RESOURCE_TYPES, (
            'Unknown resource type: %s, add to ALL_RESOURCE_TYPES!'
            % entry.resource_type
        )

    if custom_root_package_name:
        # Custom package name is available, thus use it for root_r_java_package.
        root_r_java_package = GetCustomPackagePath(custom_root_package_name)
    else:
        # Create a unique name using canonical_srcjar_path. Underscores are
        # added to ensure no reserved keywords are used for directory names.
        root_r_java_package = re.sub(
            r'[^\w\.]', '', canonical_srcjar_path.replace('/', '._')
        )

    root_java_file_contents = _RenderRootRJavaSource(
        root_r_java_package,
        all_resources_by_type,
        rjava_build_options,
        grandparent_custom_package_name,
    )
    body = _RenderRJavaBody(root_r_java_package, rjava_build_options)

    with action_helpers.atomic_output(output_srcjar_path) as f:
        with zipfile.ZipFile(f, 'w') as z:
            root_zip_path = root_r_java_package.replace('.', '/') + '/R.java'
            zip_helpers.add_to_zip_hermetic(
                z, root_zip_path, data=root_java_file_contents
            )
            for p in packages:
                zip_path = p.replace('.', '/') + '/R.java'
                data = _RenderRJavaSource(
                    p,
                    root_r_java_package,
                    rjava_build_options,
                    body=body,
                )
                zip_helpers.add_to_zip_hermetic(z, zip_path, data=data)


# Resource IDs inside resource arrays are sorted. Application resource IDs start
# with 0x7f but system resource IDs start with 0x01 thus system resource ids are
# always at the start of the array. This function finds the index of the first
# non system resource id to be used for package ID rewriting (we should not
# rewrite system resource ids).
def _GetNonSystemIndex(entry):
    """Get the index of the first application resource ID within a resource
    array."""
    res_ids = re.findall(r'0x[0-9a-f]{8}', entry.value)
    for i, res_id in enumerate(res_ids):
        if res_id.startswith('0x7f'):
            return i
    return len(res_ids)


def _RenderRJavaBody(root_r_java_package, rjava_build_options):
    lines = []
    for resource_type in sorted(ALL_RESOURCE_TYPES):
        lines.append(
            '    public static final class %s extends\n'
            '            %s.R.%s {}\n'
            % (resource_type, root_r_java_package, resource_type)
        )
    if rjava_build_options.has_on_resources_loaded:
        lines.append(
            '    public static void onResourcesLoaded(int packageId) {\n'
            '        %s.R.onResourcesLoaded(packageId);\n'
            '    }\n' % root_r_java_package
        )
    return ''.join(lines)


def _RenderRJavaSource(
    package,
    root_r_java_package,
    rjava_build_options,
    body=None,
):
    """Generates the contents of a R.java file."""
    if body is None:
        body = _RenderRJavaBody(root_r_java_package, rjava_build_options)
    return (
        '/* AUTO-GENERATED FILE.  DO NOT MODIFY. */\n\n'
        'package %s;\n\n'
        'public final class R {\n'
        '%s'
        '}\n' % (package, body)
    )


def GetCustomPackagePath(package_name):
    return 'gen.' + package_name + '_module'


def _RenderRootRJavaSource(
    package,
    all_resources_by_type,
    rjava_build_options,
    grandparent_custom_package_name,
):
    """Render an R.java source file. See _CreateRJaveSourceFile for args info."""
    final_resources_by_type = collections.defaultdict(list)
    non_final_resources_by_type = collections.defaultdict(list)
    for res_type, resources in all_resources_by_type.items():
        for entry in resources:
            # Entries in stylable that are not int[] are not actually resource ids
            # but constants.
            if rjava_build_options.IsResourceFinal(entry):
                final_resources_by_type[res_type].append(entry)
            else:
                non_final_resources_by_type[res_type].append(entry)

    # Here we diverge from what aapt does. Because we have so many
    # resources, the onResourcesLoaded method was exceeding the 64KB limit that
    # Java imposes. For this reason we split onResourcesLoaded into different
    # methods for each resource type.
    extends_string = ''
    dep_path = ''
    if grandparent_custom_package_name:
        extends_string = 'extends {{ parent_path }}.R.{{ resource_type }} '
        dep_path = GetCustomPackagePath(grandparent_custom_package_name)

    # Don't actually mark fields as "final" or else R8 complain when aapt2 uses
    # --proguard-conditional-keep-rules. E.g.:
    # Rule precondition matches static final fields javac has inlined.
    # Such rules are unsound as the shrinker cannot infer the inlining precisely.
    template = Template(
        """/* AUTO-GENERATED FILE.  DO NOT MODIFY. */

package {{ package }};

public final class R {
    {% for resource_type in resource_types %}
    public static class {{ resource_type }} """
        + extends_string
        + """ {
        {% for e in final_resources[resource_type] %}
        public static {{ e.java_type }} {{ e.name }} = {{ e.value }};
        {% endfor %}
        {% for e in non_final_resources[resource_type] %}
            {% if e.value not in ('0', '0x0') %}
        public static {{ e.java_type }} {{ e.name }} = {{ e.value }};
            {% else %}
        public static {{ e.java_type }} {{ e.name }};
            {% endif %}
        {% endfor %}
    }
    {% endfor %}
    {% if has_on_resources_loaded %}
      {% if fake_on_resources_loaded %}
    public static void onResourcesLoaded(int packageId) {
    }
      {% else %}
    private static boolean sResourcesDidLoad;

    private static void patchArray(
            int[] arr, int startIndex, int packageIdTransform) {
        for (int i = startIndex; i < arr.length; ++i) {
            arr[i] ^= packageIdTransform;
        }
    }

    public static void onResourcesLoaded(int packageId) {
        if (sResourcesDidLoad) {
            return;
        }
        sResourcesDidLoad = true;
        int packageIdTransform = (packageId ^ 0x7f) << 24;
        {#  aapt2 makes int[] resources refer to other resources by reference
            rather than by value. Thus, need to transform the int[] resources
            first, before the referenced resources are transformed in order to
            ensure the transform applies exactly once.
            See https://crbug.com/1237059 for context.
        #}
        {% for resource_type in resource_types %}
        {% for e in non_final_resources[resource_type] %}
        {% if e.java_type == 'int[]' %}
        patchArray({{ e.resource_type }}.{{ e.name }}, {{ startIndex(e) }}, \
packageIdTransform);
        {% endif %}
        {% endfor %}
        {% endfor %}
        {% for resource_type in resource_types %}
        onResourcesLoaded{{ resource_type|title }}(packageIdTransform);
        {% endfor %}
    }
    {% for res_type in resource_types %}
    private static void onResourcesLoaded{{ res_type|title }} (
            int packageIdTransform) {
        {% for e in non_final_resources[res_type] %}
        {% if res_type != 'styleable' and e.java_type != 'int[]' %}
        {{ e.resource_type }}.{{ e.name }} ^= packageIdTransform;
        {% endif %}
        {% endfor %}
    }
    {% endfor %}
      {% endif %}
    {% endif %}
}
""",
        trim_blocks=True,
        lstrip_blocks=True,
    )
    return template.render(
        package=package,
        resource_types=sorted(ALL_RESOURCE_TYPES),
        has_on_resources_loaded=rjava_build_options.has_on_resources_loaded,
        fake_on_resources_loaded=rjava_build_options.fake_on_resources_loaded,
        final_resources=final_resources_by_type,
        non_final_resources=non_final_resources_by_type,
        startIndex=_GetNonSystemIndex,
        parent_path=dep_path,
    )


def ExtractBinaryManifestValues(aapt2_path, apk_path):
    """Returns (version_code, version_name, package_name) for the given apk."""
    cmd = [
        aapt2_path,
        'dump',
        'xmltree',
        apk_path,
        '--file',
        'AndroidManifest.xml',
    ]
    filter_func = lambda output: build_utils.FilterLines(
        output, r'warn: unexpected chunk type'
    )
    output = build_utils.CheckOutput(cmd, stderr_filter=filter_func)
    version_code = re.search(r'versionCode.*?=(\d*)', output).group(1)
    version_name = re.search(r'versionName.*?="(.*?)"', output).group(1)
    package_name = re.search(r'package.*?="(.*?)"', output).group(1)
    return version_code, version_name, package_name


def ExtractArscPackage(aapt2_path, apk_path):
    """Returns (package_name, package_id) of resources.arsc from apk_path.

    When the apk does not have any entries in its resources file, in recent aapt2
    versions it will not contain a "Package" line. The package is not even in the
    actual resources.arsc/resources.pb file (which itself is mostly empty). Thus
    return (None, None) when dump succeeds and there are no errors to indicate
    that the package name does not exist in the resources file.
    """
    proc = subprocess.Popen(
        [aapt2_path, 'dump', 'resources', apk_path],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        encoding='utf-8',
    )
    for line in proc.stdout:
        # Package name=org.chromium.webview_shell id=7f
        if line.startswith('Package'):
            proc.kill()
            parts = line.split()
            package_name = parts[1].split('=')[1]
            package_id = parts[2][3:]
            return package_name, int(package_id, 16)

    # aapt2 currently crashes when dumping webview resources, but not until after
    # it prints the "Package" line (b/130553900).
    stderr_output = proc.stderr.read()
    if stderr_output:
        sys.stderr.write(stderr_output)
        raise Exception('Failed to find arsc package name')
    return None, None


def _RenameSubdirsWithPrefix(dir_path, prefix):
    subdirs = [
        d
        for d in os.listdir(dir_path)
        if os.path.isdir(os.path.join(dir_path, d))
    ]
    renamed_subdirs = []
    for d in subdirs:
        old_path = os.path.join(dir_path, d)
        new_path = os.path.join(dir_path, '{}_{}'.format(prefix, d))
        renamed_subdirs.append(new_path)
        os.rename(old_path, new_path)
    return renamed_subdirs


def _HasMultipleResDirs(zip_path):
    """Checks for magic comment set by prepare_resources.py

    Returns: True iff the zipfile has the magic comment that means it contains
    multiple res/ dirs inside instead of just contents of a single res/ dir
    (without a wrapping res/).
    """
    with zipfile.ZipFile(zip_path) as z:
        return z.comment == MULTIPLE_RES_MAGIC_STRING


def _ExtractSingleDep(z, deps_dir):
    subdirname = z.replace(os.path.sep, '_')
    subdir = os.path.join(deps_dir, subdirname)
    build_utils.ExtractAll(z, path=subdir)
    if _HasMultipleResDirs(z):
        # basename of the directory is used to create a zip during resource
        # compilation, include the path in the basename to help blame errors
        # on the correct target. For example directory 0_res may be renamed
        # chrome_android_chrome_app_java_resources_0_res pointing to the
        # name and path of the android_resources target from whence it came.
        return _RenameSubdirsWithPrefix(subdir, subdirname)
    return [subdir]


def ExtractDeps(dep_zips, deps_dir):
    """Extract a list of resource dependency zip files.

    Args:
       dep_zips: A list of zip file paths, each one will be extracted to
         a subdirectory of |deps_dir|, named after the zip file's path (e.g.
         '/some/path/foo.zip' -> '{deps_dir}/some_path_foo/').
      deps_dir: Top-level extraction directory.
    Returns:
      The list of all sub-directory paths, relative to |deps_dir|.
    Raises:
      Exception: If a sub-directory already exists with the same name before
        extraction.
    """
    seen_subdirs = set()
    for z in dep_zips:
        subdirname = z.replace(os.path.sep, '_')
        subdir = os.path.join(deps_dir, subdirname)
        if subdirname in seen_subdirs or os.path.exists(subdir):
            raise Exception('Resource zip name conflict: ' + subdirname)
        seen_subdirs.add(subdirname)

    dep_subdirs = []
    arg_tuples = [(z, deps_dir) for z in dep_zips]
    for subdirs in parallel.BulkForkAndCall(_ExtractSingleDep, arg_tuples):
        dep_subdirs.extend(subdirs)
    return dep_subdirs


class _ResourceBuildContext:
    """A temporary directory for packaging and compiling Android resources.

    Args:
      temp_dir: Optional root build directory path. If None, a temporary
        directory will be created, and removed in Close().
    """

    def __init__(self, temp_dir=None, keep_files=False):
        """Initialized the context."""
        # The top-level temporary directory.
        if temp_dir:
            self.temp_dir = temp_dir
            os.makedirs(temp_dir)
        else:
            self.temp_dir = tempfile.mkdtemp()
        self.remove_on_exit = not keep_files

        # A location to store resources extracted form dependency zip files.
        self.deps_dir = os.path.join(self.temp_dir, 'deps')
        os.mkdir(self.deps_dir)
        # A location to place aapt-generated files.
        self.gen_dir = os.path.join(self.temp_dir, 'gen')
        os.mkdir(self.gen_dir)
        # Temporary file locacations.
        self.r_txt_path = os.path.join(self.gen_dir, 'R.txt')
        self.srcjar_path = os.path.join(self.temp_dir, 'R.srcjar')
        self.info_path = os.path.join(self.temp_dir, 'size.info')
        self.stable_ids_path = os.path.join(self.temp_dir, 'in_ids.txt')
        self.emit_ids_path = os.path.join(self.temp_dir, 'out_ids.txt')
        self.proguard_path = os.path.join(self.temp_dir, 'keeps.flags')
        self.proguard_main_dex_path = os.path.join(
            self.temp_dir, 'maindex.flags'
        )
        self.arsc_path = os.path.join(self.temp_dir, 'out.ap_')
        self.proto_path = os.path.join(self.temp_dir, 'out.proto.ap_')
        self.optimized_arsc_path = os.path.join(self.temp_dir, 'out.opt.ap_')
        self.optimized_proto_path = os.path.join(
            self.temp_dir, 'out.opt.proto.ap_'
        )

    def Close(self):
        """Close the context and destroy all temporary files."""
        if self.remove_on_exit:
            shutil.rmtree(self.temp_dir)


@contextlib.contextmanager
def BuildContext(temp_dir=None, keep_files=False):
    """Generator for a _ResourceBuildContext instance."""
    context = None
    try:
        context = _ResourceBuildContext(temp_dir, keep_files)
        yield context
    finally:
        if context:
            context.Close()


def ParseAndroidResourceStringsFromXml(xml_data):
    """Parse and Android xml resource file and extract strings from it.

    Args:
      xml_data: XML file data.
    Returns:
      A (dict, namespaces) tuple, where |dict| maps string names to their UTF-8
      encoded value, and |namespaces| is a dictionary mapping prefixes to URLs
      corresponding to namespaces declared in the <resources> element.
    """
    # NOTE: This uses regular expression matching because parsing with something
    # like ElementTree makes it tedious to properly parse some of the structured
    # text found in string resources, e.g.:
    #      <string msgid="3300176832234831527" \
    #         name="abc_shareactionprovider_share_with_application">\
    #             "Condividi tramite <ns1:g id="APPLICATION_NAME">%s</ns1:g>"\
    #      </string>
    result = {}

    # Find <resources> start tag and extract namespaces from it.
    m = re.search(r'<resources([^>]*)>', xml_data, re.MULTILINE)
    if not m:
        raise Exception('<resources> start tag expected: ' + xml_data)
    input_data = xml_data[m.end() :]
    resource_attrs = m.group(1)
    re_namespace = re.compile(r'\s*(xmlns:(\w+)="([^"]+)")')
    namespaces = {}
    while resource_attrs:
        m = re_namespace.match(resource_attrs)
        if not m:
            break
        namespaces[m.group(2)] = m.group(3)
        resource_attrs = resource_attrs[m.end(1) :]

    # Find each string element now.
    re_string_element_start = re.compile(
        r'<string ([^>]* )?name="([^">]+)"[^>]*>'
    )
    re_string_element_end = re.compile(r'</string>')
    while input_data:
        m = re_string_element_start.search(input_data)
        if not m:
            break
        name = m.group(2)
        input_data = input_data[m.end() :]
        m2 = re_string_element_end.search(input_data)
        if not m2:
            raise Exception('Expected closing string tag: ' + input_data)
        text = input_data[: m2.start()]
        input_data = input_data[m2.end() :]
        if len(text) != 0 and text[0] == '"' and text[-1] == '"':
            text = text[1:-1]
        result[name] = text

    return result, namespaces


def GenerateAndroidResourceStringsXml(names_to_utf8_text, namespaces=None):
    """Generate an XML text corresponding to an Android resource strings map.

    Args:
      names_to_text: A dictionary mapping resource names to localized
        text (encoded as UTF-8).
      namespaces: A map of namespace prefix to URL.
    Returns:
      New non-Unicode string containing an XML data structure describing the
      input as an Android resource .xml file.
    """
    result = '<?xml version="1.0" encoding="utf-8"?>\n'
    result += '<resources'
    if namespaces:
        for prefix, url in sorted(namespaces.items()):
            result += ' xmlns:%s="%s"' % (prefix, url)
    result += '>\n'
    if not names_to_utf8_text:
        result += '<!-- this file intentionally empty -->\n'
    else:
        for name, utf8_text in sorted(names_to_utf8_text.items()):
            result += '<string name="%s">"%s"</string>\n' % (name, utf8_text)
    result += '</resources>\n'
    return result


def FilterAndroidResourceStringsXml(xml_file_path, string_predicate):
    """Remove unwanted localized strings from an Android resource .xml file.

    This function takes a |string_predicate| callable object that will
    receive a resource string name, and should return True iff the
    corresponding <string> element should be kept in the file.

    Args:
      xml_file_path: Android resource strings xml file path.
      string_predicate: A predicate function which will receive the string name
        and shal
    """
    with open(xml_file_path, encoding='utf-8') as f:
        xml_data = f.read()
    strings_map, namespaces = ParseAndroidResourceStringsFromXml(xml_data)

    string_deletion = False
    for name in list(strings_map.keys()):
        if not string_predicate(name):
            del strings_map[name]
            string_deletion = True

    if string_deletion:
        new_xml_data = GenerateAndroidResourceStringsXml(
            strings_map, namespaces
        )
        with open(xml_file_path, 'w', encoding='utf-8') as f:
            f.write(new_xml_data)
