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

Provided by KDAB

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