1#!/usr/bin/env python3
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""It is an AIDEGen sub task : IDE operation task!
18
19Takes a project file path as input, after passing the needed check(file
20existence, IDE type, etc.), launch the project in related IDE.
21
22    Typical usage example:
23
24    ide_util_obj = IdeUtil()
25    if ide_util_obj.is_ide_installed():
26        ide_util_obj.config_ide(project_file)
27        ide_util_obj.launch_ide()
28
29        # Get the configuration folders of IntelliJ or Android Studio.
30        ide_util_obj.get_ide_config_folders()
31"""
32
33import glob
34import logging
35import os
36import platform
37import re
38import subprocess
39
40from xml.etree import ElementTree
41
42from aidegen import constant
43from aidegen import templates
44from aidegen.lib import aidegen_metrics
45from aidegen.lib import android_dev_os
46from aidegen.lib import common_util
47from aidegen.lib import config
48from aidegen.lib import errors
49from aidegen.lib import ide_common_util
50from aidegen.lib import project_config
51from aidegen.lib import project_file_gen
52from aidegen.sdk import jdk_table
53from aidegen.lib import xml_util
54
55# Add 'nohup' to prevent IDE from being terminated when console is terminated.
56_IDEA_FOLDER = '.idea'
57_IML_EXTENSION = '.iml'
58_JDK_PATH_TOKEN = '@JDKpath'
59_COMPONENT_END_TAG = '  </component>'
60_ECLIPSE_WS = '~/Documents/AIDEGen_Eclipse_workspace'
61_ALERT_CREATE_WS = ('AIDEGen will create a workspace at %s for Eclipse, '
62                    'Enter `y` to allow AIDEgen to automatically create the '
63                    'workspace for you. Otherwise, you need to select the '
64                    'workspace after Eclipse is launched.\nWould you like '
65                    'AIDEgen to automatically create the workspace for you?'
66                    '(y/n)' % constant.ECLIPSE_WS)
67_NO_LAUNCH_IDE_CMD = """
68Can not find IDE: {}, in path: {}, you can:
69    - add IDE executable to your $PATH
70or  - specify the exact IDE executable path by "aidegen -p"
71or  - specify "aidegen -n" to generate project file only
72"""
73_INFO_IMPORT_CONFIG = ('{} needs to import the application configuration for '
74                       'the new version!\nAfter the import is finished, rerun '
75                       'the command if your project did not launch. Please '
76                       'follow the showing dialog to finish the import action.'
77                       '\n\n')
78CONFIG_DIR = 'config'
79LINUX_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
80                              'prebuilts/jdk/jdk8/linux-x86')
81LINUX_JDK_TABLE_PATH = 'config/options/jdk.table.xml'
82LINUX_FILE_TYPE_PATH = 'config/options/filetypes.xml'
83LINUX_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Android/Sdk')
84MAC_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
85                            'prebuilts/jdk/jdk8/darwin-x86')
86MAC_JDK_TABLE_PATH = 'options/jdk.table.xml'
87MAC_FILE_TYPE_XML_PATH = 'options/filetypes.xml'
88MAC_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Library/Android/sdk')
89PATTERN_KEY = 'pattern'
90TYPE_KEY = 'type'
91JSON_TYPE = 'JSON'
92TEST_MAPPING_NAME = 'TEST_MAPPING'
93_TEST_MAPPING_TYPE = '<mapping pattern="TEST_MAPPING" type="JSON" />'
94_XPATH_EXTENSION_MAP = 'component/extensionMap'
95_XPATH_MAPPING = _XPATH_EXTENSION_MAP + '/mapping'
96
97
98# pylint: disable=too-many-lines
99class IdeUtil:
100    """Provide a set of IDE operations, e.g., launch and configuration.
101
102    Attributes:
103        _ide: IdeBase derived instance, the related IDE object.
104
105    For example:
106        1. Check if IDE is installed.
107        2. Config IDE, e.g. config code style, SDK path, and etc.
108        3. Launch an IDE.
109    """
110
111    def __init__(self,
112                 installed_path=None,
113                 ide='j',
114                 config_reset=False,
115                 is_mac=False):
116        logging.debug('IdeUtil with OS name: %s%s', platform.system(),
117                      '(Mac)' if is_mac else '')
118        self._ide = _get_ide(installed_path, ide, config_reset, is_mac)
119
120    def is_ide_installed(self):
121        """Checks if the IDE is already installed.
122
123        Returns:
124            True if IDE is installed already, otherwise False.
125        """
126        return self._ide.is_ide_installed()
127
128    def launch_ide(self):
129        """Launches the relative IDE by opening the passed project file."""
130        return self._ide.launch_ide()
131
132    def config_ide(self, project_abspath):
133        """To config the IDE, e.g., setup code style, init SDK, and etc.
134
135        Args:
136            project_abspath: An absolute path of the project.
137        """
138        self._ide.project_abspath = project_abspath
139        if self.is_ide_installed() and self._ide:
140            self._ide.apply_optional_config()
141
142    def get_default_path(self):
143        """Gets IDE default installed path."""
144        return self._ide.default_installed_path
145
146    def ide_name(self):
147        """Gets IDE name."""
148        return self._ide.ide_name
149
150    def get_ide_config_folders(self):
151        """Gets the config folders of IDE."""
152        return self._ide.config_folders
153
154
155class IdeBase:
156    """The most base class of IDE, provides interface and partial path init.
157
158    Class Attributes:
159        _JDK_PATH: The path of JDK in android project.
160        _IDE_JDK_TABLE_PATH: The path of JDK table which record JDK info in IDE.
161        _IDE_FILE_TYPE_PATH: The path of filetypes.xml.
162        _JDK_CONTENT: A string, the content of the JDK configuration.
163        _DEFAULT_ANDROID_SDK_PATH: A string, the path of Android SDK.
164        _CONFIG_DIR: A string of the config folder name.
165        _SYMBOLIC_VERSIONS: A string list of the symbolic link paths of the
166                            relevant IDE.
167
168    Attributes:
169        _installed_path: String for the IDE binary path.
170        _config_reset: Boolean, True for reset configuration, else not reset.
171        _bin_file_name: String for IDE executable file name.
172        _bin_paths: A list of all possible IDE executable file absolute paths.
173        _ide_name: String for IDE name.
174        _bin_folders: A list of all possible IDE installed paths.
175        config_folders: A list of all possible paths for the IntelliJ
176                        configuration folder.
177        project_abspath: The absolute path of the project.
178
179    For example:
180        1. Check if IDE is installed.
181        2. Launch IDE.
182        3. Config IDE.
183    """
184
185    _JDK_PATH = ''
186    _IDE_JDK_TABLE_PATH = ''
187    _IDE_FILE_TYPE_PATH = ''
188    _JDK_CONTENT = ''
189    _DEFAULT_ANDROID_SDK_PATH = ''
190    _CONFIG_DIR = ''
191    _SYMBOLIC_VERSIONS = []
192
193    def __init__(self, installed_path=None, config_reset=False):
194        self._installed_path = installed_path
195        self._config_reset = config_reset
196        self._ide_name = ''
197        self._bin_file_name = ''
198        self._bin_paths = []
199        self._bin_folders = []
200        self.config_folders = []
201        self.project_abspath = ''
202
203    def is_ide_installed(self):
204        """Checks if IDE is already installed.
205
206        Returns:
207            True if IDE is installed already, otherwise False.
208        """
209        return bool(self._installed_path)
210
211    def launch_ide(self):
212        """Launches IDE by opening the passed project file."""
213        ide_common_util.launch_ide(self.project_abspath, self._get_ide_cmd(),
214                                   self._ide_name)
215
216    def apply_optional_config(self):
217        """Do IDEA global config action.
218
219        Run code style config, SDK config.
220        """
221        if not self._installed_path:
222            return
223        # Skip config action if there's no config folder exists.
224        _path_list = self._get_config_root_paths()
225        if not _path_list:
226            return
227        self.config_folders = _path_list.copy()
228
229        for _config_path in _path_list:
230            jdk_file = os.path.join(_config_path, self._IDE_JDK_TABLE_PATH)
231            jdk_xml = jdk_table.JDKTableXML(jdk_file, self._JDK_CONTENT,
232                                            self._JDK_PATH,
233                                            self._DEFAULT_ANDROID_SDK_PATH)
234            if jdk_xml.config_jdk_table_xml():
235                project_file_gen.gen_enable_debugger_module(
236                    self.project_abspath, jdk_xml.android_sdk_version)
237
238            # Set the max file size in the idea.properties.
239            intellij_config_dir = os.path.join(_config_path, self._CONFIG_DIR)
240            config.IdeaProperties(intellij_config_dir).set_max_file_size()
241
242            self._add_test_mapping_file_type(_config_path)
243
244    def _add_test_mapping_file_type(self, _config_path):
245        """Adds TEST_MAPPING file type.
246
247        IntelliJ can't recognize TEST_MAPPING files as the json file. It needs
248        adding file type mapping in filetypes.xml to recognize TEST_MAPPING
249        files.
250
251        Args:
252            _config_path: the path of IDE config.
253        """
254        file_type_path = os.path.join(_config_path, self._IDE_FILE_TYPE_PATH)
255        if not os.path.isfile(file_type_path):
256            logging.warning('Filetypes.xml is not found.')
257            return
258
259        file_type_xml = xml_util.parse_xml(file_type_path)
260        if not file_type_xml:
261            logging.warning('Can\'t parse filetypes.xml.')
262            return
263
264        root = file_type_xml.getroot()
265        add_pattern = True
266        for mapping in root.findall(_XPATH_MAPPING):
267            attrib = mapping.attrib
268            if PATTERN_KEY in attrib and TYPE_KEY in attrib:
269                if attrib[PATTERN_KEY] == TEST_MAPPING_NAME:
270                    if attrib[TYPE_KEY] != JSON_TYPE:
271                        attrib[TYPE_KEY] = JSON_TYPE
272                        file_type_xml.write(file_type_path)
273                    add_pattern = False
274                    break
275        if add_pattern:
276            root.find(_XPATH_EXTENSION_MAP).append(
277                ElementTree.fromstring(_TEST_MAPPING_TYPE))
278            pretty_xml = common_util.to_pretty_xml(root)
279            common_util.file_generate(file_type_path, pretty_xml)
280
281    def _get_config_root_paths(self):
282        """Get the config root paths from derived class.
283
284        Returns:
285            A string list of IDE config paths, return multiple paths if more
286            than one path are found, return an empty list when none is found.
287        """
288        raise NotImplementedError()
289
290    @property
291    def default_installed_path(self):
292        """Gets IDE default installed path."""
293        return ' '.join(self._bin_folders)
294
295    @property
296    def ide_name(self):
297        """Gets IDE name."""
298        return self._ide_name
299
300    def _get_ide_cmd(self):
301        """Compose launch IDE command to run a new process and redirect output.
302
303        Returns:
304            A string of launch IDE command.
305        """
306        return ide_common_util.get_run_ide_cmd(self._installed_path,
307                                               self.project_abspath)
308
309    def _init_installed_path(self, installed_path):
310        """Initialize IDE installed path.
311
312        Args:
313            installed_path: the installed path to be checked.
314        """
315        if installed_path:
316            path_list = ide_common_util.get_script_from_input_path(
317                installed_path, self._bin_file_name)
318            self._installed_path = path_list[0] if path_list else None
319        else:
320            self._installed_path = self._get_script_from_system()
321        if not self._installed_path:
322            logging.error('No %s installed.', self._ide_name)
323            return
324
325        self._set_installed_path()
326
327    def _get_script_from_system(self):
328        """Get one IDE installed path from internal path.
329
330        Returns:
331            The sh full path, or None if not found.
332        """
333        sh_list = self._get_existent_scripts_in_system()
334        return sh_list[0] if sh_list else None
335
336    def _get_possible_bin_paths(self):
337        """Gets all possible IDE installed paths."""
338        return [os.path.join(f, self._bin_file_name) for f in self._bin_folders]
339
340    def _get_ide_from_environment_paths(self):
341        """Get IDE executable binary file from environment paths.
342
343        Returns:
344            A string of IDE executable binary path if found, otherwise return
345            None.
346        """
347        env_paths = os.environ['PATH'].split(':')
348        for env_path in env_paths:
349            path = ide_common_util.get_scripts_from_dir_path(
350                env_path, self._bin_file_name)
351            if path:
352                return path
353        return None
354
355    def _setup_ide(self):
356        """The callback used to run the necessary setup work of the IDE.
357
358        When ide_util.config_ide is called to set up the JDK, SDK and some
359        features, the main thread will callback the Idexxx._setup_ide
360        to provide the chance for running the necessary setup of the specific
361        IDE. Default is to do nothing.
362        """
363
364    def _get_existent_scripts_in_system(self):
365        """Gets the relevant IDE run script path from system.
366
367        First get correct IDE installed path from internal paths, if not found
368        search it from environment paths.
369
370        Returns:
371            The list of script full path, or None if no found.
372        """
373        return (ide_common_util.get_script_from_internal_path(self._bin_paths,
374                                                              self._ide_name) or
375                self._get_ide_from_environment_paths())
376
377    def _get_user_preference(self, versions):
378        """Make sure the version is valid and update preference if needed.
379
380        Args:
381            versions: A list of the IDE script path, contains the symbolic path.
382
383        Returns: An IDE script path, or None is not found.
384        """
385        if not versions:
386            return None
387        if len(versions) == 1:
388            return versions[0]
389        with config.AidegenConfig() as conf:
390            if not self._config_reset and (conf.preferred_version(self.ide_name)
391                                           in versions):
392                return conf.preferred_version(self.ide_name)
393            display_versions = self._merge_symbolic_version(versions)
394            preferred = ide_common_util.ask_preference(display_versions,
395                                                       self.ide_name)
396            if preferred:
397                conf.set_preferred_version(self._get_real_path(preferred),
398                                           self.ide_name)
399
400            return conf.preferred_version(self.ide_name)
401
402    def _set_installed_path(self):
403        """Write the user's input installed path into the config file.
404
405        If users input an existent IDE installed path, we should keep it in
406        the configuration.
407        """
408        if self._installed_path:
409            with config.AidegenConfig() as aconf:
410                aconf.set_preferred_version(self._installed_path, self.ide_name)
411
412    def _merge_symbolic_version(self, versions):
413        """Merge the duplicate version of symbolic links.
414
415        Stable and beta versions are a symbolic link to an existing version.
416        This function assemble symbolic and real to make it more clear to read.
417        Ex:
418        ['/opt/intellij-ce-stable/bin/idea.sh',
419        '/opt/intellij-ce-2019.1/bin/idea.sh'] to
420        ['/opt/intellij-ce-stable/bin/idea.sh ->
421        /opt/intellij-ce-2019.1/bin/idea.sh',
422        '/opt/intellij-ce-2019.1/bin/idea.sh']
423
424        Args:
425            versions: A list of all installed versions.
426
427        Returns:
428            A list of versions to show for user to select. It may contain
429            'symbolic_path/idea.sh -> original_path/idea.sh'.
430        """
431        display_versions = versions[:]
432        for symbolic in self._SYMBOLIC_VERSIONS:
433            if symbolic in display_versions and (os.path.isfile(symbolic)):
434                real_path = os.path.realpath(symbolic)
435                for index, path in enumerate(display_versions):
436                    if path == symbolic:
437                        display_versions[index] = ' -> '.join(
438                            [display_versions[index], real_path])
439                        break
440        return display_versions
441
442    @staticmethod
443    def _get_real_path(path):
444        """ Get real path from merged path.
445
446        Turn the path string "/opt/intellij-ce-stable/bin/idea.sh -> /opt/
447        intellij-ce-2019.2/bin/idea.sh" into
448        "/opt/intellij-ce-stable/bin/idea.sh"
449
450        Args:
451            path: A path string may be merged with symbolic path.
452        Returns:
453            The real IntelliJ installed path.
454        """
455        return path.split()[0]
456
457
458class IdeIntelliJ(IdeBase):
459    """Provide basic IntelliJ ops, e.g., launch IDEA, and config IntelliJ.
460
461    For example:
462        1. Check if IntelliJ is installed.
463        2. Launch an IntelliJ.
464        3. Config IntelliJ.
465    """
466    def __init__(self, installed_path=None, config_reset=False):
467        super().__init__(installed_path, config_reset)
468        self._ide_name = constant.IDE_INTELLIJ
469        self._ls_ce_path = ''
470        self._ls_ue_path = ''
471
472    def _get_config_root_paths(self):
473        """Get the config root paths from derived class.
474
475        Returns:
476            A string list of IDE config paths, return multiple paths if more
477            than one path are found, return an empty list when none is found.
478        """
479        raise NotImplementedError()
480
481    def _get_preferred_version(self):
482        """Get the user's preferred IntelliJ version.
483
484        Locates the IntelliJ IDEA launch script path by following rule.
485
486        1. If config file recorded user's preference version, load it.
487        2. If config file didn't record, search them form default path if there
488           are more than one version, ask user and record it.
489
490        Returns:
491            The sh full path, or None if no IntelliJ version is installed.
492        """
493        ce_paths = ide_common_util.get_intellij_version_path(self._ls_ce_path)
494        ue_paths = ide_common_util.get_intellij_version_path(self._ls_ue_path)
495        all_versions = self._get_all_versions(ce_paths, ue_paths)
496        tmp_versions = all_versions.copy()
497        for version in tmp_versions:
498            real_version = os.path.realpath(version)
499            if config.AidegenConfig.deprecated_intellij_version(real_version):
500                all_versions.remove(version)
501        return self._get_user_preference(all_versions)
502
503    def _setup_ide(self):
504        """The callback used to run the necessary setup work for the IDE.
505
506        IntelliJ has a default flow to let the user import the configuration
507        from the previous version, aidegen makes sure not to break the behavior
508        by checking in this callback implementation.
509        """
510        run_script_path = os.path.realpath(self._installed_path)
511        app_folder = self._get_application_path(run_script_path)
512        if not app_folder:
513            logging.warning('\nInvalid IDE installed path.')
514            return
515
516        show_hint = False
517        folder_path = os.path.join(os.getenv('HOME'), app_folder,
518                                   'config', 'plugins')
519        import_process = None
520        while not os.path.isdir(folder_path):
521            # Guide the user to go through the IDE flow.
522            if not show_hint:
523                print('\n{} {}'.format(common_util.COLORED_INFO('INFO:'),
524                                       _INFO_IMPORT_CONFIG.format(
525                                           self.ide_name)))
526                try:
527                    import_process = subprocess.Popen(
528                        ide_common_util.get_run_ide_cmd(run_script_path, '',
529                                                        False), shell=True)
530                except (subprocess.SubprocessError, ValueError):
531                    logging.warning('\nSubprocess call gets the invalid input.')
532                finally:
533                    show_hint = True
534        try:
535            import_process.wait(1)
536        except subprocess.TimeoutExpired:
537            import_process.terminate()
538        return
539
540    def _get_script_from_system(self):
541        """Get correct IntelliJ installed path from internal path.
542
543        Returns:
544            The sh full path, or None if no IntelliJ version is installed.
545        """
546        found = self._get_preferred_version()
547        if found:
548            logging.debug('IDE internal installed path: %s.', found)
549        return found
550
551    @staticmethod
552    def _get_all_versions(cefiles, uefiles):
553        """Get all versions of launch script files.
554
555        Args:
556            cefiles: CE version launch script paths.
557            uefiles: UE version launch script paths.
558
559        Returns:
560            A list contains all versions of launch script files.
561        """
562        all_versions = []
563        if cefiles:
564            all_versions.extend(cefiles)
565        if uefiles:
566            all_versions.extend(uefiles)
567        return all_versions
568
569    @staticmethod
570    def _get_application_path(run_script_path):
571        """Get the relevant configuration folder based on the run script path.
572
573        Args:
574            run_script_path: The string of the run script path for the IntelliJ.
575
576        Returns:
577            The string of the IntelliJ application folder name or None if the
578            run_script_path is invalid. The returned folder format is as
579            follows,
580                1. .IdeaIC2019.3
581                2. .IntelliJIdea2019.3
582        """
583        if not run_script_path or not os.path.isfile(run_script_path):
584            return None
585        index = str.find(run_script_path, 'intellij-')
586        target_path = None if index == -1 else run_script_path[index:]
587        if not target_path or '-' not in run_script_path:
588            return None
589
590        path_data = target_path.split('-')
591        if not path_data or len(path_data) < 3:
592            return None
593
594        config_folder = None
595        ide_version = path_data[2].split(os.sep)[0]
596
597        if path_data[1] == 'ce':
598            config_folder = ''.join(['.IdeaIC', ide_version])
599        elif path_data[1] == 'ue':
600            config_folder = ''.join(['.IntelliJIdea', ide_version])
601
602        return config_folder
603
604
605class IdeLinuxIntelliJ(IdeIntelliJ):
606    """Provide the IDEA behavior implementation for OS Linux.
607
608    Class Attributes:
609        _INTELLIJ_RE: Regular expression of IntelliJ installed name in GLinux.
610
611    For example:
612        1. Check if IntelliJ is installed.
613        2. Launch an IntelliJ.
614        3. Config IntelliJ.
615    """
616
617    _JDK_PATH = LINUX_JDK_PATH
618    # TODO(b/127899277): Preserve a config for jdk version option case.
619    _CONFIG_DIR = CONFIG_DIR
620    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
621    _IDE_FILE_TYPE_PATH = LINUX_FILE_TYPE_PATH
622    _JDK_CONTENT = templates.LINUX_JDK_XML
623    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
624    _SYMBOLIC_VERSIONS = ['/opt/intellij-ce-stable/bin/idea.sh',
625                          '/opt/intellij-ue-stable/bin/idea.sh',
626                          '/opt/intellij-ce-beta/bin/idea.sh',
627                          '/opt/intellij-ue-beta/bin/idea.sh']
628    _INTELLIJ_RE = re.compile(r'intellij-(ce|ue)-')
629
630    def __init__(self, installed_path=None, config_reset=False):
631        super().__init__(installed_path, config_reset)
632        self._bin_file_name = 'idea.sh'
633        self._bin_folders = ['/opt/intellij-*/bin']
634        self._ls_ce_path = os.path.join('/opt/intellij-ce-*/bin',
635                                        self._bin_file_name)
636        self._ls_ue_path = os.path.join('/opt/intellij-ue-*/bin',
637                                        self._bin_file_name)
638        self._init_installed_path(installed_path)
639
640    def _get_config_root_paths(self):
641        """To collect the global config folder paths of IDEA as a string list.
642
643        The config folder of IntelliJ IDEA is under the user's home directory,
644        .IdeaIC20xx.x and .IntelliJIdea20xx.x are folder names for different
645        versions.
646
647        Returns:
648            A string list for IDE config root paths, and return an empty list
649            when none is found.
650        """
651        if not self._installed_path:
652            return None
653
654        _config_folders = []
655        _config_folder = ''
656        if IdeLinuxIntelliJ._INTELLIJ_RE.search(self._installed_path):
657            _path_data = os.path.realpath(self._installed_path)
658            _config_folder = self._get_application_path(_path_data)
659            if not _config_folder:
660                return None
661
662            if not os.path.isdir(os.path.join(os.getenv('HOME'),
663                                              _config_folder)):
664                logging.debug("\nThe config folder: %s doesn't exist",
665                              _config_folder)
666                self._setup_ide()
667
668            _config_folders.append(
669                os.path.join(os.getenv('HOME'), _config_folder))
670        else:
671            # TODO(b/123459239): For the case that the user provides the IDEA
672            # binary path, we now collect all possible IDEA config root paths.
673            _config_folders = glob.glob(
674                os.path.join(os.getenv('HOME'), '.IdeaI?20*'))
675            _config_folders.extend(
676                glob.glob(os.path.join(os.getenv('HOME'), '.IntelliJIdea20*')))
677            logging.debug('The config path list: %s.', _config_folders)
678
679        return _config_folders
680
681
682class IdeMacIntelliJ(IdeIntelliJ):
683    """Provide the IDEA behavior implementation for OS Mac.
684
685    For example:
686        1. Check if IntelliJ is installed.
687        2. Launch an IntelliJ.
688        3. Config IntelliJ.
689    """
690
691    _JDK_PATH = MAC_JDK_PATH
692    _IDE_JDK_TABLE_PATH = MAC_JDK_TABLE_PATH
693    _IDE_FILE_TYPE_PATH = MAC_FILE_TYPE_XML_PATH
694    _JDK_CONTENT = templates.MAC_JDK_XML
695    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
696
697    def __init__(self, installed_path=None, config_reset=False):
698        super().__init__(installed_path, config_reset)
699        self._bin_file_name = 'idea'
700        self._bin_folders = ['/Applications/IntelliJ IDEA.app/Contents/MacOS']
701        self._bin_paths = self._get_possible_bin_paths()
702        self._ls_ce_path = os.path.join(
703            '/Applications/IntelliJ IDEA CE.app/Contents/MacOS',
704            self._bin_file_name)
705        self._ls_ue_path = os.path.join(
706            '/Applications/IntelliJ IDEA.app/Contents/MacOS',
707            self._bin_file_name)
708        self._init_installed_path(installed_path)
709
710    def _get_config_root_paths(self):
711        """To collect the global config folder paths of IDEA as a string list.
712
713        Returns:
714            A string list for IDE config root paths, and return an empty list
715            when none is found.
716        """
717        if not self._installed_path:
718            return None
719
720        _config_folders = []
721        if 'IntelliJ' in self._installed_path:
722            _config_folders = glob.glob(
723                os.path.join(
724                    os.getenv('HOME'), 'Library/Preferences/IdeaI?20*'))
725            _config_folders.extend(
726                glob.glob(
727                    os.path.join(
728                        os.getenv('HOME'),
729                        'Library/Preferences/IntelliJIdea20*')))
730        return _config_folders
731
732
733class IdeStudio(IdeBase):
734    """Class offers a set of Android Studio launching utilities.
735
736    For example:
737        1. Check if Android Studio is installed.
738        2. Launch an Android Studio.
739        3. Config Android Studio.
740    """
741
742    def __init__(self, installed_path=None, config_reset=False):
743        super().__init__(installed_path, config_reset)
744        self._ide_name = constant.IDE_ANDROID_STUDIO
745
746    def _get_config_root_paths(self):
747        """Get the config root paths from derived class.
748
749        Returns:
750            A string list of IDE config paths, return multiple paths if more
751            than one path are found, return an empty list when none is found.
752        """
753        raise NotImplementedError()
754
755    def _get_script_from_system(self):
756        """Get correct Studio installed path from internal path.
757
758        Returns:
759            The sh full path, or None if no Studio version is installed.
760        """
761        found = self._get_preferred_version()
762        if found:
763            logging.debug('IDE internal installed path: %s.', found)
764        return found
765
766    def _get_preferred_version(self):
767        """Get the user's preferred Studio version.
768
769        Locates the Studio launch script path by following rule.
770
771        1. If config file recorded user's preference version, load it.
772        2. If config file didn't record, search them form default path if there
773           are more than one version, ask user and record it.
774
775        Returns:
776            The sh full path, or None if no Studio version is installed.
777        """
778        versions = self._get_existent_scripts_in_system()
779        if not versions:
780            return None
781        for version in versions:
782            real_version = os.path.realpath(version)
783            if config.AidegenConfig.deprecated_studio_version(real_version):
784                versions.remove(version)
785        return self._get_user_preference(versions)
786
787    def apply_optional_config(self):
788        """Do the configuration of Android Studio.
789
790        Configures code style and SDK for Java project and do nothing for
791        others.
792        """
793        if not self.project_abspath:
794            return
795        # TODO(b/150662865): The following workaround should be replaced.
796        # Since the path of the artifact for Java is the .idea directory but
797        # native is a CMakeLists.txt file using this to workaround first.
798        if os.path.isfile(self.project_abspath):
799            return
800        if os.path.isdir(self.project_abspath):
801            IdeBase.apply_optional_config(self)
802
803
804class IdeLinuxStudio(IdeStudio):
805    """Class offers a set of Android Studio launching utilities for OS Linux.
806
807    For example:
808        1. Check if Android Studio is installed.
809        2. Launch an Android Studio.
810        3. Config Android Studio.
811    """
812
813    _JDK_PATH = LINUX_JDK_PATH
814    _CONFIG_DIR = CONFIG_DIR
815    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
816    _JDK_CONTENT = templates.LINUX_JDK_XML
817    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
818    _SYMBOLIC_VERSIONS = [
819        '/opt/android-studio-with-blaze-stable/bin/studio.sh',
820        '/opt/android-studio-stable/bin/studio.sh',
821        '/opt/android-studio-with-blaze-beta/bin/studio.sh',
822        '/opt/android-studio-beta/bin/studio.sh']
823
824    def __init__(self, installed_path=None, config_reset=False):
825        super().__init__(installed_path, config_reset)
826        self._bin_file_name = 'studio.sh'
827        self._bin_folders = ['/opt/android-studio*/bin']
828        self._bin_paths = self._get_possible_bin_paths()
829        self._init_installed_path(installed_path)
830
831    def _get_config_root_paths(self):
832        """Collect the global config folder paths as a string list.
833
834        Returns:
835            A string list for IDE config root paths, and return an empty list
836            when none is found.
837        """
838        return glob.glob(os.path.join(os.getenv('HOME'), '.AndroidStudio*'))
839
840
841class IdeMacStudio(IdeStudio):
842    """Class offers a set of Android Studio launching utilities for OS Mac.
843
844    For example:
845        1. Check if Android Studio is installed.
846        2. Launch an Android Studio.
847        3. Config Android Studio.
848    """
849
850    _JDK_PATH = MAC_JDK_PATH
851    _IDE_JDK_TABLE_PATH = MAC_JDK_TABLE_PATH
852    _JDK_CONTENT = templates.MAC_JDK_XML
853    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
854
855    def __init__(self, installed_path=None, config_reset=False):
856        super().__init__(installed_path, config_reset)
857        self._bin_file_name = 'studio'
858        self._bin_folders = ['/Applications/Android Studio.app/Contents/MacOS']
859        self._bin_paths = self._get_possible_bin_paths()
860        self._init_installed_path(installed_path)
861
862    def _get_config_root_paths(self):
863        """Collect the global config folder paths as a string list.
864
865        Returns:
866            A string list for IDE config root paths, and return an empty list
867            when none is found.
868        """
869        return glob.glob(
870            os.path.join(
871                os.getenv('HOME'), 'Library/Preferences/AndroidStudio*'))
872
873
874class IdeEclipse(IdeBase):
875    """Class offers a set of Eclipse launching utilities.
876
877    Attributes:
878        cmd: A list of the build command.
879
880    For example:
881        1. Check if Eclipse is installed.
882        2. Launch an Eclipse.
883    """
884
885    def __init__(self, installed_path=None, config_reset=False):
886        super().__init__(installed_path, config_reset)
887        self._ide_name = constant.IDE_ECLIPSE
888        self._bin_file_name = 'eclipse'
889        self.cmd = []
890
891    def _get_script_from_system(self):
892        """Get correct IDE installed path from internal path.
893
894        Remove any file with extension, the filename should be like, 'eclipse',
895        'eclipse47' and so on, check if the file is executable and filter out
896        file such as 'eclipse.ini'.
897
898        Returns:
899            The sh full path, or None if no IntelliJ version is installed.
900        """
901        for ide_path in self._bin_paths:
902            # The binary name of Eclipse could be eclipse47, eclipse49,
903            # eclipse47_testing or eclipse49_testing. So finding the matched
904            # binary by /path/to/ide/eclipse*.
905            ls_output = glob.glob(ide_path + '*', recursive=True)
906            if ls_output:
907                ls_output = sorted(ls_output)
908                match_eclipses = []
909                for path in ls_output:
910                    if os.access(path, os.X_OK):
911                        match_eclipses.append(path)
912                if match_eclipses:
913                    match_eclipses = sorted(match_eclipses)
914                    logging.debug('Result for checking %s after sort: %s.',
915                                  self._ide_name, match_eclipses[0])
916                    return match_eclipses[0]
917        return None
918
919    def _get_ide_cmd(self):
920        """Compose launch IDE command to run a new process and redirect output.
921
922        AIDEGen will create a default workspace
923        ~/Documents/AIDEGen_Eclipse_workspace for users if they agree to do
924        that. Also, we could not import the default project through the command
925        line so remove the project path argument.
926
927        Returns:
928            A string of launch IDE command.
929        """
930        if (os.path.exists(os.path.expanduser(constant.ECLIPSE_WS))
931                or str(input(_ALERT_CREATE_WS)).lower() == 'y'):
932            self.cmd.extend(['-data', constant.ECLIPSE_WS])
933        self.cmd.extend([constant.IGNORE_STD_OUT_ERR_CMD, '&'])
934        return ' '.join(self.cmd)
935
936    def apply_optional_config(self):
937        """Override to do nothing."""
938
939    def _get_config_root_paths(self):
940        """Override to do nothing."""
941
942
943class IdeLinuxEclipse(IdeEclipse):
944    """Class offers a set of Eclipse launching utilities for OS Linux.
945
946    For example:
947        1. Check if Eclipse is installed.
948        2. Launch an Eclipse.
949    """
950
951    def __init__(self, installed_path=None, config_reset=False):
952        super().__init__(installed_path, config_reset)
953        self._bin_folders = ['/opt/eclipse*', '/usr/bin/']
954        self._bin_paths = self._get_possible_bin_paths()
955        self._init_installed_path(installed_path)
956        self.cmd = [constant.NOHUP, self._installed_path.replace(' ', r'\ ')]
957
958
959class IdeMacEclipse(IdeEclipse):
960    """Class offers a set of Eclipse launching utilities for OS Mac.
961
962    For example:
963        1. Check if Eclipse is installed.
964        2. Launch an Eclipse.
965    """
966
967    def __init__(self, installed_path=None, config_reset=False):
968        super().__init__(installed_path, config_reset)
969        self._bin_file_name = 'eclipse'
970        self._bin_folders = [os.path.expanduser('~/eclipse/**')]
971        self._bin_paths = self._get_possible_bin_paths()
972        self._init_installed_path(installed_path)
973        self.cmd = [self._installed_path.replace(' ', r'\ ')]
974
975
976class IdeCLion(IdeBase):
977    """Class offers a set of CLion launching utilities.
978
979    For example:
980        1. Check if CLion is installed.
981        2. Launch an CLion.
982    """
983
984    def __init__(self, installed_path=None, config_reset=False):
985        super().__init__(installed_path, config_reset)
986        self._ide_name = constant.IDE_CLION
987
988    def apply_optional_config(self):
989        """Override to do nothing."""
990
991    def _get_config_root_paths(self):
992        """Override to do nothing."""
993
994
995class IdeLinuxCLion(IdeCLion):
996    """Class offers a set of CLion launching utilities for OS Linux.
997
998    For example:
999        1. Check if CLion is installed.
1000        2. Launch an CLion.
1001    """
1002
1003    def __init__(self, installed_path=None, config_reset=False):
1004        super().__init__(installed_path, config_reset)
1005        self._bin_file_name = 'clion.sh'
1006        # TODO(b/141288011): Handle /opt/clion-*/bin to let users choose a
1007        # preferred version of CLion in the future.
1008        self._bin_folders = ['/opt/clion-stable/bin']
1009        self._bin_paths = self._get_possible_bin_paths()
1010        self._init_installed_path(installed_path)
1011
1012
1013class IdeMacCLion(IdeCLion):
1014    """Class offers a set of Android Studio launching utilities for OS Mac.
1015
1016    For example:
1017        1. Check if Android Studio is installed.
1018        2. Launch an Android Studio.
1019    """
1020
1021    def __init__(self, installed_path=None, config_reset=False):
1022        super().__init__(installed_path, config_reset)
1023        self._bin_file_name = 'clion'
1024        self._bin_folders = ['/Applications/CLion.app/Contents/MacOS/CLion']
1025        self._bin_paths = self._get_possible_bin_paths()
1026        self._init_installed_path(installed_path)
1027
1028
1029class IdeVSCode(IdeBase):
1030    """Class offers a set of VSCode launching utilities.
1031
1032    For example:
1033        1. Check if VSCode is installed.
1034        2. Launch an VSCode.
1035    """
1036
1037    def __init__(self, installed_path=None, config_reset=False):
1038        super().__init__(installed_path, config_reset)
1039        self._ide_name = constant.IDE_VSCODE
1040
1041    def apply_optional_config(self):
1042        """Override to do nothing."""
1043
1044    def _get_config_root_paths(self):
1045        """Override to do nothing."""
1046
1047
1048class IdeLinuxVSCode(IdeVSCode):
1049    """Class offers a set of VSCode launching utilities for OS Linux."""
1050
1051    def __init__(self, installed_path=None, config_reset=False):
1052        super().__init__(installed_path, config_reset)
1053        self._bin_file_name = 'code'
1054        self._bin_folders = ['/usr/bin']
1055        self._bin_paths = self._get_possible_bin_paths()
1056        self._init_installed_path(installed_path)
1057
1058
1059class IdeMacVSCode(IdeVSCode):
1060    """Class offers a set of VSCode launching utilities for OS Mac."""
1061
1062    def __init__(self, installed_path=None, config_reset=False):
1063        super().__init__(installed_path, config_reset)
1064        self._bin_file_name = 'code'
1065        self._bin_folders = ['/usr/local/bin']
1066        self._bin_paths = self._get_possible_bin_paths()
1067        self._init_installed_path(installed_path)
1068
1069
1070def get_ide_util_instance(ide='j'):
1071    """Get an IdeUtil class instance for launching IDE.
1072
1073    Args:
1074        ide: A key character of IDE to be launched. Default ide='j' is to
1075            launch IntelliJ.
1076
1077    Returns:
1078        An IdeUtil class instance.
1079    """
1080    conf = project_config.ProjectConfig.get_instance()
1081    if not conf.is_launch_ide:
1082        return None
1083    is_mac = (android_dev_os.AndroidDevOS.MAC == android_dev_os.AndroidDevOS.
1084              get_os_type())
1085    tool = IdeUtil(conf.ide_installed_path, ide, conf.config_reset, is_mac)
1086    if not tool.is_ide_installed():
1087        ipath = conf.ide_installed_path or tool.get_default_path()
1088        err = _NO_LAUNCH_IDE_CMD.format(constant.IDE_NAME_DICT[ide], ipath)
1089        logging.error(err)
1090        stack_trace = common_util.remove_user_home_path(err)
1091        logs = '%s exists? %s' % (common_util.remove_user_home_path(ipath),
1092                                  os.path.exists(ipath))
1093        aidegen_metrics.ends_asuite_metrics(constant.IDE_LAUNCH_FAILURE,
1094                                            stack_trace,
1095                                            logs)
1096        raise errors.IDENotExistError(err)
1097    return tool
1098
1099
1100def _get_ide(installed_path=None, ide='j', config_reset=False, is_mac=False):
1101    """Get IDE to be launched according to the ide input and OS type.
1102
1103    Args:
1104        installed_path: The IDE installed path to be checked.
1105        ide: A key character of IDE to be launched. Default ide='j' is to
1106            launch IntelliJ.
1107        config_reset: A boolean, if true reset configuration data.
1108
1109    Returns:
1110        A corresponding IDE instance.
1111    """
1112    if is_mac:
1113        return _get_mac_ide(installed_path, ide, config_reset)
1114    return _get_linux_ide(installed_path, ide, config_reset)
1115
1116
1117def _get_mac_ide(installed_path=None, ide='j', config_reset=False):
1118    """Get IDE to be launched according to the ide input for OS Mac.
1119
1120    Args:
1121        installed_path: The IDE installed path to be checked.
1122        ide: A key character of IDE to be launched. Default ide='j' is to
1123            launch IntelliJ.
1124        config_reset: A boolean, if true reset configuration data.
1125
1126    Returns:
1127        A corresponding IDE instance.
1128    """
1129    if ide == 'e':
1130        return IdeMacEclipse(installed_path, config_reset)
1131    if ide == 's':
1132        return IdeMacStudio(installed_path, config_reset)
1133    if ide == 'c':
1134        return IdeMacCLion(installed_path, config_reset)
1135    if ide == 'v':
1136        return IdeMacVSCode(installed_path, config_reset)
1137    return IdeMacIntelliJ(installed_path, config_reset)
1138
1139
1140def _get_linux_ide(installed_path=None, ide='j', config_reset=False):
1141    """Get IDE to be launched according to the ide input for OS Linux.
1142
1143    Args:
1144        installed_path: The IDE installed path to be checked.
1145        ide: A key character of IDE to be launched. Default ide='j' is to
1146            launch IntelliJ.
1147        config_reset: A boolean, if true reset configuration data.
1148
1149    Returns:
1150        A corresponding IDE instance.
1151    """
1152    if ide == 'e':
1153        return IdeLinuxEclipse(installed_path, config_reset)
1154    if ide == 's':
1155        return IdeLinuxStudio(installed_path, config_reset)
1156    if ide == 'c':
1157        return IdeLinuxCLion(installed_path, config_reset)
1158    if ide == 'v':
1159        return IdeLinuxVSCode(installed_path, config_reset)
1160    return IdeLinuxIntelliJ(installed_path, config_reset)
1161