1#!/usr/bin/env python3
2#
3# Copyright 2020 - 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"""Config Android SDK information.
18
19In order to create the configuration of Android SDK in IntelliJ automatically,
20parses the Android SDK information from the Android SDK path.
21
22Usage example:
23    android_sdk = AndroidSDK()
24    android_sdk.path_analysis(default_sdk_path)
25    api_level = android_sdk.max_api_level
26    android_sdk_path = android_sdk.android_sdk_path
27    platform_mapping = android_sdk.platform_mapping
28"""
29
30from __future__ import absolute_import
31
32import glob
33import os
34import re
35
36from aidegen.lib import common_util
37
38
39class AndroidSDK:
40    """Configures API level from the Android SDK path.
41
42    Attributes:
43        _android_sdk_path: The path to the Android SDK, None if the Android SDK
44                           doesn't exist.
45        _max_api_level: An integer, the max API level in the platforms folder.
46        _max_code_name: A string, the code name of the max API level.
47        _platform_mapping: A dictionary of Android platform versions mapping to
48                           the API level and the Android version code name.
49                           e.g.
50                           {
51                             'android-29': {'api_level': 29, 'code_name': '29'},
52                             'android-Q': {'api_level': 29, 'code_name': 'Q'}
53                           }
54    """
55
56    _API_LEVEL = 'api_level'
57    _CODE_NAME = 'code_name'
58    _RE_API_LEVEL = re.compile(r'AndroidVersion.ApiLevel=(?P<api_level>[\d]+)')
59    _RE_CODE_NAME = re.compile(r'AndroidVersion.CodeName=(?P<code_name>[A-Z])')
60    _GLOB_PROPERTIES_FILE = os.path.join('platforms', 'android-*',
61                                         'source.properties')
62    _INPUT_QUERY_TIMES = 3
63    _ENTER_ANDROID_SDK_PATH = ('\nThe Android SDK folder:{} doesn\'t exist. '
64                               'The debug function "Attach debugger to Android '
65                               'process" is disabled without Android SDK in '
66                               'IntelliJ or Android Studio. Please set it up '
67                               'to enable the function. \nPlease enter the '
68                               'absolute path to Android SDK:')
69    _WARNING_NO_ANDROID_SDK = ('Please install the Android SDK, otherwise the '
70                               'debug function "Attach debugger to Android '
71                               'process" cannot be enabled in IntelliJ or '
72                               'Android Studio.')
73
74    def __init__(self):
75        """Initializes AndroidSDK."""
76        self._max_api_level = 0
77        self._max_code_name = None
78        self._platform_mapping = {}
79        self._android_sdk_path = None
80
81    @property
82    def max_api_level(self):
83        """Gets the max API level."""
84        return self._max_api_level
85
86    @property
87    def max_code_name(self):
88        """Gets the max code name."""
89        return self._max_code_name
90
91    @property
92    def platform_mapping(self):
93        """Gets the Android platform mapping."""
94        return self._platform_mapping
95
96    @property
97    def android_sdk_path(self):
98        """Gets the Android SDK path."""
99        return self._android_sdk_path
100
101    def _parse_max_api_level(self):
102        """Parses the max API level from self._platform_mapping.
103
104        Returns:
105            An integer of API level and 0 means no Android platform exists.
106        """
107        return max(
108            [v[self._API_LEVEL] for v in self._platform_mapping.values()],
109            default=0)
110
111    def _parse_max_code_name(self):
112        """Parses the max code name from self._platform_mapping.
113
114        Returns:
115            A string of code name.
116        """
117        code_name = ''
118        for data in self._platform_mapping.values():
119            if (data[self._API_LEVEL] == self._max_api_level
120                    and data[self._CODE_NAME] > code_name):
121                code_name = data[self._CODE_NAME]
122        return code_name
123
124    def _parse_api_info(self, properties_file):
125        """Parses the API information from the source.properties file.
126
127        For the preview platform like android-Q, the source.properties file
128        contains two properties named AndroidVersion.ApiLevel, API level of
129        the platform, and AndroidVersion.CodeName such as Q, the code name of
130        the platform.
131        However, the formal platform like android-29, there is no property
132        AndroidVersion.CodeName.
133
134        Args:
135            properties_file: A path of the source.properties file.
136
137        Returns:
138            A tuple contains the API level and Code name of the
139            source.properties file.
140            API level: An integer of the platform, e.g. 29.
141            Code name: A string, e.g. 29 or Q.
142        """
143        api_level = 0
144        properties = common_util.read_file_content(properties_file)
145        match_api_level = self._RE_API_LEVEL.search(properties)
146        if match_api_level:
147            api_level = match_api_level.group(self._API_LEVEL)
148        match_code_name = self._RE_CODE_NAME.search(properties)
149        if match_code_name:
150            code_name = match_code_name.group(self._CODE_NAME)
151        else:
152            code_name = api_level
153        return api_level, code_name
154
155    def _gen_platform_mapping(self, path):
156        """Generates the Android platforms mapping.
157
158        Args:
159            path: A string, the absolute path of Android SDK.
160
161        Returns:
162            True when successful generates platform mapping, otherwise False.
163        """
164        prop_files = glob.glob(os.path.join(path, self._GLOB_PROPERTIES_FILE))
165        for prop_file in prop_files:
166            api_level, code_name = self._parse_api_info(prop_file)
167            if not api_level:
168                continue
169            platform = os.path.basename(os.path.dirname(prop_file))
170            self._platform_mapping[platform] = {
171                self._API_LEVEL: int(api_level),
172                self._CODE_NAME: code_name
173            }
174        return bool(self._platform_mapping)
175
176    def is_android_sdk_path(self, path):
177        """Checks if the Android SDK path is correct.
178
179        Confirm the Android SDK path is correct by checking if it has
180        platform versions.
181
182        Args:
183            path: A string, the path of Android SDK user input.
184
185        Returns:
186            True when get a platform version, otherwise False.
187        """
188        if self._gen_platform_mapping(path):
189            self._android_sdk_path = path
190            self._max_api_level = self._parse_max_api_level()
191            self._max_code_name = self._parse_max_code_name()
192            return True
193        return False
194
195    def path_analysis(self, sdk_path):
196        """Analyses the Android SDK path.
197
198        Confirm the path is a Android SDK folder. If it's not correct, ask user
199        to enter a new one. Skip asking when enter nothing.
200
201        Args:
202            sdk_path: A string, the path of Android SDK.
203
204        Returns:
205            True when get an Android SDK path, otherwise False.
206        """
207        for _ in range(self._INPUT_QUERY_TIMES):
208            if self.is_android_sdk_path(sdk_path):
209                return True
210            sdk_path = input(common_util.COLORED_FAIL(
211                self._ENTER_ANDROID_SDK_PATH.format(sdk_path)))
212            if not sdk_path:
213                break
214        print('\n{} {}\n'.format(common_util.COLORED_INFO('Warning:'),
215                                 self._WARNING_NO_ANDROID_SDK))
216        return False
217