screenshot.dart 6.14 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Devon Carew's avatar
Devon Carew committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6 7
import 'package:vm_service/vm_service.dart' as vm_service;

8
import '../base/common.dart';
9
import '../base/file_system.dart';
10
import '../convert.dart';
11
import '../device.dart';
12
import '../globals.dart' as globals;
Devon Carew's avatar
Devon Carew committed
13
import '../runner/flutter_command.dart';
14
import '../vmservice.dart';
Devon Carew's avatar
Devon Carew committed
15

16
const String _kOut = 'out';
17
const String _kType = 'type';
18
const String _kVmServiceUrl = 'vm-service-url';
19 20
const String _kDeviceType = 'device';
const String _kSkiaType = 'skia';
21

Devon Carew's avatar
Devon Carew committed
22
class ScreenshotCommand extends FlutterCommand {
23
  ScreenshotCommand({required this.fs}) {
24 25
    argParser.addOption(
      _kOut,
Devon Carew's avatar
Devon Carew committed
26
      abbr: 'o',
27
      valueHelp: 'path/to/file',
28 29 30
      help: 'Location to write the screenshot.',
    );
    argParser.addOption(
31
      _kVmServiceUrl,
32
      aliases: <String>[ 'observatory-url' ], // for historical reasons
33
      valueHelp: 'URI',
34
      help: 'The VM Service URL to which to connect.\n'
35
          'This is required when "--$_kType" is "$_kSkiaType".\n'
36 37
          'To find the VM service URL, use "flutter run" and look for '
          '"A Dart VM Service ... is available at" in the output.',
38 39 40 41 42
    );
    argParser.addOption(
      _kType,
      valueHelp: 'type',
      help: 'The type of screenshot to retrieve.',
43
      allowed: const <String>[_kDeviceType, _kSkiaType],
44
      allowedHelp: const <String, String>{
45
        _kDeviceType: "Delegate to the device's native screenshot capabilities. This "
46 47
                      'screenshots the entire screen currently being displayed (including content '
                      'not rendered by Flutter, like the device status bar).',
48
        _kSkiaType: 'Render the Flutter app as a Skia picture. Requires "--$_kVmServiceUrl".',
49 50
      },
      defaultsTo: _kDeviceType,
51
    );
52
    usesDeviceTimeoutOption();
53
    usesDeviceConnectionOption();
Devon Carew's avatar
Devon Carew committed
54 55
  }

56 57
  final FileSystem fs;

Devon Carew's avatar
Devon Carew committed
58 59 60 61 62 63
  @override
  String get name => 'screenshot';

  @override
  String get description => 'Take a screenshot from a connected device.';

64 65 66
  @override
  final String category = FlutterCommandCategory.tools;

67 68 69
  @override
  bool get refreshWirelessDevices => true;

Devon Carew's avatar
Devon Carew committed
70 71 72
  @override
  final List<String> aliases = <String>['pic'];

73
  Device? device;
Devon Carew's avatar
Devon Carew committed
74

75
  Future<void> _validateOptions(String? screenshotType, String? vmServiceUrl) async {
76 77
    switch (screenshotType) {
      case _kDeviceType:
78 79
        if (vmServiceUrl != null) {
          throwToolExit('VM Service URI cannot be provided for screenshot type $screenshotType');
80 81
        }
        device = await findTargetDevice();
82 83 84
        if (device == null) {
          throwToolExit('Must have a connected device for screenshot type $screenshotType');
        }
85 86
        if (!device!.supportsScreenshot) {
          throwToolExit('Screenshot not supported for ${device!.name}.');
87 88
        }
      default:
89 90
        if (vmServiceUrl == null) {
          throwToolExit('VM Service URI must be specified for screenshot type $screenshotType');
91
        }
92 93
        if (vmServiceUrl.isEmpty || Uri.tryParse(vmServiceUrl) == null) {
          throwToolExit('VM Service URI "$vmServiceUrl" is invalid');
94
        }
95 96 97
    }
  }

Devon Carew's avatar
Devon Carew committed
98
  @override
99
  Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async {
100
    await _validateOptions(stringArg(_kType), stringArg(_kVmServiceUrl));
101
    return super.verifyThenRunCommand(commandPath);
102
  }
Devon Carew's avatar
Devon Carew committed
103 104

