screenshot.dart 6.79 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 6
import 'package:vm_service/vm_service.dart' as vm_service;

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

15
const String _kOut = 'out';
16
const String _kType = 'type';
17
const String _kObservatoryUrl = 'observatory-url';
18 19 20
const String _kDeviceType = 'device';
const String _kSkiaType = 'skia';
const String _kRasterizerType = 'rasterizer';
21

Devon Carew's avatar
Devon Carew committed
22 23
class ScreenshotCommand extends FlutterCommand {
  ScreenshotCommand() {
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 32
      _kObservatoryUrl,
      aliases: <String>[ 'observatory-url' ], // for historical reasons
33
      valueHelp: 'URI',
34 35 36
      help: 'The Observatory URL to which to connect.\n'
          'This is required when "--$_kType" is "$_kSkiaType" or "$_kRasterizerType".\n'
          'To find the Observatory URL, use "flutter run" and look for '
37
          '"An Observatory ... is available at" in the output.',
38 39 40 41 42 43 44
    );
    argParser.addOption(
      _kType,
      valueHelp: 'type',
      help: 'The type of screenshot to retrieve.',
      allowed: const <String>[_kDeviceType, _kSkiaType, _kRasterizerType],
      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 49
        _kSkiaType: 'Render the Flutter app as a Skia picture. Requires "--$_kObservatoryUrl".',
        _kRasterizerType: 'Render the Flutter app using the rasterizer. Requires "--$_kObservatoryUrl."',
50 51
      },
      defaultsTo: _kDeviceType,
52
    );
53
    usesDeviceTimeoutOption();
Devon Carew's avatar
Devon Carew committed
54 55 56 57 58 59 60 61
  }

  @override
  String get name => 'screenshot';

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

62 63 64
  @override
  final String category = FlutterCommandCategory.tools;

Devon Carew's avatar
Devon Carew committed
65 66 67
  @override
  final List<String> aliases = <String>['pic'];

68
  Device? device;
Devon Carew's avatar
Devon Carew committed
69

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

Devon Carew's avatar
Devon Carew committed
94
  @override
95
  Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async {
96
    await _validateOptions(stringArgDeprecated(_kType), stringArgDeprecated(_kObservatoryUrl));
97
    return super.verifyThenRunCommand(commandPath);
98
  }
Devon Carew's avatar
Devon Carew committed
99 100

  @override
101
  Future<FlutterCommandResult> runCommand() async {
102
    File? outputFile;
103
    if (argResults?.wasParsed(_kOut) ?? false) {
104
      outputFile = globals.fs.file(stringArgDeprecated(_kOut));
105
    }
Devon Carew's avatar
Devon Carew committed
106

107
    bool success = true;
108
    switch (stringArgDeprecated(_kType)) {
109
      case _kDeviceType:
110
        await runScreenshot(outputFile);
111
        break;
112
      case _kSkiaType:
113 114
        success = await runSkia(outputFile);
        break;
115
      case _kRasterizerType:
116 117
        success = await runRasterizer(outputFile);
        break;
Devon Carew's avatar
Devon Carew committed
118
    }
119

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

124
  Future<void> runScreenshot(File? outputFile) async {
125
    outputFile ??= globals.fsUtils.getUniqueFile(
126 127 128 129
      globals.fs.currentDirectory,
      'flutter',
      'png',
    );
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
    _showOutputFileInfo(outputFile);
Devon Carew's avatar
Devon Carew committed
136
  }
137

138
  Future<bool> runSkia(File? outputFile) async {
139
    final Uri observatoryUrl = Uri.parse(stringArgDeprecated(_kObservatoryUrl)!);
140
    final FlutterVmService vmService = await connectToVmService(observatoryUrl, logger: globals.logger);
141
    final vm_service.Response? skp = await vmService.screenshotSkp();
142 143 144 145 146 147 148
    if (skp == null) {
      globals.printError(
        'The Skia picture request failed, probably because the device was '
        'disconnected',
      );
      return false;
    }
149
    outputFile ??= globals.fsUtils.getUniqueFile(
150 151 152 153
      globals.fs.currentDirectory,
      'flutter',
      'skp',
    );
154
    final IOSink sink = outputFile.openWrite();
155
    sink.add(base64.decode(skp.json?['skp'] as String));
156
    await sink.close();
157 158
    _showOutputFileInfo(outputFile);
    _ensureOutputIsNotJsonRpcError(outputFile);
159
    return true;
160 161
  }

162
  Future<bool> runRasterizer(File? outputFile) async {
163
    final Uri observatoryUrl = Uri.parse(stringArgDeprecated(_kObservatoryUrl)!);
164
    final FlutterVmService vmService = await connectToVmService(observatoryUrl, logger: globals.logger);
165
    final vm_service.Response? response = await vmService.screenshot();
166 167 168 169 170 171 172
    if (response == null) {
      globals.printError(
        'The screenshot request failed, probably because the device was '
        'disconnected',
      );
      return false;
    }
173
    outputFile ??= globals.fsUtils.getUniqueFile(
174 175 176 177
      globals.fs.currentDirectory,
      'flutter',
      'png',
    );
178
    final IOSink sink = outputFile.openWrite();
179
    sink.add(base64.decode(response.json?['screenshot'] as String));
180
    await sink.close();
181 182
    _showOutputFileInfo(outputFile);
    _ensureOutputIsNotJsonRpcError(outputFile);
183
    return true;
184 185
  }

186 187 188 189 190 191 192 193 194
  void _ensureOutputIsNotJsonRpcError(File outputFile) {
    if (outputFile.lengthSync() >= 1000) {
      return;
    }
    final String content = outputFile.readAsStringSync(
      encoding: const AsciiCodec(allowInvalid: true),
    );
    if (content.startsWith('{"jsonrpc":"2.0", "error"')) {
      throwToolExit('It appears the output file contains an error message, not valid skia output.');
195 196 197
    }
  }

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