Unverified Commit fdda777e authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] support screenshot on all device types (#80616)

Co-authored-by: 's avatarZachary Anderson <zanderso@users.noreply.github.com>
parent 71cb40f4
...@@ -30,6 +30,7 @@ import 'build_system/targets/localizations.dart'; ...@@ -30,6 +30,7 @@ import 'build_system/targets/localizations.dart';
import 'bundle.dart'; import 'bundle.dart';
import 'cache.dart'; import 'cache.dart';
import 'compile.dart'; import 'compile.dart';
import 'convert.dart';
import 'devfs.dart'; import 'devfs.dart';
import 'device.dart'; import 'device.dart';
import 'features.dart'; import 'features.dart';
...@@ -938,10 +939,18 @@ abstract class ResidentHandlers { ...@@ -938,10 +939,18 @@ abstract class ResidentHandlers {
/// If the device has a connected vmservice, this method will attempt to hide /// If the device has a connected vmservice, this method will attempt to hide
/// and restore the debug banner before taking the screenshot. /// and restore the debug banner before taking the screenshot.
/// ///
/// Throws an [AssertionError] if [Device.supportsScreenshot] is not true. /// If the device type does not support a "native" screenshot, then this
/// will fallback to a rasterizer screenshot from the engine. This has the
/// downside of being unable to display the contents of platform views.
///
/// This method will return without writing the screenshot file if any
/// RPC errors are encountered, printing them to stderr. This is true even
/// if an error occurs after the data has already been received, such as
/// from restoring the debug banner.
Future<void> screenshot(FlutterDevice device) async { Future<void> screenshot(FlutterDevice device) async {
assert(device.device.supportsScreenshot); if (!device.device.supportsScreenshot && !supportsServiceProtocol) {
return;
}
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Taking screenshot for ${device.device.name}...', 'Taking screenshot for ${device.device.name}...',
); );
...@@ -950,50 +959,79 @@ abstract class ResidentHandlers { ...@@ -950,50 +959,79 @@ abstract class ResidentHandlers {
'flutter', 'flutter',
'png', 'png',
); );
List<FlutterView> views = <FlutterView>[];
try {
bool result;
if (device.device.supportsScreenshot) {
result = await _toggleDebugBanner(device, () => device.device.takeScreenshot(outputFile));
} else {
result = await _takeVmServiceScreenshot(device, outputFile);
}
if (!result) {
return;
}
final int sizeKB = outputFile.lengthSync() ~/ 1024;
status.stop();
logger.printStatus(
'Screenshot written to ${fileSystem.path.relative(outputFile.path)} (${sizeKB}kB).',
);
} on Exception catch (error) {
status.cancel();
logger.printError('Error taking screenshot: $error');
}
}
Future<bool> _takeVmServiceScreenshot(FlutterDevice device, File outputFile) async {
final bool isWebDevice = device.targetPlatform == TargetPlatform.web_javascript;
assert(supportsServiceProtocol);
return _toggleDebugBanner(device, () async {
final vm_service.Response response = isWebDevice
? await device.vmService.callMethodWrapper('ext.dwds.screenshot')
: await device.vmService.screenshot();
if (response == null) {
throw Exception('Failed to take screenshot');
}
final String data = response.json[isWebDevice ? 'data' : 'screenshot'] as String;
outputFile.writeAsBytesSync(base64.decode(data));
});
}
Future<bool> _toggleDebugBanner(FlutterDevice device, Future<void> Function() cb) async {
List<vm_service.IsolateRef> views = <vm_service.IsolateRef>[];
if (supportsServiceProtocol) {
views = await device._getCurrentIsolates();
}
Future<bool> setDebugBanner(bool value) async { Future<bool> setDebugBanner(bool value) async {
try { try {
for (final FlutterView view in views) { for (final vm_service.IsolateRef view in views) {
await device.vmService.flutterDebugAllowBanner( await device.vmService.flutterDebugAllowBanner(
value, value,
isolateId: view.uiIsolate.id, isolateId: view.id,
); );
} }
return true; return true;
} on Exception catch (error) { } on vm_service.RPCError catch (error) {
status.cancel();
logger.printError('Error communicating with Flutter on the device: $error'); logger.printError('Error communicating with Flutter on the device: $error');
return false; return false;
} }
} }
if (!await setDebugBanner(false)) {
return false;
}
bool succeeded = true;
try { try {
if (supportsServiceProtocol && isRunningDebug) { await cb();
// Ensure that the vmService access is guarded by supportsServiceProtocol, it } finally {
// will be null in release mode. if (!await setDebugBanner(true)) {
views = await device.vmService.getFlutterViews(); succeeded = false;
if (!await setDebugBanner(false)) {
return;
}
}
try {
await device.device.takeScreenshot(outputFile);
} finally {
if (supportsServiceProtocol && isRunningDebug) {
await setDebugBanner(true);
}
} }
final int sizeKB = outputFile.lengthSync() ~/ 1024;
status.stop();
logger.printStatus(
'Screenshot written to ${fileSystem.path.relative(outputFile.path)} (${sizeKB}kB).',
);
} on Exception catch (error) {
status.cancel();
logger.printError('Error taking screenshot: $error');
} }
return succeeded;
} }
/// Remove sigusr signal handlers. /// Remove sigusr signal handlers.
Future<void> cleanupAfterSignal(); Future<void> cleanupAfterSignal();
...@@ -1633,9 +1671,7 @@ class TerminalHandler { ...@@ -1633,9 +1671,7 @@ class TerminalHandler {
return true; return true;
case 's': case 's':
for (final FlutterDevice device in residentRunner.flutterDevices) { for (final FlutterDevice device in residentRunner.flutterDevices) {
if (device.device.supportsScreenshot) { await residentRunner.screenshot(device);
await residentRunner.screenshot(device);
}
} }
return true; return true;
case 'S': case 'S':
......
...@@ -1645,143 +1645,6 @@ void main() { ...@@ -1645,143 +1645,6 @@ void main() {
DevtoolsLauncher: () => mockDevtoolsLauncher, DevtoolsLauncher: () => mockDevtoolsLauncher,
}); });
testUsingContext('ResidentRunner can take screenshot on debug device', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner can take screenshot on release device', () => testbed.run(() async {
residentRunner = ColdRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws RpcError', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if debugAllowBanner during second request', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if takeScreenshot throws', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
),
]);
// Ensure that takeScreenshot will throw an exception.
mockDevice.failScreenshot = true;
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
}));
testUsingContext("ResidentRunner can't take screenshot on device without support", () => testbed.run(() {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
mockDevice.supportsScreenshot = false;
expect(() => residentRunner.screenshot(mockFlutterDevice),
throwsAssertionError);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner does not toggle banner in non-debug mode', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
residentRunner = HotRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('FlutterDevice will not exit a paused isolate', () => testbed.run(() async { testUsingContext('FlutterDevice will not exit a paused isolate', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
FakeVmServiceRequest( FakeVmServiceRequest(
......
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