1// Copyright 2014 The Flutter 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/file_system.dart';
7import '../base/io.dart';
8import '../base/process.dart';
9import '../base/utils.dart';
10import '../base/version.dart';
11import '../convert.dart';
12import '../globals.dart' as globals;
13import '../ios/plist_parser.dart';
14import 'android_studio_validator.dart';
15
16// Android Studio layout:
17
18// Linux/Windows:
19// $HOME/.AndroidStudioX.Y/system/.home
20// $HOME/.cache/Google/AndroidStudioX.Y/.home
21
22// macOS:
23// /Applications/Android Studio.app/Contents/
24// $HOME/Applications/Android Studio.app/Contents/
25
26// Match Android Studio >= 4.1 base folder (AndroidStudio*.*)
27// and < 4.1 (.AndroidStudio*.*)
28final RegExp _dotHomeStudioVersionMatcher = RegExp(r'^\.?(AndroidStudio[^\d]*)([\d.]+)');
29
30class AndroidStudio {
31 /// A [version] value of null represents an unknown version.
32 AndroidStudio(
33 this.directory, {
34 this.version,
35 this.configuredPath,
36 this.studioAppName = 'AndroidStudio',
37 this.presetPluginsPath,
38 }) {
39 _initAndValidate();
40 }
41
42 static AndroidStudio? fromMacOSBundle(String bundlePath, {String? configuredPath}) {
43 final String studioPath = globals.fs.path.join(bundlePath, 'Contents');
44 final String plistFile = globals.fs.path.join(studioPath, 'Info.plist');
45 final Map<String, dynamic> plistValues = globals.plistParser.parseFile(plistFile);
46 // If we've found a JetBrainsToolbox wrapper, ignore it.
47 if (plistValues.containsKey('JetBrainsToolboxApp')) {
48 return null;
49 }
50
51 final String? versionString =
52 plistValues[PlistParser.kCFBundleShortVersionStringKey] as String?;
53
54 Version? version;
55 if (versionString != null) {
56 version = _parseVersion(versionString);
57 }
58
59 String? pathsSelectorValue;
60 final Map<String, dynamic>? jvmOptions = castStringKeyedMap(plistValues['JVMOptions']);
61 if (jvmOptions != null) {
62 final Map<String, dynamic>? jvmProperties = castStringKeyedMap(jvmOptions['Properties']);
63 if (jvmProperties != null) {
64 pathsSelectorValue = jvmProperties['idea.paths.selector'] as String;
65 }
66 }
67
68 final int? major = version?.major;
69 final int? minor = version?.minor;
70 String? presetPluginsPath;
71 final String? homeDirPath = globals.fsUtils.homeDirPath;
72 if (homeDirPath != null && pathsSelectorValue != null) {
73 if (major != null && major >= 4 && minor != null && minor >= 1) {
74 presetPluginsPath = globals.fs.path.join(
75 homeDirPath,
76 'Library',
77 'Application Support',
78 'Google',
79 pathsSelectorValue,
80 );
81 } else {
82 presetPluginsPath = globals.fs.path.join(
83 homeDirPath,
84 'Library',
85 'Application Support',
86 pathsSelectorValue,
87 );
88 }
89 }
90 return AndroidStudio(
91 studioPath,
92 version: version,
93 presetPluginsPath: presetPluginsPath,
94 configuredPath: configuredPath,
95 );
96 }
97
98 static AndroidStudio? fromHomeDot(Directory homeDotDir) {
99 final Match? versionMatch = _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename);
100 if (versionMatch?.groupCount != 2) {
101 return null;
102 }
103 final Version? version = Version.parse(versionMatch![2]);
104 final String? studioAppName = versionMatch[1];
105 if (studioAppName == null || version == null) {
106 return null;
107 }
108
109 final int major = version.major;
110 final int minor = version.minor;
111
112 // The install path is written in a .home text file,
113 // it location is in /.home for Android Studio >= 4.1
114 // and /system/.home for Android Studio < 4.1
115 String dotHomeFilePath;
116
117 if (major >= 4 && minor >= 1) {
118 dotHomeFilePath = globals.fs.path.join(homeDotDir.path, '.home');
119 } else {
120 dotHomeFilePath = globals.fs.path.join(homeDotDir.path, 'system', '.home');
121 }
122
123 String? installPath;
124
125 try {
126 installPath = globals.fs.file(dotHomeFilePath).readAsStringSync();
127 } on Exception {
128 // ignored, installPath will be null, which is handled below
129 }
130
131 if (installPath != null && globals.fs.isDirectorySync(installPath)) {
132 return AndroidStudio(installPath, version: version, studioAppName: studioAppName);
133 }
134 return null;
135 }
136
137 final String directory;
138 final String studioAppName;
139
140 /// The version of Android Studio.
141 ///
142 /// A null value represents an unknown version.
143 final Version? version;
144
145 final String? configuredPath;
146 final String? presetPluginsPath;
147
148 String? _javaPath;
149 bool _isValid = false;
150 final List<String> _validationMessages = <String>[];
151
152 /// The path of the JDK bundled with Android Studio.
153 ///
154 /// This will be null if the bundled JDK could not be found or run.
155 ///
156 /// If you looking to invoke the java binary or add it to the system
157 /// environment variables, consider using the [Java] class instead.
158 String? get javaPath => _javaPath;
159
160 bool get isValid => _isValid;
161
162 String? get pluginsPath {
163 if (presetPluginsPath != null) {
164 return presetPluginsPath!;
165 }
166
167 // TODO(andrewkolos): This is a bug. We shouldn't treat an unknown
168 // version as equivalent to 0.0.
169 // See https://github.com/flutter/flutter/issues/121468.
170 final int major = version?.major ?? 0;
171 final int minor = version?.minor ?? 0;
172 final String? homeDirPath = globals.fsUtils.homeDirPath;
173 if (homeDirPath == null) {
174 return null;
175 }
176 if (globals.platform.isMacOS) {
177 /// plugin path of Android Studio has been changed after version 4.1.
178 if (major >= 4 && minor >= 1) {
179 return globals.fs.path.join(
180 homeDirPath,
181 'Library',
182 'Application Support',
183 'Google',
184 'AndroidStudio$major.$minor',
185 );
186 } else {
187 return globals.fs.path.join(
188 homeDirPath,
189 'Library',
190 'Application Support',
191 'AndroidStudio$major.$minor',
192 );
193 }
194 } else {
195 // JetBrains Toolbox write plugins here
196 final String toolboxPluginsPath = '$directory.plugins';
197
198 if (globals.fs.directory(toolboxPluginsPath).existsSync()) {
199 return toolboxPluginsPath;
200 }
201
202 if (major >= 4 && minor >= 1 && globals.platform.isLinux) {
203 return globals.fs.path.join(
204 homeDirPath,
205 '.local',
206 'share',
207 'Google',
208 '$studioAppName$major.$minor',
209 );
210 }
211
212 return globals.fs.path.join(homeDirPath, '.$studioAppName$major.$minor', 'config', 'plugins');
213 }
214 }
215
216 List<String> get validationMessages => _validationMessages;
217
218 /// Locates the newest, valid version of Android Studio.
219 ///
220 /// In the case that `--android-studio-dir` is configured, the version of
221 /// Android Studio found at that location is always returned, even if it is
222 /// invalid.
223 static AndroidStudio? latestValid() {
224 final Directory? configuredStudioDir = _configuredDir();
225
226 // Find all available Studio installations.
227 final List<AndroidStudio> studios = allInstalled();
228 if (studios.isEmpty) {
229 return null;
230 }
231
232 final AndroidStudio? manuallyConfigured =
233 studios
234 .where(
235 (AndroidStudio studio) =>
236 studio.configuredPath != null &&
237 configuredStudioDir != null &&
238 _pathsAreEqual(studio.configuredPath!, configuredStudioDir.path),
239 )
240 .firstOrNull;
241
242 if (manuallyConfigured != null) {
243 return manuallyConfigured;
244 }
245
246 AndroidStudio? newest;
247 for (final AndroidStudio studio in studios.where((AndroidStudio s) => s.isValid)) {
248 if (newest == null) {
249 newest = studio;
250 continue;
251 }
252
253 // We prefer installs with known versions.
254 if (studio.version != null && newest.version == null) {
255 newest = studio;
256 } else if (studio.version != null &&
257 newest.version != null &&
258 studio.version! > newest.version!) {
259 newest = studio;
260 } else if (studio.version == null &&
261 newest.version == null &&
262 studio.directory.compareTo(newest.directory) > 0) {
263 newest = studio;
264 }
265 }
266
267 return newest;
268 }
269
270 static List<AndroidStudio> allInstalled() =>
271 globals.platform.isMacOS ? _allMacOS() : _allLinuxOrWindows();
272
273 static List<AndroidStudio> _allMacOS() {
274 final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[];
275
276 void checkForStudio(String path) {
277 if (!globals.fs.isDirectorySync(path)) {
278 return;
279 }
280 try {
281 final Iterable<Directory> directories =
282 globals.fs.directory(path).listSync(followLinks: false).whereType<Directory>();
283 for (final Directory directory in directories) {
284 final String name = directory.basename;
285 // An exact match, or something like 'Android Studio 3.0 Preview.app'.
286 if (name.startsWith('Android Studio') && name.endsWith('.app')) {
287 candidatePaths.add(directory);
288 } else if (!directory.path.endsWith('.app')) {
289 checkForStudio(directory.path);
290 }
291 }
292 } on Exception catch (e) {
293 globals.printTrace('Exception while looking for Android Studio: $e');
294 }
295 }
296
297 checkForStudio('/Applications');
298 final String? homeDirPath = globals.fsUtils.homeDirPath;
299 if (homeDirPath != null) {
300 checkForStudio(globals.fs.path.join(homeDirPath, 'Applications'));
301 }
302
303 Directory? configuredStudioDir = _configuredDir();
304 if (configuredStudioDir != null) {
305 if (configuredStudioDir.basename == 'Contents') {
306 configuredStudioDir = configuredStudioDir.parent;
307 }
308 if (!candidatePaths.any(
309 (FileSystemEntity e) => _pathsAreEqual(e.path, configuredStudioDir!.path),
310 )) {
311 candidatePaths.add(configuredStudioDir);
312 }
313 }
314
315 // Query Spotlight for unexpected installation locations.
316 String spotlightQueryResult = '';
317 try {
318 final ProcessResult spotlightResult = globals.processManager.runSync(<String>[
319 'mdfind',
320 // com.google.android.studio, com.google.android.studio-EAP
321 'kMDItemCFBundleIdentifier="com.google.android.studio*"',
322 ]);
323 spotlightQueryResult = spotlightResult.stdout as String;
324 } on ProcessException {
325 // The Spotlight query is a nice-to-have, continue checking known installation locations.
326 }
327 for (final String studioPath in LineSplitter.split(spotlightQueryResult)) {
328 final Directory appBundle = globals.fs.directory(studioPath);
329 if (!candidatePaths.any((FileSystemEntity e) => e.path == studioPath)) {
330 candidatePaths.add(appBundle);
331 }
332 }
333
334 return candidatePaths
335 .map<AndroidStudio?>((FileSystemEntity e) {
336 if (configuredStudioDir == null) {
337 return AndroidStudio.fromMacOSBundle(e.path);
338 }
339
340 return AndroidStudio.fromMacOSBundle(
341 e.path,
342 configuredPath:
343 _pathsAreEqual(configuredStudioDir.path, e.path) ? configuredStudioDir.path : null,
344 );
345 })
346 .whereType<AndroidStudio>()
347 .toList();
348 }
349
350 static List<AndroidStudio> _allLinuxOrWindows() {
351 final List<AndroidStudio> studios = <AndroidStudio>[];
352
353 bool alreadyFoundStudioAt(String path, {Version? newerThan}) {
354 return studios.any((AndroidStudio studio) {
355 if (studio.directory != path) {
356 return false;
357 }
358 if (newerThan != null) {
359 if (studio.version == null) {
360 return false;
361 }
362 return studio.version!.compareTo(newerThan) >= 0;
363 }
364 return true;
365 });
366 }
367
368 // Read all $HOME/.AndroidStudio*/system/.home
369 // or $HOME/.cache/Google/AndroidStudio*/.home files.
370 // There may be several pointing to the same installation,
371 // so we grab only the latest one.
372 final String? homeDirPath = globals.fsUtils.homeDirPath;
373
374 if (homeDirPath != null && globals.fs.directory(homeDirPath).existsSync()) {
375 // >=4.1 has new install location at $HOME/.cache/Google
376 final String cacheDirPath = globals.fs.path.join(homeDirPath, '.cache', 'Google');
377 final List<Directory> directoriesToSearch = <Directory>[
378 globals.fs.directory(homeDirPath),
379 if (globals.fs.isDirectorySync(cacheDirPath)) globals.fs.directory(cacheDirPath),
380 ];
381
382 final List<Directory> entities = <Directory>[];
383
384 for (final Directory baseDir in directoriesToSearch) {
385 final Iterable<Directory> directories =
386 baseDir.listSync(followLinks: false).whereType<Directory>();
387 entities.addAll(
388 directories.where(
389 (Directory directory) => _dotHomeStudioVersionMatcher.hasMatch(directory.basename),
390 ),
391 );
392 }
393
394 for (final Directory entity in entities) {
395 final AndroidStudio? studio = AndroidStudio.fromHomeDot(entity);
396 if (studio != null && !alreadyFoundStudioAt(studio.directory, newerThan: studio.version)) {
397 studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
398 studios.add(studio);
399 }
400 }
401 }
402
403 // Discover Android Studio > 4.1
404 if (globals.platform.isWindows && globals.platform.environment.containsKey('LOCALAPPDATA')) {
405 final Directory cacheDir = globals.fs.directory(
406 globals.fs.path.join(globals.platform.environment['LOCALAPPDATA']!, 'Google'),
407 );
408 if (!cacheDir.existsSync()) {
409 return studios;
410 }
411 for (final Directory dir in cacheDir.listSync().whereType<Directory>()) {
412 final String name = globals.fs.path.basename(dir.path);
413 AndroidStudioValidator.idToTitle.forEach((String id, String title) {
414 if (name.startsWith(id)) {
415 final String version = name.substring(id.length);
416 String? installPath;
417
418 try {
419 installPath =
420 globals.fs.file(globals.fs.path.join(dir.path, '.home')).readAsStringSync();
421 } on FileSystemException {
422 // ignored
423 }
424 if (installPath != null && globals.fs.isDirectorySync(installPath)) {
425 final AndroidStudio studio = AndroidStudio(
426 installPath,
427 version: Version.parse(version),
428 studioAppName: title,
429 );
430 if (!alreadyFoundStudioAt(studio.directory, newerThan: studio.version)) {
431 studios.removeWhere(
432 (AndroidStudio other) => _pathsAreEqual(other.directory, studio.directory),
433 );
434 studios.add(studio);
435 }
436 }
437 }
438 });
439 }
440 }
441
442 final String? configuredStudioDir = globals.config.getValue('android-studio-dir') as String?;
443 if (configuredStudioDir != null) {
444 final AndroidStudio? matchingAlreadyFoundInstall =
445 studios
446 .where((AndroidStudio other) => _pathsAreEqual(configuredStudioDir, other.directory))
447 .firstOrNull;
448 if (matchingAlreadyFoundInstall != null) {
449 studios.remove(matchingAlreadyFoundInstall);
450 studios.add(
451 AndroidStudio(
452 configuredStudioDir,
453 configuredPath: configuredStudioDir,
454 version: matchingAlreadyFoundInstall.version,
455 ),
456 );
457 } else {
458 studios.add(AndroidStudio(configuredStudioDir, configuredPath: configuredStudioDir));
459 }
460 }
461
462 if (globals.platform.isLinux) {
463 void checkWellKnownPath(String path) {
464 if (globals.fs.isDirectorySync(path) && !alreadyFoundStudioAt(path)) {
465 studios.add(AndroidStudio(path));
466 }
467 }
468
469 // Add /opt/android-studio and $HOME/android-studio, if they exist.
470 checkWellKnownPath('/opt/android-studio');
471 checkWellKnownPath('${globals.fsUtils.homeDirPath}/android-studio');
472 }
473 return studios;
474 }
475
476 /// Gets the Android Studio install directory set by the user, if it is configured.
477 ///
478 /// The returned [Directory], if not null, is guaranteed to have existed during
479 /// this function's execution.
480 static Directory? _configuredDir() {
481 final String? configuredPath = globals.config.getValue('android-studio-dir') as String?;
482 if (configuredPath == null) {
483 return null;
484 }
485 final Directory result = globals.fs.directory(configuredPath);
486
487 bool? configuredStudioPathExists;
488 String? exceptionMessage;
489 try {
490 configuredStudioPathExists = result.existsSync();
491 } on FileSystemException catch (e) {
492 exceptionMessage = e.toString();
493 }
494
495 if (configuredStudioPathExists == false || exceptionMessage != null) {
496 throwToolExit('''
497Could not find the Android Studio installation at the manually configured path "$configuredPath".
498${exceptionMessage == null ? '' : 'Encountered exception: $exceptionMessage\n\n'}
499Please verify that the path is correct and update it by running this command: flutter config --android-studio-dir '<path>'
500To have flutter search for Android Studio installations automatically, remove
501the configured path by running this command: flutter config --android-studio-dir
502''');
503 }
504
505 return result;
506 }
507
508 static String? extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) {
509 return keyMatcher.stringMatch(plistValue)?.split('=').last.trim().replaceAll('"', '');
510 }
511
512 void _initAndValidate() {
513 _isValid = false;
514 _validationMessages.clear();
515
516 if (configuredPath != null) {
517 _validationMessages.add('android-studio-dir = $configuredPath');
518 }
519
520 if (!globals.fs.isDirectorySync(directory)) {
521 _validationMessages.add('Android Studio not found at $directory');
522 return;
523 }
524
525 final String javaPath;
526 if (globals.platform.isMacOS) {
527 if (version != null && version!.major < 2020) {
528 javaPath = globals.fs.path.join(directory, 'jre', 'jdk', 'Contents', 'Home');
529 } else if (version != null && version!.major < 2022) {
530 javaPath = globals.fs.path.join(directory, 'jre', 'Contents', 'Home');
531 // See https://github.com/flutter/flutter/issues/125246 for more context.
532 } else {
533 javaPath = globals.fs.path.join(directory, 'jbr', 'Contents', 'Home');
534 }
535 } else {
536 if (version != null && version!.major < 2022) {
537 javaPath = globals.fs.path.join(directory, 'jre');
538 } else {
539 javaPath = globals.fs.path.join(directory, 'jbr');
540 }
541 }
542 final String javaExecutable = globals.fs.path.join(javaPath, 'bin', 'java');
543 if (!globals.processManager.canRun(javaExecutable)) {
544 _validationMessages.add('Unable to find bundled Java version.');
545 } else {
546 RunResult? result;
547 try {
548 result = globals.processUtils.runSync(<String>[javaExecutable, '-version']);
549 } on ProcessException catch (e) {
550 _validationMessages.add('Failed to run Java: $e');
551 }
552 if (result != null && result.exitCode == 0) {
553 final List<String> versionLines = result.stderr.split('\n');
554 final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
555 _validationMessages.add('Java version $javaVersion');
556 _javaPath = javaPath;
557 _isValid = true;
558 } else {
559 _validationMessages.add('Unable to determine bundled Java version.');
560 }
561 }
562 }
563
564 static Version? _parseVersion(String text) {
565 // Matches the version string for Preview builds on macOS.
566 // Example match: EAP AI-242.21829.142.2422.12358220
567 // We try to capture "2422" here, which can be translated to a
568 // more human-friendly "24.2.2".
569 final RegExp eapVersionPattern = RegExp(r'EAP\s+[A-Z]{2}-\d+\.\d+\.\d+\.(\d+)\.\d+');
570 final Match? eapVersionMatch = eapVersionPattern.firstMatch(text);
571
572 if (eapVersionMatch == null) {
573 return Version.parse(text);
574 }
575
576 final String? rawVersionMatch = eapVersionMatch.group(1);
577
578 // length of 4 is because that how version is encrypted: first two digits
579 // for year (part of major version), third for minor version, fourth for patch.
580 if (rawVersionMatch == null || rawVersionMatch.length != 4) {
581 return null;
582 }
583
584 final int? major = int.tryParse('20${rawVersionMatch[0]}${rawVersionMatch[1]}');
585 final int? minor = int.tryParse(rawVersionMatch[2]);
586 final int? patch = int.tryParse(rawVersionMatch[3]);
587 if (major == null || minor == null || patch == null) {
588 return null;
589 }
590 return Version(major, minor, patch);
591 }
592
593 @override
594 String toString() => 'Android Studio ($version)';
595}
596
597bool _pathsAreEqual(String path, String other) {
598 return globals.fs.path.canonicalize(path) == globals.fs.path.canonicalize(other);
599}
600

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com