Exemple #1
0
 def before_execute(self):
     self._finder = GradleDirectoryFinder(
         self._name,
         self._module_path,
         self._cache_dir,
         package_name=self._module_info['packagename'],
         config=self._config)
Exemple #2
0
    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))
    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))
Exemple #4
0
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self,
                 module_name,
                 path,
                 config,
                 changed_files,
                 module_info,
                 is_art,
                 all_module_info=None,
                 module_dir_map=None,
                 is_any_modules_have_res_changed=False,
                 changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self,
                                                      module_name,
                                                      path,
                                                      config,
                                                      changed_files,
                                                      module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(
                    self._config['project_source_sets'][mname]
                    ['main_res_directory'])
                self._merged_res_paths.extend(
                    self._config['project_source_sets'][mname]
                    ['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(
            self._name,
            self._module_path,
            self._cache_dir,
            package_name=self._module_info['packagename'],
            config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def _get_aapt_args(self):
        aapt_args = [
            self._aapt, 'package', '-f', '-I',
            os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
            '-M',
            fix_package_name(self._config,
                             self._finder.get_dst_manifest_path())
        ]

        for rdir in self._config['project_source_sets'][
                self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config[
                'extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'],
                                           'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][
                self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for m in self._module_info['local_module_dep']:
            if m in self._config['project_source_sets']:
                for adir in self._config['project_source_sets'][m][
                        'main_assets_directory']:
                    if os.path.exists(adir):
                        aapt_args.append('-A')
                        aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [
                fpath.replace('\\', '/') for fpath in final_changed_list
            ]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')
        return aapt_args, final_changed_list

    def check_other_modules_resources(self):
        if self._name == self._config[
                'main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(
                        self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(
            self._finder.get_backup_dir(),
            self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(
            get_file_content(main_r_fpath))
        buf = android_tools.fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(
            self.__get_freeline_backup_r_dir(),
            self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(
                backupdir, self._config['package'].replace('.', os.sep),
                'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [
                    self._config['package'], self._module_info['packagename']
                ]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep),
                                         'R.java')
                    if os.path.exists(
                            rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(
                    self._finder.get_backup_dir(),
                    self._module_info['packagename'].replace('.',
                                                             os.sep), 'R.java')
                if os.path.exists(main_r_path):
                    content = android_tools.fix_unicode_parse_error(
                        get_file_content(main_r_path), main_r_path)
                    write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module,
                                           self._module_dir_map[module],
                                           self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(
            os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][
            self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info[
                        'packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath,
                                               fn.replace('.class', ''))
                if fn.endswith(
                        '.class'
                ) and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace(
                        '.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath,
                                                   fn.replace('.class', ''))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets',
                                'debug',
                                fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(),
                                fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name,
                                                 res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(),
                                           package_name=package_name)
        if r_path and os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                      package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(
                    content, self._config['package'])
                content = android_tools.fix_unicode_parse_error(
                    content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module][
                'main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class',
                                  'public class').replace(
                                      'public static final class',
                                      'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace(
                'class ' + tag + ' {', 'class ' + tag + ' extends ' +
                main_package_name + '.R.' + tag + ' {')
        return content
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self, module_name, path, config, changed_files, module_info, is_art, all_module_info=None,
                 module_dir_map=None, is_any_modules_have_res_changed=False, changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self, module_name, path, config, changed_files, module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        self._replace_mapper = {}
        self._is_retrolambda_enabled = 'retrolambda' in self._config and self._name in self._config['retrolambda'] \
                                       and self._config['retrolambda'][self._name]['enabled']
        self._is_databinding_enabled = 'databinding_modules' in self._config and self._name in self._config[
            'databinding_modules']
        self._is_dagger_enabled = 'apt_libraries' in self._config and self._config['apt_libraries']['dagger']
        self._apt_output_dir = None
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_res_directory'])
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(self._name, self._module_path, self._cache_dir,
                                             package_name=self._module_info['packagename'], config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def process_databinding(self, original_changed_files, changed_files_ref):
        if 'databinding' in self._config:
            if self._config['databinding_modules'] == 0:
                self.debug('no modules for processing databinding')
                return

            databinding_config = self._config['databinding']
            DatabindingDirectoryLookUp.load_path_map(self._config['build_cache_dir'])
            procossor = DataBindingProcessor(self._config)
            for module_config in databinding_config:
                module_name = module_config['name']
                if module_name in original_changed_files['projects']:
                    resources_files = original_changed_files['projects'][module_config['name']]['res']
                    if len(resources_files) == 0:
                        self.debug('module {} has no resources files changed'.format(module_name))
                        continue

                    changed_files_map = {}
                    res_dirs = self._config['project_source_sets'][module_name]['main_res_directory']
                    # TODO: detect matches missing issue
                    for path in resources_files:
                        for rdir in res_dirs:
                            if path.startswith(rdir):
                                if rdir in changed_files_map:
                                    changed_files_map[rdir].append(path)
                                else:
                                    changed_files_map[rdir] = [path]
                                break

                    for rdir in changed_files_map.keys():
                        output_res_dir = DatabindingDirectoryLookUp.find_target_res_path(rdir)
                        output_java_dir = DatabindingDirectoryLookUp.find_target_java_path(rdir)
                        output_layoutinfo_dir = DatabindingDirectoryLookUp.get_merged_layoutinfo_dir(self._cache_dir)
                        if output_res_dir and output_java_dir and output_layoutinfo_dir:
                            changed_files_list = changed_files_map[rdir]
                            procossor.process_module_databinding(module_config, rdir, output_res_dir,
                                                                 output_layoutinfo_dir, output_java_dir,
                                                                 self._config['sdk_directory'],
                                                                 changed_files=changed_files_list)
                            # replace file path
                            for path in changed_files_list:
                                new_path = path.replace(rdir, output_res_dir)
                                self._merged_res_paths.append(output_res_dir)  # append new path prefix
                                self.debug('replace {} with output path: {}'.format(path, new_path))
                                self._replace_mapper[new_path] = path
                                self._changed_files['res'].remove(path)
                                self._changed_files['res'].append(new_path)

                            # mark java compiler
                            if os.path.exists(output_layoutinfo_dir):
                                has_layoutinfo = False
                                for name in os.listdir(output_layoutinfo_dir):
                                    if name.endswith('.xml'):
                                        has_layoutinfo = True
                                        break

                                if has_layoutinfo:
                                    info_file = os.path.join(output_java_dir, 'android', 'databinding', 'layouts',
                                                             'DataBindingInfo.java')
                                    if os.path.exists(info_file):
                                        append_files = [info_file]
                                        append_files.extend(procossor.extract_related_java_files(module_name,
                                                                                                 output_layoutinfo_dir))

                                        if 'apt' not in changed_files_ref['projects'][module_name]:
                                            changed_files_ref['projects'][module_name]['apt'] = []

                                        for fpath in append_files:
                                            self.debug('add {} to {} module'.format(fpath, module_name))
                                            changed_files_ref['projects'][module_name]['apt'].append(fpath)

                                        if not android_tools.is_src_changed(self._config['build_cache_dir']):
                                            android_tools.mark_src_changed(self._config['build_cache_dir'])

    def _get_aapt_args(self):
        aapt_args = [self._aapt, 'package', '-f', '-I',
                     os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
                     '-M', fix_package_name(self._config, self._finder.get_dst_manifest_path())]

        for rdir in self._config['project_source_sets'][self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(DatabindingDirectoryLookUp.find_target_res_path(rdir))

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(DatabindingDirectoryLookUp.find_target_res_path(rdir))

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config['extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'], 'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['local_dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [fpath.replace('\\', '/') for fpath in final_changed_list]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('--no-version-vectors')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')

        if 'ignore_resource_ids' in self._config and len(self._config['ignore_resource_ids']) > 0 and not is_windows_system():
            aapt_args.append('--ignore-ids')
            aapt_args.append(':'.join(self._config['ignore_resource_ids']))

        return aapt_args, final_changed_list

    def recover_original_file_path(self):
        copylist = list(self._changed_files['res'])
        for fpath in copylist:
            if fpath in self._replace_mapper:
                self._changed_files['res'].remove(fpath)
                self._changed_files['res'].append(self._replace_mapper[fpath])

    def check_other_modules_resources(self):
        if self._name == self._config['main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(self._finder.get_backup_dir(),
                                    self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(get_file_content(main_r_fpath))
        buf = android_tools.fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                         self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(backupdir, self._config['package'].replace('.', os.sep), 'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [self._config['package'], self._module_info['packagename']]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep), 'R.java')
                    if os.path.exists(rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if fpath and os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(self._finder.get_backup_dir(),
                                           self._module_info['packagename'].replace('.', os.sep), 'R.java')
                if os.path.exists(main_r_path):
                    content = android_tools.fix_unicode_parse_error(get_file_content(main_r_path), main_r_path)
                    write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        # add main module classes dir to classpath to generate databinding files
        main_module_name = self._config['main_project_name']
        if self._name != main_module_name and self._is_databinding_enabled:
            finder = GradleDirectoryFinder(main_module_name, self._module_dir_map[main_module_name], self._cache_dir,
                                           config=self._config)
            self._classpaths.append(finder.get_dst_classes_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))

    def fill_extra_javac_args(self):
        if 'apt' in self._config and self._name in self._config['apt'] and self._config['apt'][self._name]['enabled']:
            apt_config = self._config['apt'][self._name]
            self._apt_output_dir = apt_config['aptOutput']
            apt_args = ['-s', apt_config['aptOutput']]

            if apt_config['processor']:
                apt_args.append('-processor')
                apt_args.append(apt_config['processor'])

            if not apt_config['disableDiscovery']:
                apt_args.append('-processorpath')
                apt_args.append(apt_config['processorPath'])

            apt_args.extend(apt_config['aptArgs'])
            self._extra_javac_args.extend(apt_args)
        elif self._is_databinding_enabled:
            if self._name == self._config['main_project_name']:
                apt_output = os.path.join(self._config['build_directory'], 'generated', 'source', 'apt',
                                          self._config['product_flavor'], 'debug')
            else:
                apt_output = os.path.join(self._config['build_directory'], 'generated', 'source', 'apt', 'release')

            self._apt_output_dir = apt_output
            if not os.path.exists(apt_output):
                os.makedirs(apt_output)

            if self._config['databinding_compiler_jar'] != '':
                self.debug('add compiler jar to classpath: {}'.format(self._config['databinding_compiler_jar']))
                self._module_info['dep_jar_path'].append(self._config['databinding_compiler_jar'])

            apt_args = ['-s', apt_output, '-processorpath', os.pathsep.join(self._module_info['dep_jar_path'])]
            self._extra_javac_args.extend(apt_args)

    def run_apt_only(self):
        if self._is_databinding_enabled and self._should_run_databinding_apt():
            apt_args = self._generate_java_compile_args(extra_javac_args_enabled=True)
            self.debug('apt exec: ' + ' '.join(apt_args))
            output, err, code = cexec(apt_args, callback=None)

            if code != 0:
                raise FreelineException('apt compile failed.', '{}\n{}'.format(output, err))

            if self._apt_output_dir and os.path.exists(self._apt_output_dir):
                apt_cache_path = os.path.join(self._config['build_cache_dir'], 'apt_files_stat_cache.json')
                if os.path.exists(apt_cache_path):
                    apt_cache = load_json_cache(apt_cache_path)
                for dirpath, dirnames, files in os.walk(self._apt_output_dir):
                    for fn in files:
                        fpath = os.path.join(dirpath, fn)
                        if apt_cache and self._name in apt_cache:
                            if fpath in apt_cache[self._name]:
                                new_md5 = get_md5(fpath)
                                if new_md5 != apt_cache[self._name][fpath]['md5']:
                                    self.debug('detect new md5 value, add apt file to change list: {}'.format(fpath))
                                    self._changed_files['src'].append(fpath)
                            else:
                                self.debug('find new apt file, add to change list: {}'.format(fpath))
                                self._changed_files['src'].append(fpath)
                        else:
                            self.debug('apt cache not found, add to change list: {}'.format(fpath))
                            self._changed_files['src'].append(fpath)

    def run_javac_task(self):
        if self._is_only_r_changed() and not self._is_other_modules_has_src_changed:
            self._is_need_javac = False
            android_tools.clean_src_changed_flag(self._cache_dir)
            self.debug('apt process do not generate new files, ignore javac task.')
            return

        extra_javac_args_enabled = not (self._is_databinding_enabled and self._should_run_databinding_apt())
        javacargs = self._generate_java_compile_args(extra_javac_args_enabled=extra_javac_args_enabled)

        self.debug('javac exec: ' + ' '.join(javacargs))
        output, err, code = cexec(javacargs, callback=None)

        if code != 0:
            raise FreelineException('incremental javac compile failed.', '{}\n{}'.format(output, err))
        else:
            if self._is_r_file_changed:
                old_r_file = self._finder.get_dst_r_path(config=self._config)
                new_r_file = android_tools.DirectoryFinder.get_r_file_path(self._finder.get_backup_dir())
                if old_r_file and new_r_file:
                    shutil.copyfile(new_r_file, old_r_file)
                    self.debug('copy {} to {}'.format(new_r_file, old_r_file))

    def _should_run_databinding_apt(self):
        if 'apt' in self._changed_files:
            for fpath in self._changed_files['apt']:
                if fpath.endswith('DataBindingInfo.java'):
                    return True
        return False

    def _generate_java_compile_args(self, extra_javac_args_enabled=False):
        javacargs = [self._javac]
        arguments = ['-encoding', 'UTF-8', '-g']
        if not self._is_retrolambda_enabled:
            arguments.extend(['-target', '1.7', '-source', '1.7'])

        arguments.append('-cp')
        arguments.append(os.pathsep.join(self._classpaths))

        for fpath in self._changed_files['src']:
            arguments.append(fpath)

        if extra_javac_args_enabled:
            if 'apt' in self._changed_files:
                for fpath in self._changed_files['apt']:
                    arguments.append(fpath)

            filter_tags = []
            if self._is_databinding_enabled:
                filter_tags.extend(['BindingAdapter', 'BindingConversion', 'Bindable'])

            if self._is_dagger_enabled:
                filter_tags.extend(['DaggerComponent', 'DaggerModule'])

            files = self._get_apt_related_files(filter_tags=filter_tags)
            for fpath in files:
                if fpath and os.path.exists(fpath) and fpath not in self._changed_files['src']:
                    if 'apt' in self._changed_files and fpath in self._changed_files['apt']:
                        continue
                    self.debug('add apt related file: {}'.format(fpath))
                    arguments.append(fpath)

            arguments.extend(self._extra_javac_args)

        arguments.append('-d')
        arguments.append(self._finder.get_patch_classes_cache_dir())

        # ref: https://support.microsoft.com/en-us/kb/830473
        if is_windows_system():
            arguments_length = sum(map(len, arguments))
            if arguments_length > 8000:
                argument_file_path = os.path.join(self._finder.get_module_cache_dir(), 'javac_args_file')
                self.debug('arguments length: {} > 8000, save args to {}'.format(arguments_length, argument_file_path))

                if os.path.exists(argument_file_path):
                    os.remove(argument_file_path)

                arguments_content = ' '.join(arguments)
                self.debug('javac arguments: ' + arguments_content)
                write_file_content(argument_file_path, arguments_content)
                arguments = ['@{}'.format(argument_file_path)]

        javacargs.extend(arguments)
        return javacargs

    def _get_apt_related_files(self, filter_tags=None):
        path = self._get_apt_related_files_cache_path()
        if os.path.exists(path):
            return load_json_cache(path)
        else:
            info_path = os.path.join(self._cache_dir, 'freeline_annotation_info.json')
            if os.path.exists(info_path):
                info_cache = load_json_cache(info_path)
                related_files = []
                for anno, files in info_cache.iteritems():
                    if filter_tags and anno not in filter_tags:
                        self.debug('ignore annotation: {}'.format(anno))
                        continue

                    for info in files:
                        if info['module'] == self._name or info['module'] in self._module_info['local_module_dep']:
                            if 'java_path' in info and info['java_path']:
                                related_files.append(info['java_path'])
                write_json_cache(self._get_apt_related_files_cache_path(), related_files)
                return related_files
        return []

    def _append_new_related_files(self):
        related_files = self._get_apt_related_files()

        def append_files(file_list):
            for fpath in file_list:
                if fpath and fpath not in related_files:
                    self.debug('add new related file: {}'.format(fpath))
                    related_files.append(fpath)

        append_files(self._changed_files['src'])
        append_files(self._changed_files['apt'])
        write_json_cache(self._get_apt_related_files_cache_path(), related_files)

    def _get_apt_related_files_cache_path(self):
        return os.path.join(self._cache_dir, 'apt_related_files_cache.json')

    def run_retrolambda(self):
        if self._is_need_javac and self._is_retrolambda_enabled:
            lambda_config = self._config['retrolambda'][self._name]
            target_dir = self._finder.get_patch_classes_cache_dir()
            jar_args = [Builder.get_java(self._config),
                        '-Dretrolambda.inputDir={}'.format(target_dir),
                        '-Dretrolambda.outputDir={}'.format(target_dir)]

            if lambda_config['supportIncludeFiles']:
                files_stat_path = os.path.join(self._cache_dir, self._name, 'lambda_files_stat.json')

                include_files = []
                if os.path.exists(files_stat_path):
                    files_stat = load_json_cache(files_stat_path)
                else:
                    files_stat = {}

                for dirpath, dirnames, files in os.walk(target_dir):
                    for fn in files:
                        fpath = os.path.join(dirpath, fn)
                        if fpath not in files_stat:
                            include_files.append(fpath)
                            self.debug('incremental build new lambda file: {}'.format(fpath))
                        else:
                            if os.path.getmtime(fpath) > files_stat[fpath]['mtime']:
                                include_files.append(fpath)
                                self.debug('incremental build lambda file: {}'.format(fpath))

                include_files_param = os.pathsep.join(include_files)
                if len(include_files_param) > 3496:
                    include_files_path = os.path.join(self._cache_dir, self._name, 'retrolambda_inc.list')
                    self.__save_parms_to_file(include_files_path, include_files)
                    jar_args.append('-Dretrolambda.includedFile={}'.format(include_files_path))
                else:
                    jar_args.append('-Dretrolambda.includedFiles={}'.format(include_files_param))

            lambda_classpaths = [target_dir, lambda_config['rtJar']]
            lambda_classpaths.extend(self._classpaths)
            param = os.pathsep.join(lambda_classpaths)

            if lambda_config['supportIncludeFiles'] and len(param) > 3496:
                classpath_file = os.path.join(self._cache_dir, self._name, 'retrolambda_classpaths.path')
                self.__save_parms_to_file(classpath_file, lambda_classpaths)
                jar_args.append('-Dretrolambda.classpathFile={}'.format(classpath_file))
            else:
                jar_args.append('-Dretrolambda.classpath={}'.format(param))

            jar_args.append('-cp')
            jar_args.append(lambda_config['targetJar'])
            jar_args.append(lambda_config['mainClass'])

            self.debug('retrolambda exec: ' + ' '.join(jar_args))
            output, err, code = cexec(jar_args, callback=None)

            if code != 0:
                raise FreelineException('retrolambda compile failed.', '{}\n{}'.format(output, err))

            if lambda_config['supportIncludeFiles']:
                for fpath in include_files:
                    if fpath not in files_stat:
                        files_stat[fpath] = {}
                    files_stat[fpath]['mtime'] = os.path.getmtime(fpath)
                write_json_cache(files_stat_path, files_stat)
                self.debug('save lambda files stat to {}'.format(files_stat_path))

    def __save_parms_to_file(self, path, params):
        if os.path.exists(path):
            os.remove(path)
        content = ''
        for param in params:
            content += param + '\n'
        write_file_content(path, content)
        self.debug('save retrolambda params to {}'.format(path))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets', 'debug', fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(), fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name, res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        self.debug('relative path not found: {}'.format(res))
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(), package_name=package_name)
        if r_path and os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(), package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(content, self._config['package'])
                content = android_tools.fix_unicode_parse_error(content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module]['main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class', 'public class').replace('public static final class',
                                                                                'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace('class ' + tag + ' {',
                                      'class ' + tag + ' extends ' + main_package_name + '.R.' + tag + ' {')
        return content
 def before_execute(self):
     self._finder = GradleDirectoryFinder(self._name, self._module_path, self._cache_dir,
                                          package_name=self._module_info['packagename'], config=self._config)
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self, module_name, path, config, changed_files, module_info, is_art, all_module_info=None,
                 module_dir_map=None, is_any_modules_have_res_changed=False, changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self, module_name, path, config, changed_files, module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_res_directory'])
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(self._name, self._module_path, self._cache_dir,
                                             package_name=self._module_info['packagename'], config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def _get_aapt_args(self):
        aapt_args = [self._aapt, 'package', '-f', '-I',
                     os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
                     '-M', self._finder.get_dst_manifest_path()]

        for rdir in self._config['project_source_sets'][self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config['extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'], 'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for m in self._module_info['local_module_dep']:
            if m in self._config['project_source_sets']:
                for adir in self._config['project_source_sets'][m]['main_assets_directory']:
                    if os.path.exists(adir):
                        aapt_args.append('-A')
                        aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [fpath.replace('\\', '/') for fpath in final_changed_list]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')
        return aapt_args, final_changed_list

    def check_other_modules_resources(self):
        if self._name == self._config['main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(self._finder.get_backup_dir(),
                                    self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(get_file_content(main_r_fpath))
        buf = self.__fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                         self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(backupdir, self._config['package'].replace('.', os.sep), 'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [self._config['package'], self._module_info['packagename']]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep), 'R.java')
                    if os.path.exists(rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(self._finder.get_backup_dir(),
                                           self._module_info['packagename'].replace('.', os.sep), 'R.java')
                content = self.__fix_unicode_parse_error(get_file_content(main_r_path), main_r_path)
                write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets', 'debug', fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(), fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name, res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(), package_name=package_name)
        if os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(), package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(content, self._config['package'])
                content = self.__fix_unicode_parse_error(content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module]['main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    def __fix_unicode_parse_error(self, content, path):
        if content is not None and is_windows_system():
            self.debug("avoid windows unicode error for {}".format(path))
            return content.replace(r"\u", r"d")
        return content

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class', 'public class').replace('public static final class',
                                                                                'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace('class ' + tag + ' {',
                                      'class ' + tag + ' extends ' + main_package_name + '.R.' + tag + ' {')
        return content
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self, module_name, path, config, changed_files, module_info, is_art, all_module_info=None,
                 module_dir_map=None, is_any_modules_have_res_changed=False, changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self, module_name, path, config, changed_files, module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        self._replace_mapper = {}
        self._is_retrolambda_enabled = 'retrolambda' in self._config and self._name in self._config['retrolambda'] \
                                       and self._config['retrolambda'][self._name]['enabled']
        self._is_databinding_enabled = 'databinding_modules' in self._config and self._name in self._config[
            'databinding_modules']
        self._is_dagger_enabled = 'apt_libraries' in self._config and self._config['apt_libraries']['dagger']
        self._apt_output_dir = None
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_res_directory'])
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(self._name, self._module_path, self._cache_dir,
                                             package_name=self._module_info['packagename'], config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def process_databinding(self, original_changed_files, changed_files_ref):
        if 'databinding' in self._config:
            if self._config['databinding_modules'] == 0:
                self.debug('no modules for processing databinding')
                return

            databinding_config = self._config['databinding']
            DatabindingDirectoryLookUp.load_path_map(self._config['build_cache_dir'])
            procossor = DataBindingProcessor(self._config)
            for module_config in databinding_config:
                module_name = module_config['name']
                if module_name in original_changed_files['projects']:
                    resources_files = original_changed_files['projects'][module_config['name']]['res']
                    if len(resources_files) == 0:
                        self.debug('module {} has no resources files changed'.format(module_name))
                        continue

                    changed_files_map = {}
                    res_dirs = self._config['project_source_sets'][module_name]['main_res_directory']
                    # TODO: detect matches missing issue
                    for path in resources_files:
                        for rdir in res_dirs:
                            if path.startswith(rdir):
                                if rdir in changed_files_map:
                                    changed_files_map[rdir].append(path)
                                else:
                                    changed_files_map[rdir] = [path]
                                break

                    for rdir in changed_files_map.keys():
                        output_res_dir = DatabindingDirectoryLookUp.find_target_res_path(rdir)
                        output_java_dir = DatabindingDirectoryLookUp.find_target_java_path(rdir)
                        output_layoutinfo_dir = DatabindingDirectoryLookUp.get_merged_layoutinfo_dir(self._cache_dir)
                        if output_res_dir and output_java_dir and output_layoutinfo_dir:
                            changed_files_list = changed_files_map[rdir]
                            procossor.process_module_databinding(module_config, rdir, output_res_dir,
                                                                 output_layoutinfo_dir, output_java_dir,
                                                                 self._config['sdk_directory'],
                                                                 changed_files=changed_files_list)
                            # replace file path
                            for path in changed_files_list:
                                new_path = path.replace(rdir, output_res_dir)
                                self._merged_res_paths.append(output_res_dir)  # append new path prefix
                                self.debug('replace {} with output path: {}'.format(path, new_path))
                                self._replace_mapper[new_path] = path
                                self._changed_files['res'].remove(path)
                                self._changed_files['res'].append(new_path)

                            # mark java compiler
                            if os.path.exists(output_layoutinfo_dir):
                                has_layoutinfo = False
                                for name in os.listdir(output_layoutinfo_dir):
                                    if name.endswith('.xml'):
                                        has_layoutinfo = True
                                        break

                                if has_layoutinfo:
                                    info_file = os.path.join(output_java_dir, 'android', 'databinding', 'layouts',
                                                             'DataBindingInfo.java')
                                    if os.path.exists(info_file):
                                        append_files = [info_file]
                                        append_files.extend(procossor.extract_related_java_files(module_name,
                                                                                                 output_layoutinfo_dir))

                                        if 'apt' not in changed_files_ref['projects'][module_name]:
                                            changed_files_ref['projects'][module_name]['apt'] = []

                                        for fpath in append_files:
                                            self.debug('add {} to {} module'.format(fpath, module_name))
                                            changed_files_ref['projects'][module_name]['apt'].append(fpath)

                                        if not android_tools.is_src_changed(self._config['build_cache_dir']):
                                            android_tools.mark_src_changed(self._config['build_cache_dir'])

    def _get_aapt_args(self):
        aapt_args = [self._aapt, 'package', '-f', '-I',
                     os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
                     '-M', fix_package_name(self._config, self._finder.get_dst_manifest_path())]

        for rdir in self._config['project_source_sets'][self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(DatabindingDirectoryLookUp.find_target_res_path(rdir))

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(DatabindingDirectoryLookUp.find_target_res_path(rdir))

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config['extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'], 'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['local_dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [fpath.replace('\\', '/') for fpath in final_changed_list]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('--no-version-vectors')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')

        if 'ignore_resource_ids' in self._config and len(self._config['ignore_resource_ids']) > 0 and not is_windows_system():
            aapt_args.append('--ignore-ids')
            aapt_args.append(':'.join(self._config['ignore_resource_ids']))

        return aapt_args, final_changed_list

    def recover_original_file_path(self):
        copylist = list(self._changed_files['res'])
        for fpath in copylist:
            if fpath in self._replace_mapper:
                self._changed_files['res'].remove(fpath)
                self._changed_files['res'].append(self._replace_mapper[fpath])

    def check_other_modules_resources(self):
        if self._name == self._config['main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(self._finder.get_backup_dir(),
                                    self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(get_file_content(main_r_fpath))
        buf = android_tools.fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                         self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(backupdir, self._config['package'].replace('.', os.sep), 'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [self._config['package'], self._module_info['packagename']]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep), 'R.java')
                    if os.path.exists(rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if fpath and os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(self._finder.get_backup_dir(),
                                           self._module_info['packagename'].replace('.', os.sep), 'R.java')
                if os.path.exists(main_r_path):
                    content = android_tools.fix_unicode_parse_error(get_file_content(main_r_path), main_r_path)
                    write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        # add main module classes dir to classpath to generate databinding files
        main_module_name = self._config['main_project_name']
        if self._name != main_module_name and self._is_databinding_enabled:
            finder = GradleDirectoryFinder(main_module_name, self._module_dir_map[main_module_name], self._cache_dir,
                                           config=self._config)
            self._classpaths.append(finder.get_dst_classes_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))

    def fill_extra_javac_args(self):
        if 'apt' in self._config and self._name in self._config['apt'] and self._config['apt'][self._name]['enabled']:
            apt_config = self._config['apt'][self._name]
            self._apt_output_dir = apt_config['aptOutput']
            apt_args = ['-s', apt_config['aptOutput']]

            if apt_config['processor']:
                apt_args.append('-processor')
                apt_args.append(apt_config['processor'])

            if not apt_config['disableDiscovery']:
                apt_args.append('-processorpath')
                apt_args.append(apt_config['processorPath'])

            apt_args.extend(apt_config['aptArgs'])
            self._extra_javac_args.extend(apt_args)
        elif self._is_databinding_enabled:
            if self._name == self._config['main_project_name']:
                apt_output = os.path.join(self._config['build_directory'], 'generated', 'source', 'apt',
                                          self._config['product_flavor'], 'debug')
            else:
                apt_output = os.path.join(self._config['build_directory'], 'generated', 'source', 'apt', 'release')

            self._apt_output_dir = apt_output
            if not os.path.exists(apt_output):
                os.makedirs(apt_output)

            if self._config['databinding_compiler_jar'] != '':
                self.debug('add compiler jar to classpath: {}'.format(self._config['databinding_compiler_jar']))
                self._module_info['dep_jar_path'].append(self._config['databinding_compiler_jar'])

            apt_args = ['-s', apt_output, '-processorpath', os.pathsep.join(self._module_info['dep_jar_path'])]
            self._extra_javac_args.extend(apt_args)

    def run_apt_only(self):
        if self._is_databinding_enabled and self._should_run_databinding_apt():
            apt_args = self._generate_java_compile_args(extra_javac_args_enabled=True)
            self.debug('apt exec: ' + ' '.join(apt_args))
            output, err, code = cexec(apt_args, callback=None)

            if code != 0:
                raise FreelineException('apt compile failed.', '{}\n{}'.format(output, err))

            if self._apt_output_dir and os.path.exists(self._apt_output_dir):
                apt_cache_path = os.path.join(self._config['build_cache_dir'], 'apt_files_stat_cache.json')
                if os.path.exists(apt_cache_path):
                    apt_cache = load_json_cache(apt_cache_path)
                for dirpath, dirnames, files in os.walk(self._apt_output_dir):
                    for fn in files:
                        fpath = os.path.join(dirpath, fn)
                        if apt_cache and self._name in apt_cache:
                            if fpath in apt_cache[self._name]:
                                new_md5 = get_md5(fpath)
                                if new_md5 != apt_cache[self._name][fpath]['md5']:
                                    self.debug('detect new md5 value, add apt file to change list: {}'.format(fpath))
                                    self._changed_files['src'].append(fpath)
                            else:
                                self.debug('find new apt file, add to change list: {}'.format(fpath))
                                self._changed_files['src'].append(fpath)
                        else:
                            self.debug('apt cache not found, add to change list: {}'.format(fpath))
                            self._changed_files['src'].append(fpath)

    def run_javac_task(self):
        if self._is_only_r_changed() and not self._is_other_modules_has_src_changed:
            self._is_need_javac = False
            android_tools.clean_src_changed_flag(self._cache_dir)
            self.debug('apt process do not generate new files, ignore javac task.')
            return

        extra_javac_args_enabled = not (self._is_databinding_enabled and self._should_run_databinding_apt())
        javacargs = self._generate_java_compile_args(extra_javac_args_enabled=extra_javac_args_enabled)

        self.debug('javac exec: ' + ' '.join(javacargs))
        output, err, code = cexec(javacargs, callback=None)

        if code != 0:
            raise FreelineException('incremental javac compile failed.', '{}\n{}'.format(output, err))
        else:
            if self._is_r_file_changed:
                old_r_file = self._finder.get_dst_r_path(config=self._config)
                new_r_file = android_tools.DirectoryFinder.get_r_file_path(self._finder.get_backup_dir())
                if old_r_file and new_r_file:
                    shutil.copyfile(new_r_file, old_r_file)
                    self.debug('copy {} to {}'.format(new_r_file, old_r_file))

    def _should_run_databinding_apt(self):
        if 'apt' in self._changed_files:
            for fpath in self._changed_files['apt']:
                if fpath.endswith('DataBindingInfo.java'):
                    return True
        return False

    def _generate_java_compile_args(self, extra_javac_args_enabled=False):
        javacargs = [self._javac]
        arguments = ['-encoding', 'UTF-8', '-g']
        if not self._is_retrolambda_enabled:
            arguments.extend(['-target', '1.7', '-source', '1.7'])

        arguments.append('-cp')
        arguments.append(os.pathsep.join(self._classpaths))

        for fpath in self._changed_files['src']:
            arguments.append(fpath)

        if extra_javac_args_enabled:
            if 'apt' in self._changed_files:
                for fpath in self._changed_files['apt']:
                    arguments.append(fpath)

            filter_tags = []
            if self._is_databinding_enabled:
                filter_tags.extend(['BindingAdapter', 'BindingConversion', 'Bindable'])

            if self._is_dagger_enabled:
                filter_tags.extend(['DaggerComponent', 'DaggerModule'])

            files = self._get_apt_related_files(filter_tags=filter_tags)
            for fpath in files:
                if fpath and os.path.exists(fpath) and fpath not in self._changed_files['src']:
                    if 'apt' in self._changed_files and fpath in self._changed_files['apt']:
                        continue
                    self.debug('add apt related file: {}'.format(fpath))
                    arguments.append(fpath)

            arguments.extend(self._extra_javac_args)

        arguments.append('-d')
        arguments.append(self._finder.get_patch_classes_cache_dir())

        # ref: https://support.microsoft.com/en-us/kb/830473
        if is_windows_system():
            arguments_length = sum(map(len, arguments))
            if arguments_length > 8000:
                argument_file_path = os.path.join(self._finder.get_module_cache_dir(), 'javac_args_file')
                self.debug('arguments length: {} > 8000, save args to {}'.format(arguments_length, argument_file_path))

                if os.path.exists(argument_file_path):
                    os.remove(argument_file_path)

                arguments_content = ' '.join(arguments)
                self.debug('javac arguments: ' + arguments_content)
                write_file_content(argument_file_path, arguments_content)
                arguments = ['@{}'.format(argument_file_path)]

        javacargs.extend(arguments)
        return javacargs

    def _get_apt_related_files(self, filter_tags=None):
        path = self._get_apt_related_files_cache_path()
        if os.path.exists(path):
            return load_json_cache(path)
        else:
            info_path = os.path.join(self._cache_dir, 'freeline_annotation_info.json')
            if os.path.exists(info_path):
                info_cache = load_json_cache(info_path)
                related_files = []
                for anno, files in info_cache.iteritems():
                    if filter_tags and anno not in filter_tags:
                        self.debug('ignore annotation: {}'.format(anno))
                        continue

                    for info in files:
                        if info['module'] == self._name or info['module'] in self._module_info['local_module_dep']:
                            if 'java_path' in info and info['java_path']:
                                related_files.append(info['java_path'])
                write_json_cache(self._get_apt_related_files_cache_path(), related_files)
                return related_files
        return []

    def _append_new_related_files(self):
        related_files = self._get_apt_related_files()

        def append_files(file_list):
            for fpath in file_list:
                if fpath and fpath not in related_files:
                    self.debug('add new related file: {}'.format(fpath))
                    related_files.append(fpath)

        append_files(self._changed_files['src'])
        append_files(self._changed_files['apt'])
        write_json_cache(self._get_apt_related_files_cache_path(), related_files)

    def _get_apt_related_files_cache_path(self):
        return os.path.join(self._cache_dir, 'apt_related_files_cache.json')

    def run_retrolambda(self):
        if self._is_need_javac and self._is_retrolambda_enabled:
            lambda_config = self._config['retrolambda'][self._name]
            target_dir = self._finder.get_patch_classes_cache_dir()
            jar_args = [Builder.get_java(self._config),
                        '-Dretrolambda.inputDir={}'.format(target_dir),
                        '-Dretrolambda.outputDir={}'.format(target_dir)]

            if lambda_config['supportIncludeFiles']:
                files_stat_path = os.path.join(self._cache_dir, self._name, 'lambda_files_stat.json')

                include_files = []
                if os.path.exists(files_stat_path):
                    files_stat = load_json_cache(files_stat_path)
                else:
                    files_stat = {}

                for dirpath, dirnames, files in os.walk(target_dir):
                    for fn in files:
                        fpath = os.path.join(dirpath, fn)
                        if fpath not in files_stat:
                            include_files.append(fpath)
                            self.debug('incremental build new lambda file: {}'.format(fpath))
                        else:
                            if os.path.getmtime(fpath) > files_stat[fpath]['mtime']:
                                include_files.append(fpath)
                                self.debug('incremental build lambda file: {}'.format(fpath))

                include_files_param = os.pathsep.join(include_files)
                if len(include_files_param) > 3496:
                    include_files_path = os.path.join(self._cache_dir, self._name, 'retrolambda_inc.list')
                    self.__save_parms_to_file(include_files_path, include_files)
                    jar_args.append('-Dretrolambda.includedFile={}'.format(include_files_path))
                else:
                    jar_args.append('-Dretrolambda.includedFiles={}'.format(include_files_param))

            lambda_classpaths = [target_dir, lambda_config['rtJar']]
            lambda_classpaths.extend(self._classpaths)
            param = os.pathsep.join(lambda_classpaths)

            if lambda_config['supportIncludeFiles'] and len(param) > 3496:
                classpath_file = os.path.join(self._cache_dir, self._name, 'retrolambda_classpaths.path')
                self.__save_parms_to_file(classpath_file, lambda_classpaths)
                jar_args.append('-Dretrolambda.classpathFile={}'.format(classpath_file))
            else:
                jar_args.append('-Dretrolambda.classpath={}'.format(param))

            jar_args.append('-cp')
            jar_args.append(lambda_config['targetJar'])
            jar_args.append(lambda_config['mainClass'])

            self.debug('retrolambda exec: ' + ' '.join(jar_args))
            output, err, code = cexec(jar_args, callback=None)

            if code != 0:
                raise FreelineException('retrolambda compile failed.', '{}\n{}'.format(output, err))

            if lambda_config['supportIncludeFiles']:
                for fpath in include_files:
                    if fpath not in files_stat:
                        files_stat[fpath] = {}
                    files_stat[fpath]['mtime'] = os.path.getmtime(fpath)
                write_json_cache(files_stat_path, files_stat)
                self.debug('save lambda files stat to {}'.format(files_stat_path))

    def __save_parms_to_file(self, path, params):
        if os.path.exists(path):
            os.remove(path)
        content = ''
        for param in params:
            content += param + '\n'
        write_file_content(path, content)
        self.debug('save retrolambda params to {}'.format(path))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets', 'debug', fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(), fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name, res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        self.debug('relative path not found: {}'.format(res))
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(), package_name=package_name)
        if r_path and os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(), package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(content, self._config['package'])
                content = android_tools.fix_unicode_parse_error(content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module]['main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class', 'public class').replace('public static final class',
                                                                                'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace('class ' + tag + ' {',
                                      'class ' + tag + ' extends ' + main_package_name + '.R.' + tag + ' {')
        return content
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self,
                 module_name,
                 path,
                 config,
                 changed_files,
                 module_info,
                 is_art,
                 all_module_info=None,
                 module_dir_map=None,
                 is_any_modules_have_res_changed=False,
                 changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self,
                                                      module_name,
                                                      path,
                                                      config,
                                                      changed_files,
                                                      module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        self._is_retrolambda_enabled = 'retrolambda' in self._config and self._name in self._config['retrolambda'] \
                                       and self._config['retrolambda'][self._name]['enabled']
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(
                    self._config['project_source_sets'][mname]
                    ['main_res_directory'])
                self._merged_res_paths.extend(
                    self._config['project_source_sets'][mname]
                    ['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(
            self._name,
            self._module_path,
            self._cache_dir,
            package_name=self._module_info['packagename'],
            config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def _get_aapt_args(self):
        aapt_args = [
            self._aapt, 'package', '-f', '-I',
            os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
            '-M',
            fix_package_name(self._config,
                             self._finder.get_dst_manifest_path())
        ]

        for rdir in self._config['project_source_sets'][
                self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config[
                'extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'],
                                           'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][
                self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for m in self._module_info['local_module_dep']:
            if m in self._config['project_source_sets']:
                for adir in self._config['project_source_sets'][m][
                        'main_assets_directory']:
                    if os.path.exists(adir):
                        aapt_args.append('-A')
                        aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [
                fpath.replace('\\', '/') for fpath in final_changed_list
            ]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('--no-version-vectors')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')
        return aapt_args, final_changed_list

    def check_other_modules_resources(self):
        if self._name == self._config[
                'main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(
                        self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(
            self._finder.get_backup_dir(),
            self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(
            get_file_content(main_r_fpath))
        buf = android_tools.fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(
            self.__get_freeline_backup_r_dir(),
            self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(
                backupdir, self._config['package'].replace('.', os.sep),
                'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [
                    self._config['package'], self._module_info['packagename']
                ]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep),
                                         'R.java')
                    if os.path.exists(
                            rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(
                    self._finder.get_backup_dir(),
                    self._module_info['packagename'].replace('.',
                                                             os.sep), 'R.java')
                if os.path.exists(main_r_path):
                    content = android_tools.fix_unicode_parse_error(
                        get_file_content(main_r_path), main_r_path)
                    write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module,
                                           self._module_dir_map[module],
                                           self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(
            os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][
            self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info[
                        'packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath,
                                               fn.replace('.class', ''))
                if fn.endswith(
                        '.class'
                ) and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace(
                        '.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath,
                                                   fn.replace('.class', ''))

    def fill_extra_javac_args(self):
        if 'apt' in self._config and self._name in self._config[
                'apt'] and self._config['apt'][self._name]['enabled']:
            apt_config = self._config['apt'][self._name]
            apt_args = ['-s', apt_config['aptOutput']]

            if apt_config['processor']:
                apt_args.append('-processor')
                apt_args.append(apt_config['processor'])

            if not apt_config['disableDiscovery']:
                apt_args.append('-processorpath')
                apt_args.append(apt_config['processorPath'])

            apt_args.extend(apt_config['aptArgs'])
            self._extra_javac_args.extend(apt_args)

    def run_javac_task(self):
        javacargs = [self._javac, '-encoding', 'UTF-8', '-g']
        if not self._is_retrolambda_enabled:
            javacargs.extend(['-target', '1.7', '-source', '1.7'])

        javacargs.append('-cp')
        javacargs.append(os.pathsep.join(self._classpaths))

        for fpath in self._changed_files['src']:
            javacargs.append(fpath)

        javacargs.extend(self._extra_javac_args)
        javacargs.append('-d')
        javacargs.append(self._finder.get_patch_classes_cache_dir())

        self.debug('javac exec: ' + ' '.join(javacargs))
        output, err, code = cexec(javacargs, callback=None)

        if code != 0:
            raise FreelineException('incremental javac compile failed.',
                                    '{}\n{}'.format(output, err))
        else:
            if self._is_r_file_changed:
                old_r_file = self._finder.get_dst_r_path(config=self._config)
                new_r_file = android_tools.DirectoryFinder.get_r_file_path(
                    self._finder.get_backup_dir())
                shutil.copyfile(new_r_file, old_r_file)
                self.debug('copy {} to {}'.format(new_r_file, old_r_file))

    def run_retrolambda(self):
        if self._is_retrolambda_enabled:
            lambda_config = self._config['retrolambda'][self._name]
            target_dir = self._finder.get_patch_classes_cache_dir()
            jar_args = [
                Builder.get_java(self._config),
                '-Dretrolambda.inputDir={}'.format(target_dir),
                '-Dretrolambda.outputDir={}'.format(target_dir)
            ]

            if lambda_config['supportIncludeFiles']:
                include_files = []
                classes = []
                for dirpath, dirnames, files in os.walk(target_dir):
                    for fn in files:
                        if fn.endswith('.class'):
                            classes.append(
                                os.path.relpath(os.path.join(dirpath, fn),
                                                target_dir))

                src_dirs = self._config['project_source_sets'][
                    self._name]['main_src_directory']
                for fpath in self._changed_files['src']:
                    short_path = fpath.replace('.java', '.class')
                    for src_dir in src_dirs:
                        if src_dir in short_path:
                            short_path = os.path.relpath(fpath,
                                                         src_dir).replace(
                                                             '.java', '')
                            break

                    for clazz in classes:
                        if short_path + '.class' in clazz or short_path + '$' in clazz or 'R.class' in clazz \
                                or 'R$' in clazz or short_path + '_' in clazz:
                            include_file = os.path.join(target_dir, clazz)
                            if os.path.exists(include_file):
                                self.debug(
                                    'incremental build lambda file: {}'.format(
                                        include_file))
                                include_files.append(include_file)

                include_files_param = os.pathsep.join(include_files)
                if len(include_files_param) > 3496:
                    include_files_path = os.path.join(self._cache_dir,
                                                      self._name,
                                                      'retrolambda_inc.list')
                    self.__save_parms_to_file(include_files_path,
                                              include_files)
                    jar_args.append('-Dretrolambda.includedFile={}'.format(
                        include_files_path))
                else:
                    jar_args.append('-Dretrolambda.includedFiles={}'.format(
                        include_files_param))

            lambda_classpaths = [target_dir, lambda_config['rtJar']]
            lambda_classpaths.extend(self._classpaths)
            param = os.pathsep.join(lambda_classpaths)

            if lambda_config['supportIncludeFiles'] and len(param) > 3496:
                classpath_file = os.path.join(self._cache_dir, self._name,
                                              'retrolambda_classpaths.path')
                self.__save_parms_to_file(classpath_file, lambda_classpaths)
                jar_args.append(
                    '-Dretrolambda.classpathFile={}'.format(classpath_file))
            else:
                jar_args.append('-Dretrolambda.classpath={}'.format(param))

            jar_args.append('-cp')
            jar_args.append(lambda_config['targetJar'])
            jar_args.append(lambda_config['mainClass'])

            self.debug('retrolambda exec: ' + ' '.join(jar_args))
            output, err, code = cexec(jar_args, callback=None)

            if code != 0:
                raise FreelineException('retrolambda compile failed.',
                                        '{}\n{}'.format(output, err))

    def __save_parms_to_file(self, path, params):
        if os.path.exists(path):
            os.remove(path)
        content = ''
        for param in params:
            content += param + '\n'
        write_file_content(path, content)
        self.debug('save retrolambda params to {}'.format(path))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets',
                                'debug',
                                fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(),
                                fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name,
                                                 res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(),
                                           package_name=package_name)
        if r_path and os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                      package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(
                    content, self._config['package'])
                content = android_tools.fix_unicode_parse_error(
                    content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module][
                'main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class',
                                  'public class').replace(
                                      'public static final class',
                                      'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace(
                'class ' + tag + ' {', 'class ' + tag + ' extends ' +
                main_package_name + '.R.' + tag + ' {')
        return content
class GradleIncBuildInvoker(android_tools.AndroidIncBuildInvoker):
    def __init__(self, module_name, path, config, changed_files, module_info, is_art, all_module_info=None,
                 module_dir_map=None, is_any_modules_have_res_changed=False, changed_modules=None):
        android_tools.AndroidIncBuildInvoker.__init__(self, module_name, path, config, changed_files, module_info,
                                                      is_art=is_art)
        self._all_module_info = all_module_info
        self._module_dir_map = module_dir_map
        self._is_any_modules_have_res_changed = is_any_modules_have_res_changed
        self._changed_modules = changed_modules
        self._merged_res_paths = []
        self._merged_res_paths.append(self._finder.get_backup_res_dir())
        self._is_retrolambda_enabled = 'retrolambda' in self._config and self._name in self._config['retrolambda'] \
                                       and self._config['retrolambda'][self._name]['enabled']
        for mname in self._all_module_info.keys():
            if mname in self._config['project_source_sets']:
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_res_directory'])
                self._merged_res_paths.extend(self._config['project_source_sets'][mname]['main_assets_directory'])

    def before_execute(self):
        self._finder = GradleDirectoryFinder(self._name, self._module_path, self._cache_dir,
                                             package_name=self._module_info['packagename'], config=self._config)

    def check_res_task(self):
        if self._name != self._config['main_project_name']:
            self.debug('skip {} aapt task'.format(self._name))
            return False
        return android_tools.AndroidIncBuildInvoker.check_res_task(self)

    def fill_dependant_jars(self):
        self._res_dependencies = self._module_info['dep_jar_path']

    def _get_aapt_args(self):
        aapt_args = [self._aapt, 'package', '-f', '-I',
                     os.path.join(self._config['compile_sdk_directory'], 'android.jar'),
                     '-M', fix_package_name(self._config, self._finder.get_dst_manifest_path())]

        for rdir in self._config['project_source_sets'][self._name]['main_res_directory']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for rdir in self._module_info['local_dep_res_path']:
            if os.path.exists(rdir):
                aapt_args.append('-S')
                aapt_args.append(rdir)

        for resdir in self._module_info['dep_res_path']:
            if os.path.exists(resdir):
                aapt_args.append('-S')
                aapt_args.append(resdir)

        if 'extra_dep_res_paths' in self._config and self._config['extra_dep_res_paths'] is not None:
            arr = self._config['extra_dep_res_paths']
            for path in arr:
                path = path.strip()
                if os.path.isdir(path):
                    aapt_args.append('-S')
                    aapt_args.append(path)

        aapt_args.append('-S')
        aapt_args.append(self._finder.get_backup_res_dir())

        freeline_assets_dir = os.path.join(self._config['build_cache_dir'], 'freeline-assets')
        aapt_args.append('-A')
        aapt_args.append(freeline_assets_dir)

        for adir in self._config['project_source_sets'][self._name]['main_assets_directory']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['local_dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        for adir in self._module_info['dep_assets_path']:
            if os.path.exists(adir):
                aapt_args.append('-A')
                aapt_args.append(adir)

        gen_path = self._finder.get_backup_dir()
        aapt_args.append('--custom-package')
        aapt_args.append(self._config['package'])
        aapt_args.append('-m')
        aapt_args.append('-J')
        aapt_args.append(gen_path)
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('-P')
        aapt_args.append(self._finder.get_public_xml_path())

        final_changed_list = self._parse_changed_list()

        if is_windows_system():
            final_changed_list = [fpath.replace('\\', '/') for fpath in final_changed_list]

        final_changed_list_chain = ':'.join(final_changed_list)

        aapt_args.append('-F')
        aapt_args.append(self._finder.get_dst_res_pack_path(self._name))
        aapt_args.append('--debug-mode')
        aapt_args.append('--auto-add-overlay')
        aapt_args.append('--no-version-vectors')

        if len(final_changed_list_chain) > 0 and self._is_art:
            aapt_args.append('--buildIncrement')
            aapt_args.append(final_changed_list_chain)
            aapt_args.append('--resoucres-md5-cache-path')
            aapt_args.append(os.path.join(self._cache_dir, "arsc_cache.dat"))

        aapt_args.append('--ignore-assets')
        aapt_args.append('public_id.xml:public.xml:*.bak:.*')
        return aapt_args, final_changed_list

    def check_other_modules_resources(self):
        if self._name == self._config['main_project_name'] and self._all_module_info is not None:
            changed_modules = self._changed_modules

            if len(changed_modules) > 0:
                self.__modify_main_r()

                for module in changed_modules:
                    fpath = self.__modify_other_modules_r(self._all_module_info[module]['packagename'])
                    self.debug('modify {}'.format(fpath))

    def __modify_main_r(self):
        main_r_fpath = os.path.join(self._finder.get_backup_dir(),
                                    self._module_info['packagename'].replace('.', os.sep), 'R.java')

        self.debug('modify {}'.format(main_r_fpath))
        buf = GradleIncBuildInvoker.remove_final_tag(get_file_content(main_r_fpath))
        buf = android_tools.fix_unicode_parse_error(buf, main_r_fpath)
        write_file_content(main_r_fpath, buf)

        target_main_r_dir = os.path.join(self.__get_freeline_backup_r_dir(),
                                         self._module_info['packagename'].replace('.', os.sep))
        if not os.path.exists(target_main_r_dir):
            os.makedirs(target_main_r_dir)

        target_main_r_path = os.path.join(target_main_r_dir, 'R.java')
        self.debug('copy {} to {}'.format(main_r_fpath, target_main_r_path))
        shutil.copy(main_r_fpath, target_main_r_path)

    def append_r_file(self):
        if self._name != self._config['main_project_name']:
            backupdir = self.__get_freeline_backup_r_dir()
            main_r_path = os.path.join(backupdir, self._config['package'].replace('.', os.sep), 'R.java')

            # main_r_path existence means that resource modification exists, so that need to add R.java to classpath
            if os.path.exists(main_r_path):
                pns = [self._config['package'], self._module_info['packagename']]

                for m in self._module_info['local_module_dep']:
                    pns.append(self._all_module_info[m]['packagename'])

                for pn in pns:
                    rpath = os.path.join(backupdir, pn.replace('.', os.sep), 'R.java')
                    if os.path.exists(rpath) and rpath not in self._changed_files['src']:
                        self._changed_files['src'].append(rpath)
                        self.debug('add R.java to changed list: ' + rpath)
                    elif pn == self._module_info['packagename']:
                        fpath = self.__modify_other_modules_r(pn)
                        self.debug('modify {}'.format(fpath))
                        if fpath and os.path.exists(fpath):
                            self._changed_files['src'].append(fpath)
                            self.debug('add R.java to changed list: ' + fpath)
        else:
            if is_windows_system():
                main_r_path = os.path.join(self._finder.get_backup_dir(),
                                           self._module_info['packagename'].replace('.', os.sep), 'R.java')
                if os.path.exists(main_r_path):
                    content = android_tools.fix_unicode_parse_error(get_file_content(main_r_path), main_r_path)
                    write_file_content(main_r_path, content)

    def fill_classpaths(self):
        # classpaths:
        # 1. patch classes
        # 2. dependent modules' patch classes
        # 3. android.jar
        # 4. third party jars
        # 5. generated classes in build directory
        patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
        self._classpaths.append(patch_classes_cache_dir)
        self._classpaths.append(self._finder.get_dst_classes_dir())
        for module in self._module_info['local_module_dep']:
            finder = GradleDirectoryFinder(module, self._module_dir_map[module], self._cache_dir)
            self._classpaths.append(finder.get_patch_classes_cache_dir())

        self._classpaths.append(os.path.join(self._config['compile_sdk_directory'], 'android.jar'))
        self._classpaths.extend(self._module_info['dep_jar_path'])

        # remove existing same-name class in build directory
        srcdirs = self._config['project_source_sets'][self._name]['main_src_directory']
        for dirpath, dirnames, files in os.walk(patch_classes_cache_dir):
            for fn in files:
                if self._is_r_file_changed and self._module_info['packagename'] + '.R.' in fn:
                    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                if fn.endswith('.class') and '$' not in fn and 'R.' not in fn and 'Manifest.' not in fn:
                    cp = os.path.join(dirpath, fn)
                    java_src = cp.replace('.class', '.java').split('classes' + os.path.sep)[1]
                    existence = True
                    for src_dir in srcdirs:
                        if os.path.exists(os.path.join(src_dir, java_src)):
                            existence = True
                            break
                        # if not os.path.exists(os.path.join(src_dir, java_src)):
                        #    android_tools.delete_class(dirpath, fn.replace('.class', ''))
                    if not existence:
                        android_tools.delete_class(dirpath, fn.replace('.class', ''))

    def fill_extra_javac_args(self):
        if 'apt' in self._config and self._name in self._config['apt'] and self._config['apt'][self._name]['enabled']:
            apt_config = self._config['apt'][self._name]
            apt_args = ['-s', apt_config['aptOutput']]

            if apt_config['processor']:
                apt_args.append('-processor')
                apt_args.append(apt_config['processor'])

            if not apt_config['disableDiscovery']:
                apt_args.append('-processorpath')
                apt_args.append(apt_config['processorPath'])

            apt_args.extend(apt_config['aptArgs'])
            self._extra_javac_args.extend(apt_args)

    def run_javac_task(self):
        javacargs = [self._javac, '-encoding', 'UTF-8', '-g']
        if not self._is_retrolambda_enabled:
            javacargs.extend(['-target', '1.7', '-source', '1.7'])

        javacargs.append('-cp')
        javacargs.append(os.pathsep.join(self._classpaths))

        for fpath in self._changed_files['src']:
            javacargs.append(fpath)

        javacargs.extend(self._extra_javac_args)
        javacargs.append('-d')
        javacargs.append(self._finder.get_patch_classes_cache_dir())

        self.debug('javac exec: ' + ' '.join(javacargs))
        output, err, code = cexec(javacargs, callback=None)

        if code != 0:
            raise FreelineException('incremental javac compile failed.', '{}\n{}'.format(output, err))
        else:
            if self._is_r_file_changed:
                old_r_file = self._finder.get_dst_r_path(config=self._config)
                new_r_file = android_tools.DirectoryFinder.get_r_file_path(self._finder.get_backup_dir())
                shutil.copyfile(new_r_file, old_r_file)
                self.debug('copy {} to {}'.format(new_r_file, old_r_file))

    def run_retrolambda(self):
        if self._is_retrolambda_enabled:
            lambda_config = self._config['retrolambda'][self._name]
            target_dir = self._finder.get_patch_classes_cache_dir()
            jar_args = [Builder.get_java(self._config),
                        '-Dretrolambda.inputDir={}'.format(target_dir),
                        '-Dretrolambda.outputDir={}'.format(target_dir)]

            if lambda_config['supportIncludeFiles']:
                include_files = []
                classes = []
                for dirpath, dirnames, files in os.walk(target_dir):
                    for fn in files:
                        if fn.endswith('.class'):
                            classes.append(os.path.relpath(os.path.join(dirpath, fn), target_dir))

                src_dirs = self._config['project_source_sets'][self._name]['main_src_directory']
                for fpath in self._changed_files['src']:
                    short_path = fpath.replace('.java', '.class')
                    for src_dir in src_dirs:
                        if src_dir in short_path:
                            short_path = os.path.relpath(fpath, src_dir).replace('.java', '')
                            break

                    for clazz in classes:
                        if short_path + '.class' in clazz or short_path + '$' in clazz or 'R.class' in clazz \
                                or 'R$' in clazz or short_path + '_' in clazz:
                            include_file = os.path.join(target_dir, clazz)
                            if os.path.exists(include_file):
                                self.debug('incremental build lambda file: {}'.format(include_file))
                                include_files.append(include_file)

                include_files_param = os.pathsep.join(include_files)
                if len(include_files_param) > 3496:
                    include_files_path = os.path.join(self._cache_dir, self._name, 'retrolambda_inc.list')
                    self.__save_parms_to_file(include_files_path, include_files)
                    jar_args.append('-Dretrolambda.includedFile={}'.format(include_files_path))
                else:
                    jar_args.append('-Dretrolambda.includedFiles={}'.format(include_files_param))

            lambda_classpaths = [target_dir, lambda_config['rtJar']]
            lambda_classpaths.extend(self._classpaths)
            param = os.pathsep.join(lambda_classpaths)

            if lambda_config['supportIncludeFiles'] and len(param) > 3496:
                classpath_file = os.path.join(self._cache_dir, self._name, 'retrolambda_classpaths.path')
                self.__save_parms_to_file(classpath_file, lambda_classpaths)
                jar_args.append('-Dretrolambda.classpathFile={}'.format(classpath_file))
            else:
                jar_args.append('-Dretrolambda.classpath={}'.format(param))

            jar_args.append('-cp')
            jar_args.append(lambda_config['targetJar'])
            jar_args.append(lambda_config['mainClass'])

            self.debug('retrolambda exec: ' + ' '.join(jar_args))
            output, err, code = cexec(jar_args, callback=None)

            if code != 0:
                raise FreelineException('retrolambda compile failed.', '{}\n{}'.format(output, err))

    def __save_parms_to_file(self, path, params):
        if os.path.exists(path):
            os.remove(path)
        content = ''
        for param in params:
            content += param + '\n'
        write_file_content(path, content)
        self.debug('save retrolambda params to {}'.format(path))

    def _get_res_incremental_dst_path(self, fpath):
        if 'assets' + os.sep in fpath:
            return os.path.join(self._finder.get_base_gen_dir(), 'assets', 'debug', fpath.split('assets' + os.sep)[1])
        elif 'res' + os.sep in fpath:
            return os.path.join(self._finder.get_res_dir(), fpath.split('res' + os.sep)[1])

    def _parse_changed_list(self):
        changed_list = []
        for rfile in self._changed_files['res']:
            if rfile not in changed_list:
                changed_list.append(self._get_res_relative_path(rfile))

        for afile in self._changed_files['assets']:
            if afile not in changed_list:
                changed_list.append(self._get_res_relative_path(afile))
        return changed_list

    def _get_res_relative_path(self, res):
        if res.startswith('res') or res.startswith('AndroidManifest.xml'):
            return res

        def path_fix(path):
            return path if path.endswith(os.sep) else path + os.sep

        for respath in self._merged_res_paths:
            respath = path_fix(respath)
            if res.startswith(respath):
                index = respath.strip(os.sep).rfind(os.sep)
                if index >= 0:
                    res_dir_name = respath[index + 1:].strip(os.sep)
                    relative_path = os.path.join(res_dir_name, res.replace(respath, ''))
                    self.debug("find relative path: {}".format(relative_path))
                    return relative_path
        return None

    def __get_freeline_backup_r_dir(self):
        dirpath = os.path.join(self._cache_dir, 'freeline-backup-r')
        if not os.path.exists(dirpath):
            os.makedirs(dirpath)
        return dirpath

    def __modify_other_modules_r(self, package_name, finder=None):
        if not finder:
            finder = self._finder

        r_path = android_tools.find_r_file(finder.get_dst_r_dir(), package_name=package_name)
        if r_path and os.path.exists(r_path):
            target_dir = os.path.join(self.__get_freeline_backup_r_dir(), package_name.replace('.', os.sep))
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            target_path = os.path.join(target_dir, 'R.java')
            if not os.path.exists(target_path):
                self.debug('copy {} to {}'.format(r_path, target_path))
                shutil.copy(r_path, target_path)

                content = get_file_content(target_path)
                content = GradleIncBuildInvoker.remove_final_tag(content)
                content = GradleIncBuildInvoker.extend_main_r(content, self._config['package'])
                content = android_tools.fix_unicode_parse_error(content, target_path)
                write_file_content(target_path, content)

            return target_path

    def __find_res_in_which_module(self, res_path):
        for module in self._all_module_info.keys():
            # rdir = android_tools.get_res_dir(module)
            res_dirs = self._config['project_source_sets'][module]['main_res_directory']
            for rdir in res_dirs:
                if rdir is not None:
                    if res_path.startswith(rdir) or rdir in res_path:
                        return module
        return None

    @staticmethod
    def remove_final_tag(content):
        content = content.replace('public final class', 'public class').replace('public static final class',
                                                                                'public static class')
        return content

    @staticmethod
    def extend_main_r(content, main_package_name):
        import re
        result = re.findall(r'''public static class (.*) \{''', content)
        for tag in result:
            content = content.replace('class ' + tag + ' {',
                                      'class ' + tag + ' extends ' + main_package_name + '.R.' + tag + ' {')
        return content