Unverified Commit 401d401c authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] reland: avoid creating Android Devices if AndroidSDK cannot be found (#64665)

Avoid creating AndroidDevice discovery if the SDK cannot be located. Previously the tool would use which/where adb, however this required us to handle the AndroidSdk class being potentially null - which required an additional layer of indirection around all access. Sometimes these were forgotten leading to NPEs.

In general, not much can be done with an Android Device if the actual SDK is not installed.

Reland with fixed code + tests for null SDK + adb in AndroidDeviceDiscovery
parent f35a8b74
...@@ -255,7 +255,7 @@ class AndroidDevice extends Device { ...@@ -255,7 +255,7 @@ class AndroidDevice extends Device {
AndroidDevicePortForwarder _portForwarder; AndroidDevicePortForwarder _portForwarder;
List<String> adbCommandForDevice(List<String> args) { List<String> adbCommandForDevice(List<String> args) {
return <String>[getAdbPath(_androidSdk), '-s', id, ...args]; return <String>[_androidSdk.adbPath, '-s', id, ...args];
} }
String runAdbCheckedSync( String runAdbCheckedSync(
...@@ -318,13 +318,13 @@ class AndroidDevice extends Device { ...@@ -318,13 +318,13 @@ class AndroidDevice extends Device {
try { try {
final RunResult adbVersion = await _processUtils.run( final RunResult adbVersion = await _processUtils.run(
<String>[getAdbPath(_androidSdk), 'version'], <String>[_androidSdk.adbPath, 'version'],
throwOnError: true, throwOnError: true,
); );
if (_isValidAdbVersion(adbVersion.stdout)) { if (_isValidAdbVersion(adbVersion.stdout)) {
return true; return true;
} }
_logger.printError('The ADB at "${getAdbPath(_androidSdk)}" is too old; please install version 1.0.39 or later.'); _logger.printError('The ADB at "${_androidSdk.adbPath}" is too old; please install version 1.0.39 or later.');
} on Exception catch (error, trace) { } on Exception catch (error, trace) {
_logger.printError('Error running ADB: $error', stackTrace: trace); _logger.printError('Error running ADB: $error', stackTrace: trace);
} }
...@@ -339,7 +339,7 @@ class AndroidDevice extends Device { ...@@ -339,7 +339,7 @@ class AndroidDevice extends Device {
// adb server is out of date. killing.. // adb server is out of date. killing..
// * daemon started successfully * // * daemon started successfully *
await _processUtils.run( await _processUtils.run(
<String>[getAdbPath(_androidSdk), 'start-server'], <String>[_androidSdk.adbPath, 'start-server'],
throwOnError: true, throwOnError: true,
); );
......
...@@ -55,14 +55,13 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -55,14 +55,13 @@ class AndroidDevices extends PollingDeviceDiscovery {
@override @override
Future<List<Device>> pollingGetDevices({ Duration timeout }) async { Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
final String adbPath = getAdbPath(_androidSdk); if (_androidSdk == null || _androidSdk.adbPath == null) {
if (adbPath == null) {
return <AndroidDevice>[]; return <AndroidDevice>[];
} }
String text; String text;
try { try {
text = (await _processUtils.run( text = (await _processUtils.run(
<String>[adbPath, 'devices', '-l'], <String>[_androidSdk.adbPath, 'devices', '-l'],
throwOnError: true, throwOnError: true,
)).stdout.trim(); )).stdout.trim();
} on ArgumentError catch (exception) { } on ArgumentError catch (exception) {
...@@ -88,12 +87,11 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -88,12 +87,11 @@ class AndroidDevices extends PollingDeviceDiscovery {
@override @override
Future<List<String>> getDiagnostics() async { Future<List<String>> getDiagnostics() async {
final String adbPath = getAdbPath(_androidSdk); if (_androidSdk == null || _androidSdk.adbPath == null) {
if (adbPath == null) {
return <String>[]; return <String>[];
} }
final RunResult result = await _processUtils.run(<String>[adbPath, 'devices', '-l']); final RunResult result = await _processUtils.run(<String>[_androidSdk.adbPath, 'devices', '-l']);
if (result.exitCode != 0) { if (result.exitCode != 0) {
return <String>[]; return <String>[];
} else { } else {
......
...@@ -22,20 +22,6 @@ const String kAndroidSdkRoot = 'ANDROID_SDK_ROOT'; ...@@ -22,20 +22,6 @@ const String kAndroidSdkRoot = 'ANDROID_SDK_ROOT';
final RegExp _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$'); final RegExp _numberedAndroidPlatformRe = RegExp(r'^android-([0-9]+)$');
final RegExp _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$'); final RegExp _sdkVersionRe = RegExp(r'^ro.build.version.sdk=([0-9]+)$');
/// Locate ADB. Prefer to use one from an Android SDK, if we can locate that.
/// This should be used over accessing androidSdk.adbPath directly because it
/// will work for those users who have Android Platform Tools installed but
/// not the full SDK.
String getAdbPath(AndroidSdk existingSdk) {
if (existingSdk?.adbPath != null) {
return existingSdk.adbPath;
}
if (existingSdk?.latestVersion == null) {
return globals.os.which('adb')?.path;
}
return existingSdk?.adbPath;
}
// Android SDK layout: // Android SDK layout:
// $ANDROID_SDK_ROOT/platform-tools/adb // $ANDROID_SDK_ROOT/platform-tools/adb
......
...@@ -58,13 +58,13 @@ class AndroidWorkflow implements Workflow { ...@@ -58,13 +58,13 @@ class AndroidWorkflow implements Workflow {
bool get appliesToHostPlatform => _featureFlags.isAndroidEnabled; bool get appliesToHostPlatform => _featureFlags.isAndroidEnabled;
@override @override
bool get canListDevices => getAdbPath(_androidSdk) != null; bool get canListDevices => _androidSdk != null && _androidSdk.adbPath != null;
@override @override
bool get canLaunchDevices => _androidSdk != null && _androidSdk.validateSdkWellFormed().isEmpty; bool get canLaunchDevices => _androidSdk != null && _androidSdk.validateSdkWellFormed().isEmpty;
@override @override
bool get canListEmulators => _androidSdk.emulatorPath != null; bool get canListEmulators => _androidSdk != null && _androidSdk.emulatorPath != null;
} }
class AndroidValidator extends DoctorValidator { class AndroidValidator extends DoctorValidator {
......
...@@ -17,7 +17,7 @@ import '../../src/fake_process_manager.dart'; ...@@ -17,7 +17,7 @@ import '../../src/fake_process_manager.dart';
import '../../src/testbed.dart'; import '../../src/testbed.dart';
void main() { void main() {
testWithoutContext('AndroidDevices returns empty device list on null adb', () async { testWithoutContext('AndroidDevices returns empty device list and diagnostics on null adb', () async {
final AndroidDevices androidDevices = AndroidDevices( final AndroidDevices androidDevices = AndroidDevices(
androidSdk: MockAndroidSdk(null), androidSdk: MockAndroidSdk(null),
logger: BufferLogger.test(), logger: BufferLogger.test(),
...@@ -31,7 +31,25 @@ void main() { ...@@ -31,7 +31,25 @@ void main() {
); );
expect(await androidDevices.pollingGetDevices(), isEmpty); expect(await androidDevices.pollingGetDevices(), isEmpty);
}, skip: true); // a null adb unconditionally calls a static method in AndroidSDK that hits the context. expect(await androidDevices.getDiagnostics(), isEmpty);
});
testWithoutContext('AndroidDevices returns empty device list and diagnostics on null Android SDK', () async {
final AndroidDevices androidDevices = AndroidDevices(
androidSdk: null,
logger: BufferLogger.test(),
androidWorkflow: AndroidWorkflow(
androidSdk: MockAndroidSdk(null),
featureFlags: TestFeatureFlags(),
),
processManager: FakeProcessManager.list(<FakeCommand>[]),
fileSystem: MemoryFileSystem.test(),
platform: FakePlatform(),
);
expect(await androidDevices.pollingGetDevices(), isEmpty);
expect(await androidDevices.getDiagnostics(), isEmpty);
});
testWithoutContext('AndroidDevices throwsToolExit on missing adb path', () { testWithoutContext('AndroidDevices throwsToolExit on missing adb path', () {
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
......
...@@ -22,6 +22,7 @@ import 'package:process/process.dart'; ...@@ -22,6 +22,7 @@ import 'package:process/process.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/mocks.dart' show MockAndroidSdk, MockProcess, MockProcessManager, MockStdio; import '../../src/mocks.dart' show MockAndroidSdk, MockProcess, MockProcessManager, MockStdio;
import '../../src/testbed.dart';
class MockAndroidSdkVersion extends Mock implements AndroidSdkVersion {} class MockAndroidSdkVersion extends Mock implements AndroidSdkVersion {}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
...@@ -56,6 +57,17 @@ void main() { ...@@ -56,6 +57,17 @@ void main() {
return (List<String> command) => MockProcess(stdout: stdoutStream); return (List<String> command) => MockProcess(stdout: stdoutStream);
} }
testWithoutContext('AndroidWorkflow handles a null AndroidSDK', () {
final AndroidWorkflow androidWorkflow = AndroidWorkflow(
featureFlags: TestFeatureFlags(),
androidSdk: null,
);
expect(androidWorkflow.canLaunchDevices, false);
expect(androidWorkflow.canListDevices, false);
expect(androidWorkflow.canListEmulators, false);
});
testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot find sdkmanager', () async { testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot find sdkmanager', () async {
processManager.canRunSucceeds = false; processManager.canRunSucceeds = false;
when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
......
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