• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import '../base/common.dart';
6import '../base/context.dart';
7import '../base/file_system.dart';
8import '../base/io.dart';
9import '../base/platform.dart';
10import '../base/process_manager.dart';
11import '../base/version.dart';
12import '../globals.dart';
13import '../ios/plist_parser.dart';
14
15AndroidStudio get androidStudio => context.get<AndroidStudio>();
16
17// Android Studio layout:
18
19// Linux/Windows:
20// $HOME/.AndroidStudioX.Y/system/.home
21
22// macOS:
23// /Applications/Android Studio.app/Contents/
24// $HOME/Applications/Android Studio.app/Contents/
25
26final RegExp _dotHomeStudioVersionMatcher =
27    RegExp(r'^\.(AndroidStudio[^\d]*)([\d.]+)');
28
29String get javaPath => androidStudio?.javaPath;
30
31class AndroidStudio implements Comparable<AndroidStudio> {
32  AndroidStudio(
33    this.directory, {
34    Version version,
35    this.configured,
36    this.studioAppName = 'AndroidStudio',
37    this.presetPluginsPath,
38  }) : version = version ?? Version.unknown {
39    _init();
40  }
41
42  factory AndroidStudio.fromMacOSBundle(String bundlePath) {
43    String studioPath = fs.path.join(bundlePath, 'Contents');
44    String plistFile = fs.path.join(studioPath, 'Info.plist');
45    Map<String, dynamic> plistValues = PlistParser.instance.parseFile(plistFile);
46    // As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio.
47    // Check if we've found a JetBrainsToolbox wrapper and deal with it properly.
48    final String jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'];
49    if (jetBrainsToolboxAppBundlePath != null) {
50      studioPath = fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
51      plistFile = fs.path.join(studioPath, 'Info.plist');
52      plistValues = PlistParser.instance.parseFile(plistFile);
53    }
54
55    final String versionString = plistValues[PlistParser.kCFBundleShortVersionStringKey];
56
57    Version version;
58    if (versionString != null)
59      version = Version.parse(versionString);
60
61    String pathsSelectorValue;
62    final Map<String, dynamic> jvmOptions = plistValues['JVMOptions'];
63    if (jvmOptions != null) {
64      final Map<String, dynamic> jvmProperties = jvmOptions['Properties'];
65      if (jvmProperties != null) {
66        pathsSelectorValue = jvmProperties['idea.paths.selector'];
67      }
68    }
69    final String presetPluginsPath = pathsSelectorValue == null
70        ? null
71        : fs.path.join(homeDirPath, 'Library', 'Application Support', '$pathsSelectorValue');
72    return AndroidStudio(studioPath, version: version, presetPluginsPath: presetPluginsPath);
73  }
74
75  factory AndroidStudio.fromHomeDot(Directory homeDotDir) {
76    final Match versionMatch =
77        _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename);
78    if (versionMatch?.groupCount != 2) {
79      return null;
80    }
81    final Version version = Version.parse(versionMatch[2]);
82    final String studioAppName = versionMatch[1];
83    if (studioAppName == null || version == null) {
84      return null;
85    }
86    String installPath;
87    try {
88      installPath = fs
89          .file(fs.path.join(homeDotDir.path, 'system', '.home'))
90          .readAsStringSync();
91    } catch (e) {
92      // ignored, installPath will be null, which is handled below
93    }
94    if (installPath != null && fs.isDirectorySync(installPath)) {
95      return AndroidStudio(
96          installPath,
97          version: version,
98          studioAppName: studioAppName,
99      );
100    }
101    return null;
102  }
103
104  final String directory;
105  final String studioAppName;
106  final Version version;
107  final String configured;
108  final String presetPluginsPath;
109
110  String _javaPath;
111  bool _isValid = false;
112  final List<String> _validationMessages = <String>[];
113
114  String get javaPath => _javaPath;
115
116  bool get isValid => _isValid;
117
118  String get pluginsPath {
119    if (presetPluginsPath != null) {
120      return presetPluginsPath;
121    }
122    final int major = version?.major;
123    final int minor = version?.minor;
124    if (platform.isMacOS) {
125      return fs.path.join(
126          homeDirPath,
127          'Library',
128          'Application Support',
129          'AndroidStudio$major.$minor');
130    } else {
131      return fs.path.join(homeDirPath,
132          '.$studioAppName$major.$minor',
133          'config',
134          'plugins');
135    }
136  }
137
138  List<String> get validationMessages => _validationMessages;
139
140  @override
141  int compareTo(AndroidStudio other) {
142    final int result = version.compareTo(other.version);
143    if (result == 0)
144      return directory.compareTo(other.directory);
145    return result;
146  }
147
148  /// Locates the newest, valid version of Android Studio.
149  static AndroidStudio latestValid() {
150    final String configuredStudio = config.getValue('android-studio-dir');
151    if (configuredStudio != null) {
152      String configuredStudioPath = configuredStudio;
153      if (platform.isMacOS && !configuredStudioPath.endsWith('Contents'))
154        configuredStudioPath = fs.path.join(configuredStudioPath, 'Contents');
155      return AndroidStudio(configuredStudioPath,
156          configured: configuredStudio);
157    }
158
159    // Find all available Studio installations.
160    final List<AndroidStudio> studios = allInstalled();
161    if (studios.isEmpty) {
162      return null;
163    }
164    studios.sort();
165    return studios.lastWhere((AndroidStudio s) => s.isValid,
166        orElse: () => null);
167  }
168
169  static List<AndroidStudio> allInstalled() =>
170      platform.isMacOS ? _allMacOS() : _allLinuxOrWindows();
171
172  static List<AndroidStudio> _allMacOS() {
173    final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[];
174
175    void _checkForStudio(String path) {
176      if (!fs.isDirectorySync(path))
177        return;
178      try {
179        final Iterable<Directory> directories = fs
180            .directory(path)
181            .listSync(followLinks: false)
182            .whereType<Directory>();
183        for (Directory directory in directories) {
184          final String name = directory.basename;
185          // An exact match, or something like 'Android Studio 3.0 Preview.app'.
186          if (name.startsWith('Android Studio') && name.endsWith('.app')) {
187            candidatePaths.add(directory);
188          } else if (!directory.path.endsWith('.app')) {
189            _checkForStudio(directory.path);
190          }
191        }
192      } catch (e) {
193        printTrace('Exception while looking for Android Studio: $e');
194      }
195    }
196
197    _checkForStudio('/Applications');
198    _checkForStudio(fs.path.join(homeDirPath, 'Applications'));
199
200    final String configuredStudioDir = config.getValue('android-studio-dir');
201    if (configuredStudioDir != null) {
202      FileSystemEntity configuredStudio = fs.file(configuredStudioDir);
203      if (configuredStudio.basename == 'Contents') {
204        configuredStudio = configuredStudio.parent;
205      }
206      if (!candidatePaths
207          .any((FileSystemEntity e) => e.path == configuredStudio.path)) {
208        candidatePaths.add(configuredStudio);
209      }
210    }
211
212    return candidatePaths
213        .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path))
214        .where((AndroidStudio s) => s != null)
215        .toList();
216  }
217
218  static List<AndroidStudio> _allLinuxOrWindows() {
219    final List<AndroidStudio> studios = <AndroidStudio>[];
220
221    bool _hasStudioAt(String path, { Version newerThan }) {
222      return studios.any((AndroidStudio studio) {
223        if (studio.directory != path)
224          return false;
225        if (newerThan != null) {
226          return studio.version.compareTo(newerThan) >= 0;
227        }
228        return true;
229      });
230    }
231
232    // Read all $HOME/.AndroidStudio*/system/.home files. There may be several
233    // pointing to the same installation, so we grab only the latest one.
234    if (fs.directory(homeDirPath).existsSync()) {
235      for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) {
236        if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
237          final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);
238          if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
239            studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
240            studios.add(studio);
241          }
242        }
243      }
244    }
245
246    final String configuredStudioDir = config.getValue('android-studio-dir');
247    if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) {
248      studios.add(AndroidStudio(configuredStudioDir,
249          configured: configuredStudioDir));
250    }
251
252    if (platform.isLinux) {
253      void _checkWellKnownPath(String path) {
254        if (fs.isDirectorySync(path) && !_hasStudioAt(path)) {
255          studios.add(AndroidStudio(path));
256        }
257      }
258
259      // Add /opt/android-studio and $HOME/android-studio, if they exist.
260      _checkWellKnownPath('/opt/android-studio');
261      _checkWellKnownPath('$homeDirPath/android-studio');
262    }
263    return studios;
264  }
265
266  static String extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) {
267    if (plistValue == null || keyMatcher == null) {
268      return null;
269    }
270    return keyMatcher?.stringMatch(plistValue)?.split('=')?.last?.trim()?.replaceAll('"', '');
271  }
272
273  void _init() {
274    _isValid = false;
275    _validationMessages.clear();
276
277    if (configured != null) {
278      _validationMessages.add('android-studio-dir = $configured');
279    }
280
281    if (!fs.isDirectorySync(directory)) {
282      _validationMessages.add('Android Studio not found at $directory');
283      return;
284    }
285
286    final String javaPath = platform.isMacOS ?
287        fs.path.join(directory, 'jre', 'jdk', 'Contents', 'Home') :
288        fs.path.join(directory, 'jre');
289    final String javaExecutable = fs.path.join(javaPath, 'bin', 'java');
290    if (!processManager.canRun(javaExecutable)) {
291      _validationMessages.add('Unable to find bundled Java version.');
292    } else {
293      final ProcessResult result = processManager.runSync(<String>[javaExecutable, '-version']);
294      if (result.exitCode == 0) {
295        final List<String> versionLines = result.stderr.split('\n');
296        final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
297        _validationMessages.add('Java version $javaVersion');
298        _javaPath = javaPath;
299        _isValid = true;
300      } else {
301        _validationMessages.add('Unable to determine bundled Java version.');
302      }
303    }
304  }
305
306  @override
307  String toString() => 'Android Studio ($version)';
308}
309