  @override
105
  Future<FlutterCommandResult> runCommand() async {
106
    File? outputFile;
107
    if (argResults?.wasParsed(_kOut) ?? false) {
108
      outputFile = fs.file(stringArg(_kOut));
109
    }
Devon Carew's avatar
Devon Carew committed
110

111
    bool success = true;
112
    switch (stringArg(_kType)) {
113
      case _kDeviceType:
114
        await runScreenshot(outputFile);
115
      case _kSkiaType:
116
        success = await runSkia(outputFile);
Devon Carew's avatar
Devon Carew committed
117
    }
118

119 120
    return success ? FlutterCommandResult.success()
                   : FlutterCommandResult.fail();
121
  }
Devon Carew's avatar
Devon Carew committed
122

123
  Future<void> runScreenshot(File? outputFile) async {
124
    outputFile ??= globals.fsUtils.getUniqueFile(
125
      fs.currentDirectory,
126 127 128
      'flutter',
      'png',
    );
129

Devon Carew's avatar
Devon Carew committed
130
    try {
131
      await device!.takeScreenshot(outputFile);
132
    } on Exception catch (error) {
133
      throwToolExit('Error taking screenshot: $error');
Devon Carew's avatar
Devon Carew committed
134
    }
135 136 137 138 139 140 141 142 143 144 145

    checkOutput(outputFile, fs);

    try {
      _showOutputFileInfo(outputFile);
    } on Exception catch (error) {
      throwToolExit(
        'Error with provided file path: "${outputFile.path}"\n'
        'Error: $error'
      );
    }
Devon Carew's avatar
Devon Carew committed
146
  }
147

148
  Future<bool> runSkia(File? outputFile) async {
149
    final Uri vmServiceUrl = Uri.parse(stringArg(_kVmServiceUrl)!);
150
    final FlutterVmService vmService = await connectToVmService(vmServiceUrl, logger: globals.logger);
151
    final vm_service.Response? skp = await vmService.screenshotSkp();
152 153 154 155 156 157 158
    if (skp == null) {
      globals.printError(
        'The Skia picture request failed, probably because the device was '
        'disconnected',
      );
      return false;
    }
159
    outputFile ??= globals.fsUtils.getUniqueFile(
160
      fs.currentDirectory,
161 162 163
      'flutter',
      'skp',
    );
164
    final IOSink sink = outputFile.openWrite();
165
    sink.add(base64.decode(skp.json?['skp'] as String));
166
    await sink.close();
167
    _showOutputFileInfo(outputFile);
168
    ensureOutputIsNotJsonRpcError(outputFile);
169
    return true;
170 171
  }

172 173 174 175 176 177 178 179 180
  static void checkOutput(File outputFile, FileSystem fs) {
    if (!fs.file(outputFile.path).existsSync()) {
      throwToolExit(
          'File was not created, ensure path is valid\n'
          'Path provided: "${outputFile.path}"'
      );
    }
  }

181 182
  @visibleForTesting
  static void ensureOutputIsNotJsonRpcError(File outputFile) {
183 184 185 186 187 188 189
    if (outputFile.lengthSync() >= 1000) {
      return;
    }
    final String content = outputFile.readAsStringSync(
      encoding: const AsciiCodec(allowInvalid: true),
    );
    if (content.startsWith('{"jsonrpc":"2.0", "error"')) {
190
      throwToolExit('It appears the output file contains an error message, not valid output.');
191 192 193
    }
  }

194 195
  void _showOutputFileInfo(File outputFile) {
    final int sizeKB = (outputFile.lengthSync()) ~/ 1024;
196
    globals.printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
197
  }
Devon Carew's avatar
Devon Carew committed
198
}