Unverified Commit b460d0ee authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Migrate emulators to null safety (#92738)

parent 9bff08b7
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.8
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -23,11 +21,11 @@ import 'android_sdk.dart'; ...@@ -23,11 +21,11 @@ import 'android_sdk.dart';
class AndroidEmulators extends EmulatorDiscovery { class AndroidEmulators extends EmulatorDiscovery {
AndroidEmulators({ AndroidEmulators({
@required AndroidSdk androidSdk, AndroidSdk? androidSdk,
@required AndroidWorkflow androidWorkflow, required AndroidWorkflow androidWorkflow,
@required FileSystem fileSystem, required FileSystem fileSystem,
@required Logger logger, required Logger logger,
@required ProcessManager processManager, required ProcessManager processManager,
}) : _androidSdk = androidSdk, }) : _androidSdk = androidSdk,
_androidWorkflow = androidWorkflow, _androidWorkflow = androidWorkflow,
_fileSystem = fileSystem, _fileSystem = fileSystem,
...@@ -36,7 +34,7 @@ class AndroidEmulators extends EmulatorDiscovery { ...@@ -36,7 +34,7 @@ class AndroidEmulators extends EmulatorDiscovery {
_processUtils = ProcessUtils(logger: logger, processManager: processManager); _processUtils = ProcessUtils(logger: logger, processManager: processManager);
final AndroidWorkflow _androidWorkflow; final AndroidWorkflow _androidWorkflow;
final AndroidSdk _androidSdk; final AndroidSdk? _androidSdk;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final Logger _logger; final Logger _logger;
final ProcessManager _processManager; final ProcessManager _processManager;
...@@ -50,14 +48,14 @@ class AndroidEmulators extends EmulatorDiscovery { ...@@ -50,14 +48,14 @@ class AndroidEmulators extends EmulatorDiscovery {
@override @override
bool get canLaunchAnything => _androidWorkflow.canListEmulators bool get canLaunchAnything => _androidWorkflow.canListEmulators
&& _androidSdk.getAvdManagerPath() != null; && _androidSdk?.getAvdManagerPath() != null;
@override @override
Future<List<Emulator>> get emulators => _getEmulatorAvds(); Future<List<Emulator>> get emulators => _getEmulatorAvds();
/// Return the list of available emulator AVDs. /// Return the list of available emulator AVDs.
Future<List<AndroidEmulator>> _getEmulatorAvds() async { Future<List<AndroidEmulator>> _getEmulatorAvds() async {
final String emulatorPath = _androidSdk?.emulatorPath; final String? emulatorPath = _androidSdk?.emulatorPath;
if (emulatorPath == null) { if (emulatorPath == null) {
return <AndroidEmulator>[]; return <AndroidEmulator>[];
} }
...@@ -82,7 +80,7 @@ class AndroidEmulators extends EmulatorDiscovery { ...@@ -82,7 +80,7 @@ class AndroidEmulators extends EmulatorDiscovery {
AndroidEmulator _loadEmulatorInfo(String id) { AndroidEmulator _loadEmulatorInfo(String id) {
id = id.trim(); id = id.trim();
final String avdPath = _androidSdk.getAvdPath(); final String? avdPath = _androidSdk?.getAvdPath();
final AndroidEmulator androidEmulatorWithoutProperties = AndroidEmulator( final AndroidEmulator androidEmulatorWithoutProperties = AndroidEmulator(
id, id,
processManager: _processManager, processManager: _processManager,
...@@ -97,10 +95,11 @@ class AndroidEmulators extends EmulatorDiscovery { ...@@ -97,10 +95,11 @@ class AndroidEmulators extends EmulatorDiscovery {
return androidEmulatorWithoutProperties; return androidEmulatorWithoutProperties;
} }
final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync()); final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
if (ini['path'] == null) { final String? path = ini['path'];
if (path == null) {
return androidEmulatorWithoutProperties; return androidEmulatorWithoutProperties;
} }
final File configFile = _fileSystem.file(_fileSystem.path.join(ini['path'], 'config.ini')); final File configFile = _fileSystem.file(_fileSystem.path.join(path, 'config.ini'));
if (!configFile.existsSync()) { if (!configFile.existsSync()) {
return androidEmulatorWithoutProperties; return androidEmulatorWithoutProperties;
} }
...@@ -117,20 +116,20 @@ class AndroidEmulators extends EmulatorDiscovery { ...@@ -117,20 +116,20 @@ class AndroidEmulators extends EmulatorDiscovery {
class AndroidEmulator extends Emulator { class AndroidEmulator extends Emulator {
AndroidEmulator(String id, { AndroidEmulator(String id, {
Map<String, String> properties, Map<String, String>? properties,
@required Logger logger, required Logger logger,
@required AndroidSdk androidSdk, AndroidSdk? androidSdk,
@required ProcessManager processManager, required ProcessManager processManager,
}) : _properties = properties, }) : _properties = properties,
_logger = logger, _logger = logger,
_androidSdk = androidSdk, _androidSdk = androidSdk,
_processUtils = ProcessUtils(logger: logger, processManager: processManager), _processUtils = ProcessUtils(logger: logger, processManager: processManager),
super(id, properties != null && properties.isNotEmpty); super(id, properties != null && properties.isNotEmpty);
final Map<String, String> _properties; final Map<String, String>? _properties;
final Logger _logger; final Logger _logger;
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
final AndroidSdk _androidSdk; final AndroidSdk? _androidSdk;
// Android Studio uses the ID with underscores replaced with spaces // Android Studio uses the ID with underscores replaced with spaces
// for the name if displayname is not set so we do the same. // for the name if displayname is not set so we do the same.
...@@ -138,7 +137,7 @@ class AndroidEmulator extends Emulator { ...@@ -138,7 +137,7 @@ class AndroidEmulator extends Emulator {
String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim(); String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim();
@override @override
String get manufacturer => _prop('hw.device.manufacturer'); String? get manufacturer => _prop('hw.device.manufacturer');
@override @override
Category get category => Category.mobile; Category get category => Category.mobile;
...@@ -146,12 +145,16 @@ class AndroidEmulator extends Emulator { ...@@ -146,12 +145,16 @@ class AndroidEmulator extends Emulator {
@override @override
PlatformType get platformType => PlatformType.android; PlatformType get platformType => PlatformType.android;
String _prop(String name) => _properties != null ? _properties[name] : null; String? _prop(String name) => _properties != null ? _properties![name] : null;
@override @override
Future<void> launch({@visibleForTesting Duration startupDuration, bool coldBoot = false}) async { Future<void> launch({@visibleForTesting Duration? startupDuration, bool coldBoot = false}) async {
final String? emulatorPath = _androidSdk?.emulatorPath;
if (emulatorPath == null) {
throw Exception('Emulator is missing from the Android SDK');
}
final List<String> command = <String>[ final List<String> command = <String>[
_androidSdk.emulatorPath, emulatorPath,
'-avd', '-avd',
id, id,
if (coldBoot) if (coldBoot)
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.8
import 'dart:math' as math; import 'dart:math' as math;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -19,16 +17,16 @@ import 'base/process.dart'; ...@@ -19,16 +17,16 @@ import 'base/process.dart';
import 'device.dart'; import 'device.dart';
import 'ios/ios_emulators.dart'; import 'ios/ios_emulators.dart';
EmulatorManager get emulatorManager => context.get<EmulatorManager>(); EmulatorManager? get emulatorManager => context.get<EmulatorManager>();
/// A class to get all available emulators. /// A class to get all available emulators.
class EmulatorManager { class EmulatorManager {
EmulatorManager({ EmulatorManager({
@required AndroidSdk androidSdk, AndroidSdk? androidSdk,
@required Logger logger, required Logger logger,
@required ProcessManager processManager, required ProcessManager processManager,
@required AndroidWorkflow androidWorkflow, required AndroidWorkflow androidWorkflow,
@required FileSystem fileSystem, required FileSystem fileSystem,
}) : _androidSdk = androidSdk, }) : _androidSdk = androidSdk,
_processUtils = ProcessUtils(logger: logger, processManager: processManager), _processUtils = ProcessUtils(logger: logger, processManager: processManager),
_androidEmulators = AndroidEmulators( _androidEmulators = AndroidEmulators(
...@@ -41,7 +39,7 @@ class EmulatorManager { ...@@ -41,7 +39,7 @@ class EmulatorManager {
_emulatorDiscoverers.add(_androidEmulators); _emulatorDiscoverers.add(_androidEmulators);
} }
final AndroidSdk _androidSdk; final AndroidSdk? _androidSdk;
final AndroidEmulators _androidEmulators; final AndroidEmulators _androidEmulators;
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
...@@ -55,14 +53,19 @@ class EmulatorManager { ...@@ -55,14 +53,19 @@ class EmulatorManager {
final List<Emulator> emulators = await getAllAvailableEmulators(); final List<Emulator> emulators = await getAllAvailableEmulators();
searchText = searchText.toLowerCase(); searchText = searchText.toLowerCase();
bool exactlyMatchesEmulatorId(Emulator emulator) => bool exactlyMatchesEmulatorId(Emulator emulator) =>
emulator.id?.toLowerCase() == searchText || emulator.id.toLowerCase() == searchText ||
emulator.name?.toLowerCase() == searchText; emulator.name.toLowerCase() == searchText;
bool startsWithEmulatorId(Emulator emulator) => bool startsWithEmulatorId(Emulator emulator) =>
emulator.id?.toLowerCase()?.startsWith(searchText) == true || emulator.id.toLowerCase().startsWith(searchText) == true ||
emulator.name?.toLowerCase()?.startsWith(searchText) == true; emulator.name.toLowerCase().startsWith(searchText) == true;
final Emulator exactMatch = Emulator? exactMatch;
emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null); for (final Emulator emulator in emulators) {
if (exactlyMatchesEmulatorId(emulator)) {
exactMatch = emulator;
break;
}
}
if (exactMatch != null) { if (exactMatch != null) {
return <Emulator>[exactMatch]; return <Emulator>[exactMatch];
} }
...@@ -85,7 +88,7 @@ class EmulatorManager { ...@@ -85,7 +88,7 @@ class EmulatorManager {
} }
/// Return the list of all available emulators. /// Return the list of all available emulators.
Future<CreateEmulatorResult> createEmulator({ String name }) async { Future<CreateEmulatorResult> createEmulator({ String? name }) async {
if (name == null || name.isEmpty) { if (name == null || name.isEmpty) {
const String autoName = 'flutter_emulator'; const String autoName = 'flutter_emulator';
// Don't use getEmulatorsMatching here, as it will only return one // Don't use getEmulatorsMatching here, as it will only return one
...@@ -102,21 +105,23 @@ class EmulatorManager { ...@@ -102,21 +105,23 @@ class EmulatorManager {
name = '${autoName}_${++suffix}'; name = '${autoName}_${++suffix}';
} }
} }
if (!_androidEmulators.canLaunchAnything) { final String emulatorName = name!;
return CreateEmulatorResult(name, final String? avdManagerPath = _androidSdk?.avdManagerPath;
if (avdManagerPath == null || !_androidEmulators.canLaunchAnything) {
return CreateEmulatorResult(emulatorName,
success: false, error: 'avdmanager is missing from the Android SDK' success: false, error: 'avdmanager is missing from the Android SDK'
); );
} }
final String device = await _getPreferredAvailableDevice(); final String? device = await _getPreferredAvailableDevice(avdManagerPath);
if (device == null) { if (device == null) {
return CreateEmulatorResult(name, return CreateEmulatorResult(emulatorName,
success: false, error: 'No device definitions are available'); success: false, error: 'No device definitions are available');
} }
final String sdkId = await _getPreferredSdkId(); final String? sdkId = await _getPreferredSdkId(avdManagerPath);
if (sdkId == null) { if (sdkId == null) {
return CreateEmulatorResult(name, return CreateEmulatorResult(emulatorName,
success: false, success: false,
error: error:
'No suitable Android AVD system images are available. You may need to install these' 'No suitable Android AVD system images are available. You may need to install these'
...@@ -128,7 +133,7 @@ class EmulatorManager { ...@@ -128,7 +133,7 @@ class EmulatorManager {
// to flutter users. Specifically: // to flutter users. Specifically:
// - Removes lines that say "null" (!) // - Removes lines that say "null" (!)
// - Removes lines that tell the user to use '--force' to overwrite emulators // - Removes lines that tell the user to use '--force' to overwrite emulators
String cleanError(String error) { String? cleanError(String? error) {
if (error == null || error.trim() == '') { if (error == null || error.trim() == '') {
return null; return null;
} }
...@@ -141,16 +146,16 @@ class EmulatorManager { ...@@ -141,16 +146,16 @@ class EmulatorManager {
.trim(); .trim();
} }
final RunResult runResult = await _processUtils.run(<String>[ final RunResult runResult = await _processUtils.run(<String>[
_androidSdk?.avdManagerPath, avdManagerPath,
'create', 'create',
'avd', 'avd',
'-n', name, '-n', emulatorName,
'-k', sdkId, '-k', sdkId,
'-d', device, '-d', device,
], environment: _androidSdk?.sdkManagerEnv, ], environment: _androidSdk?.sdkManagerEnv,
); );
return CreateEmulatorResult( return CreateEmulatorResult(
name, emulatorName,
success: runResult.exitCode == 0, success: runResult.exitCode == 0,
output: runResult.stdout, output: runResult.stdout,
error: cleanError(runResult.stderr), error: cleanError(runResult.stderr),
...@@ -162,9 +167,9 @@ class EmulatorManager { ...@@ -162,9 +167,9 @@ class EmulatorManager {
'pixel_xl', 'pixel_xl',
]; ];
Future<String> _getPreferredAvailableDevice() async { Future<String?> _getPreferredAvailableDevice(String avdManagerPath) async {
final List<String> args = <String>[ final List<String> args = <String>[
_androidSdk?.avdManagerPath, avdManagerPath,
'list', 'list',
'device', 'device',
'-c', '-c',
...@@ -180,19 +185,21 @@ class EmulatorManager { ...@@ -180,19 +185,21 @@ class EmulatorManager {
.where((String l) => preferredDevices.contains(l.trim())) .where((String l) => preferredDevices.contains(l.trim()))
.toList(); .toList();
return preferredDevices.firstWhere( for (final String device in preferredDevices) {
(String d) => availableDevices.contains(d), if (availableDevices.contains(device)) {
orElse: () => null, return device;
); }
}
return null;
} }
static final RegExp _androidApiVersion = RegExp(r';android-(\d+);'); static final RegExp _androidApiVersion = RegExp(r';android-(\d+);');
Future<String> _getPreferredSdkId() async { Future<String?> _getPreferredSdkId(String avdManagerPath) async {
// It seems that to get the available list of images, we need to send a // It seems that to get the available list of images, we need to send a
// request to create without the image and it'll provide us a list :-( // request to create without the image and it'll provide us a list :-(
final List<String> args = <String>[ final List<String> args = <String>[
_androidSdk?.avdManagerPath, avdManagerPath,
'create', 'create',
'avd', 'avd',
'-n', 'temp', '-n', 'temp',
...@@ -209,7 +216,7 @@ class EmulatorManager { ...@@ -209,7 +216,7 @@ class EmulatorManager {
.toList(); .toList();
final List<int> availableApiVersions = availableIDs final List<int> availableApiVersions = availableIDs
.map<String>((String id) => _androidApiVersion.firstMatch(id).group(1)) .map<String>((String id) => _androidApiVersion.firstMatch(id)!.group(1)!)
.map<int>((String apiVersion) => int.parse(apiVersion)) .map<int>((String apiVersion) => int.parse(apiVersion))
.toList(); .toList();
...@@ -220,10 +227,12 @@ class EmulatorManager { ...@@ -220,10 +227,12 @@ class EmulatorManager {
// We're out of preferences, we just have to return the first one with the high // We're out of preferences, we just have to return the first one with the high
// API version. // API version.
return availableIDs.firstWhere( for (final String id in availableIDs) {
(String id) => id.contains(';android-$apiVersion;'), if (id.contains(';android-$apiVersion;')) {
orElse: () => null, return id;
); }
}
return null;
} }
/// Whether we're capable of listing any emulators given the current environment configuration. /// Whether we're capable of listing any emulators given the current environment configuration.
...@@ -252,7 +261,7 @@ abstract class Emulator { ...@@ -252,7 +261,7 @@ abstract class Emulator {
final String id; final String id;
final bool hasConfig; final bool hasConfig;
String get name; String get name;
String get manufacturer; String? get manufacturer;
Category get category; Category get category;
PlatformType get platformType; PlatformType get platformType;
...@@ -282,10 +291,10 @@ abstract class Emulator { ...@@ -282,10 +291,10 @@ abstract class Emulator {
final List<List<String>> table = <List<String>>[ final List<List<String>> table = <List<String>>[
for (final Emulator emulator in emulators) for (final Emulator emulator in emulators)
<String>[ <String>[
emulator.id ?? '', emulator.id,
emulator.name ?? '', emulator.name,
emulator.manufacturer ?? '', emulator.manufacturer ?? '',
emulator.platformType?.toString() ?? '', emulator.platformType.toString(),
], ],
]; ];
...@@ -315,10 +324,10 @@ abstract class Emulator { ...@@ -315,10 +324,10 @@ abstract class Emulator {
} }
class CreateEmulatorResult { class CreateEmulatorResult {
CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error}); CreateEmulatorResult(this.emulatorName, {required this.success, this.output, this.error});
final bool success; final bool success;
final String emulatorName; final String emulatorName;
final String output; final String? output;
final String error; final String? error;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.8 import '../base/common.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../device.dart'; import '../device.dart';
import '../emulator.dart'; import '../emulator.dart';
...@@ -15,7 +14,7 @@ class IOSEmulators extends EmulatorDiscovery { ...@@ -15,7 +14,7 @@ class IOSEmulators extends EmulatorDiscovery {
bool get supportsPlatform => globals.platform.isMacOS; bool get supportsPlatform => globals.platform.isMacOS;
@override @override
bool get canListAnything => globals.iosWorkflow.canListEmulators; bool get canListAnything => globals.iosWorkflow?.canListEmulators == true;
@override @override
Future<List<Emulator>> get emulators async => getEmulators(); Future<List<Emulator>> get emulators async => getEmulators();
...@@ -41,12 +40,16 @@ class IOSEmulator extends Emulator { ...@@ -41,12 +40,16 @@ class IOSEmulator extends Emulator {
@override @override
Future<void> launch({bool coldBoot = false}) async { Future<void> launch({bool coldBoot = false}) async {
final String? simulatorPath = globals.xcode?.getSimulatorPath();
if (simulatorPath == null) {
throwToolExit('Could not find Simulator app');
}
Future<bool> launchSimulator(List<String> additionalArgs) async { Future<bool> launchSimulator(List<String> additionalArgs) async {
final List<String> args = <String>[ final List<String> args = <String>[
'open', 'open',
...additionalArgs, ...additionalArgs,
'-a', '-a',
globals.xcode.getSimulatorPath(), simulatorPath,
]; ];
final RunResult launchResult = await globals.processUtils.run(args); final RunResult launchResult = await globals.processUtils.run(args);
...@@ -70,7 +73,7 @@ class IOSEmulator extends Emulator { ...@@ -70,7 +73,7 @@ class IOSEmulator extends Emulator {
/// Return the list of iOS Simulators (there can only be zero or one). /// Return the list of iOS Simulators (there can only be zero or one).
List<IOSEmulator> getEmulators() { List<IOSEmulator> getEmulators() {
final String simulatorPath = globals.xcode.getSimulatorPath(); final String? simulatorPath = globals.xcode?.getSimulatorPath();
if (simulatorPath == null) { if (simulatorPath == null) {
return <IOSEmulator>[]; return <IOSEmulator>[];
} }
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.8
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/android/android_emulator.dart'; import 'package:flutter_tools/src/android/android_emulator.dart';
...@@ -125,7 +123,7 @@ void main() { ...@@ -125,7 +123,7 @@ void main() {
}); });
group('Android emulator launch ', () { group('Android emulator launch ', () {
FakeAndroidSdk mockSdk; late FakeAndroidSdk mockSdk;
setUp(() { setUp(() {
mockSdk = FakeAndroidSdk(); mockSdk = FakeAndroidSdk();
...@@ -198,10 +196,30 @@ void main() { ...@@ -198,10 +196,30 @@ void main() {
expect(logger.errorText, isEmpty); expect(logger.errorText, isEmpty);
}); });
testWithoutContext('throws if emulator not found', () async {
mockSdk.emulatorPath = null;
final AndroidEmulator emulator = AndroidEmulator(
emulatorID,
processManager: FakeProcessManager.empty(),
androidSdk: mockSdk,
logger: BufferLogger.test(),
);
await expectLater(
() => emulator.launch(startupDuration: Duration.zero),
throwsA(isException.having(
(Exception exception) => exception.toString(),
'description',
contains('Emulator is missing from the Android SDK'),
)),
);
});
}); });
} }
class FakeAndroidSdk extends Fake implements AndroidSdk { class FakeAndroidSdk extends Fake implements AndroidSdk {
@override @override
String emulatorPath; String? emulatorPath;
} }
...@@ -97,7 +97,6 @@ void main() { ...@@ -97,7 +97,6 @@ void main() {
stdout: 'existing-avd-1', stdout: 'existing-avd-1',
), ),
]), ]),
androidSdk: null,
androidWorkflow: AndroidWorkflow( androidWorkflow: AndroidWorkflow(
androidSdk: sdk, androidSdk: sdk,
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment