screengrab 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module Screengrab
2
+ class DetectValues
3
+ # This is needed to supply default values which are based on config values determined in the initial
4
+ # configuration pass
5
+ def self.set_additional_default_values
6
+ config = Screengrab.config
7
+
8
+ # First, try loading the Screengrabfile from the current directory
9
+ config.load_configuration_file(Screengrab.screengrabfile_name)
10
+
11
+ unless config[:tests_package_name]
12
+ config[:tests_package_name] = "#{config[:app_package_name]}.test"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,105 @@
1
+ require 'fastlane_core'
2
+ require 'credentials_manager'
3
+
4
+ module Screengrab
5
+ class Options
6
+ DEVICE_TYPES = ["phone", "sevenInch", "tenInch", "tv", "wear"].freeze
7
+
8
+ def self.available_options
9
+ @@options ||= [
10
+ FastlaneCore::ConfigItem.new(key: :android_home,
11
+ short_option: "-n",
12
+ optional: true,
13
+ default_value: ENV['ANDROID_HOME'] || ENV['ANDROID_SDK'],
14
+ description: "Path to the root of your Android SDK installation, e.g. ~/tools/android-sdk-macosx"),
15
+ FastlaneCore::ConfigItem.new(key: :build_tools_version,
16
+ short_option: "-i",
17
+ optional: true,
18
+ description: "The Android build tools version to use, e.g. '23.0.2'"),
19
+ FastlaneCore::ConfigItem.new(key: :locales,
20
+ description: "A list of locales which should be used",
21
+ short_option: "-q",
22
+ type: Array,
23
+ default_value: ['en-US']),
24
+ FastlaneCore::ConfigItem.new(key: :clear_previous_screenshots,
25
+ env_name: 'SCREENGRAB_CLEAR_PREVIOUS_SCREENSHOTS',
26
+ description: "Enabling this option will automatically clear previously generated screenshots before running screengrab",
27
+ default_value: false,
28
+ is_string: false),
29
+ FastlaneCore::ConfigItem.new(key: :output_directory,
30
+ short_option: "-o",
31
+ env_name: "SCREENGRAB_OUTPUT_DIRECTORY",
32
+ description: "The directory where to store the screenshots",
33
+ default_value: File.join("fastlane", "metadata", "android")),
34
+ FastlaneCore::ConfigItem.new(key: :skip_open_summary,
35
+ env_name: 'SCREENGRAB_SKIP_OPEN_SUMMARY',
36
+ description: "Don't open the summary after running `screengrab`",
37
+ default_value: false,
38
+ is_string: false),
39
+ FastlaneCore::ConfigItem.new(key: :app_package_name,
40
+ env_name: 'SCREENGRAB_APP_PACKAGE_NAME',
41
+ short_option: "-a",
42
+ description: "The package name of the app under test (e.g. com.yourcompany.yourapp)",
43
+ default_value: ENV["screengrab_APP_PACKAGE_NAME"] || CredentialsManager::AppfileConfig.try_fetch_value(:package_name)),
44
+ FastlaneCore::ConfigItem.new(key: :tests_package_name,
45
+ env_name: 'SCREENGRAB_TESTS_PACKAGE_NAME',
46
+ optional: true,
47
+ description: "The package name of the tests bundle (e.g. com.yourcompany.yourapp.test)"),
48
+ FastlaneCore::ConfigItem.new(key: :use_tests_in_packages,
49
+ env_name: 'SCREENGRAB_USE_TESTS_IN_PACKAGES',
50
+ optional: true,
51
+ short_option: "-p",
52
+ type: Array,
53
+ description: "Only run tests in these Java packages"),
54
+ FastlaneCore::ConfigItem.new(key: :use_tests_in_classes,
55
+ env_name: 'SCREENGRAB_USE_TESTS_IN_CLASSES',
56
+ optional: true,
57
+ short_option: "-l",
58
+ type: Array,
59
+ description: "Only run tests in these Java classes"),
60
+ FastlaneCore::ConfigItem.new(key: :test_instrumentation_runner,
61
+ env_name: 'SCREENGRAB_TEST_INSTRUMENTATION_RUNNER',
62
+ optional: true,
63
+ default_value: 'android.support.test.runner.AndroidJUnitRunner',
64
+ description: "The fully qualified class name of your test instrumentation runner"),
65
+ FastlaneCore::ConfigItem.new(key: :ending_locale,
66
+ env_name: 'SCREENGRAB_ENDING_LOCALE',
67
+ optional: true,
68
+ is_string: true,
69
+ default_value: 'en-US',
70
+ description: "Return the device to this locale after running tests"),
71
+ FastlaneCore::ConfigItem.new(key: :app_apk_path,
72
+ env_name: 'SCREENGRAB_APP_APK_PATH',
73
+ optional: true,
74
+ description: "The path to the APK for the app under test",
75
+ short_option: "-k",
76
+ default_value: Dir[File.join("app", "build", "outputs", "apk", "app-debug.apk")].last,
77
+ verify_block: proc do |value|
78
+ UI.user_error! "Could not find APK file at path '#{value}'" unless File.exist?(value)
79
+ end),
80
+ FastlaneCore::ConfigItem.new(key: :tests_apk_path,
81
+ env_name: 'SCREENGRAB_TESTS_APK_PATH',
82
+ optional: true,
83
+ description: "The path to the APK for the the tests bundle",
84
+ short_option: "-b",
85
+ default_value: Dir[File.join("app", "build", "outputs", "apk", "app-debug-androidTest-unaligned.apk")].last,
86
+ verify_block: proc do |value|
87
+ UI.user_error! "Could not find APK file at path '#{value}'" unless File.exist?(value)
88
+ end),
89
+ FastlaneCore::ConfigItem.new(key: :specific_device,
90
+ env_name: 'SCREENGRAB_SPECIFIC_DEVICE',
91
+ optional: true,
92
+ description: "Use the device or emulator with the given serial number or qualifier",
93
+ short_option: "-s"),
94
+ FastlaneCore::ConfigItem.new(key: :device_type,
95
+ env_name: 'SCREENGRAB_DEVICE_TYPE',
96
+ description: "Type of device used for screenshots. Matches Google Play Types (phone, sevenInch, tenInch, tv, wear)",
97
+ short_option: "-d",
98
+ default_value: "phone",
99
+ verify_block: proc do |value|
100
+ UI.user_error! "device_type must be one of: #{DEVICE_TYPES}" unless DEVICE_TYPES.include?(value)
101
+ end)
102
+ ]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,206 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>fastlane/screengrab</title>
5
+ <meta charset="UTF-8">
6
+ <style type="text/css">
7
+ * {
8
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
9
+ font-weight: 300;
10
+ }
11
+ .language {
12
+
13
+ }
14
+ .deviceName {
15
+ display: block;
16
+ font-size: 30px;
17
+ padding-bottom: 24px;
18
+ padding-top: 45px;
19
+ }
20
+ .screenshot {
21
+ cursor: pointer;
22
+ border: 1px #EEE solid;
23
+ z-index: 0;
24
+ }
25
+ th {
26
+ text-align: left;
27
+ }
28
+ td {
29
+ text-align: center;
30
+ min-width: 200px;
31
+ }
32
+ #overlay {
33
+ position:fixed;
34
+ top:0;
35
+ left:0;
36
+ background:rgba(0,0,0,0.8);
37
+ z-index:5;
38
+ width:100%;
39
+ height:100%;
40
+ display:none;
41
+ cursor: zoom-out;
42
+ text-align: center;
43
+ }
44
+ #imageDisplay {
45
+ height: auto;
46
+ width: auto;
47
+ z-index: 10;
48
+ cursor: pointer;
49
+ }
50
+ #imageInfo {
51
+ background: none repeat scroll 0 0 rgba(0, 0, 0, 0.2);
52
+ border-radius: 5px;
53
+ color: white;
54
+ margin: 20px;
55
+ padding: 10px;
56
+ position: absolute;
57
+ right: 0;
58
+ top: 0;
59
+ width: 250px;
60
+ z-index: -1;
61
+ }
62
+ #imageInfo:hover {
63
+ z-index: 20;
64
+ }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <% divide_size_by = 5.0 %>
69
+ <% max_width = 180 %>
70
+ <% image_counter = 0 %>
71
+
72
+
73
+ <% @data.each do |language, content| %>
74
+ <h1 class="language"><%= language %></h1>
75
+ <hr>
76
+ <table>
77
+ <% content.each do |device_name, screens| %>
78
+ <tr>
79
+ <th colspan="<%= screens.count %>">
80
+ <span class="deviceName"><%= device_name %></span>
81
+ </th>
82
+ </tr>
83
+ <tr>
84
+ <% screens.each do |screen_path| %>
85
+ <% next if screen_path.include?"_framed.png" %>
86
+ <td>
87
+ <% style = "width: 100%;" %>
88
+ <% image_counter += 1 %>
89
+ <a href="<%= screen_path %>" target="_blank" class="screenshotLink">
90
+ <img class="screenshot"
91
+ src="<%= screen_path %>"
92
+ style="<%= style %>"
93
+ alt="<%= language %> <%= device_name %>"
94
+ data-counter="<%= image_counter %>" />
95
+ </a>
96
+ </td>
97
+ <% end %>
98
+ </tr>
99
+ <% end %>
100
+ </table>
101
+ <% end %>
102
+ <div id="overlay">
103
+ <img id="imageDisplay" src="" alt="" />
104
+ <div id="imageInfo"></div>
105
+ </div>
106
+ <script type="text/javascript">
107
+ var overlay = document.getElementById('overlay');
108
+ var imageDisplay = document.getElementById('imageDisplay');
109
+ var imageInfo = document.getElementById('imageInfo');
110
+ var screenshotLink = document.getElementsByClassName('screenshotLink');
111
+
112
+ function doClick(el) {
113
+ if (document.createEvent) {
114
+ var evObj = document.createEvent('MouseEvents', true);
115
+ evObj.initMouseEvent("click", false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
116
+ el.dispatchEvent(evObj);
117
+ } else if (document.createEventObject) { //IE
118
+ var evObj = document.createEventObject();
119
+ el.fireEvent('onclick', evObj);
120
+ }
121
+ }
122
+
123
+ for (index = 0; index < screenshotLink.length; ++index) {
124
+ screenshotLink[index].addEventListener('click', function(e) {
125
+ e.preventDefault();
126
+
127
+ var img = e.target;
128
+ if (e.target.tagName == 'A') {
129
+ img = e.target.children[0];
130
+ }
131
+
132
+ // beautify
133
+ var tmpImg = new Image();
134
+ tmpImg.src = img.src;
135
+ imageDisplay.style.height = 'auto';
136
+ imageDisplay.style.width = 'auto';
137
+ if (window.innerHeight < tmpImg.height) {
138
+ imageDisplay.style.height = document.documentElement.clientHeight+'px';
139
+ } else if (window.innerWidth < tmpImg.width) {
140
+ imageDisplay.style.width = document.documentElement.clientWidth;+'px';
141
+ } else {
142
+ imageDisplay.style.paddingTop = parseInt((window.innerHeight - tmpImg.height) / 2)+'px';
143
+ }
144
+
145
+ imageDisplay.src = img.src;
146
+ imageDisplay.alt = img.alt;
147
+ imageDisplay.dataset.counter = img.dataset.counter;
148
+
149
+ imageInfo.innerHTML = '<h3>'+img.alt+'</h3>';
150
+ imageInfo.innerHTML += img.src.split("/").pop();
151
+ imageInfo.innerHTML += '<br />'+tmpImg.height+'&times;'+tmpImg.width+'px';
152
+
153
+ overlay.style.display = "block";
154
+ });
155
+ }
156
+
157
+ imageDisplay.addEventListener('click', function(e) {
158
+ e.stopPropagation(); // !
159
+
160
+ overlay.style.display = "none";
161
+
162
+ img_counter = parseInt(e.target.dataset.counter) + 1;
163
+ try {
164
+ link = document.body.querySelector('img[data-counter="'+img_counter+'"]').parentNode;
165
+ } catch (e) {
166
+ try {
167
+ link = document.body.querySelector('img[data-counter="0"]').parentNode;
168
+ } catch (e) {
169
+ return false;
170
+ }
171
+ }
172
+ doClick(link);
173
+ });
174
+
175
+ overlay.addEventListener('click', function(e) {
176
+ overlay.style.display = "none";
177
+ })
178
+
179
+ function keyPressed(e) {
180
+ e = e || window.event;
181
+ var charCode = e.keyCode || e.which;
182
+ switch(charCode) {
183
+ case 27: // Esc
184
+ overlay.style.display = "none";
185
+ break;
186
+ case 34: // right-arrow, Page Down, keypad right, ...
187
+ case 39:
188
+ case 54:
189
+ case 102:
190
+ e.preventDefault();
191
+ doClick(imageDisplay);
192
+ break;
193
+ case 33: // left-arrow, Page Up, keypad right, ...
194
+ case 37:
195
+ case 52:
196
+ case 100:
197
+ e.preventDefault();
198
+ document.getElementById('imageDisplay').dataset.counter -= 2; // hacky
199
+ doClick(imageDisplay);
200
+ break;
201
+ }
202
+ };
203
+ document.body.addEventListener('keydown', keyPressed);
204
+ </script>
205
+ </body>
206
+ </html>
@@ -0,0 +1,299 @@
1
+
2
+ module Screengrab
3
+ class Runner
4
+ NEEDED_PERMISSIONS = [
5
+ 'android.permission.READ_EXTERNAL_STORAGE',
6
+ 'android.permission.WRITE_EXTERNAL_STORAGE',
7
+ 'android.permission.CHANGE_CONFIGURATION'
8
+ ].freeze
9
+
10
+ attr_accessor :number_of_retries
11
+
12
+ def initialize(executor = FastlaneCore::CommandExecutor,
13
+ config = Screengrab.config,
14
+ android_env = Screengrab.android_environment)
15
+
16
+ @executor = executor
17
+ @config = config
18
+ @android_env = android_env
19
+ end
20
+
21
+ def run
22
+ FastlaneCore::PrintTable.print_values(config: @config, hide_keys: [], title: "Summary for screengrab #{Screengrab::VERSION}")
23
+
24
+ app_apk_path = @config.fetch(:app_apk_path, ask: false)
25
+ tests_apk_path = @config.fetch(:tests_apk_path, ask: false)
26
+ discovered_apk_paths = Dir[File.join('**', '*.apk')]
27
+
28
+ apk_paths_provided = app_apk_path && !app_apk_path.empty? && tests_apk_path && !tests_apk_path.empty?
29
+
30
+ unless apk_paths_provided || discovered_apk_paths.any?
31
+ UI.error 'No APK paths were provided and no APKs could be found'
32
+ UI.error "Please provide APK paths with 'app_apk_path' and 'tests_apk_path' and make sure you have assembled APKs prior to running this command."
33
+ return
34
+ end
35
+
36
+ test_classes_to_use = @config[:use_tests_in_classes]
37
+ test_packages_to_use = @config[:use_tests_in_packages]
38
+
39
+ if test_classes_to_use && test_classes_to_use.any? && test_packages_to_use && test_packages_to_use.any?
40
+ UI.error "'use_tests_in_classes' and 'use_tests_in_packages' can not be combined. Please use one or the other."
41
+ return
42
+ end
43
+
44
+ if (test_classes_to_use.nil? || test_classes_to_use.empty?) && (test_packages_to_use.nil? || test_packages_to_use.empty?)
45
+ UI.important 'Limiting the test classes run by `screengrab` to just those that generate screenshots can make runs faster.'
46
+ UI.important 'Consider using the :use_tests_in_classes or :use_tests_in_packages option, and organize your tests accordingly.'
47
+ end
48
+
49
+ device_type_dir_name = "#{@config[:device_type]}Screenshots"
50
+ clear_local_previous_screenshots(device_type_dir_name)
51
+
52
+ device_serial = select_device
53
+
54
+ device_screenshots_paths = [
55
+ determine_external_screenshots_path(device_serial),
56
+ determine_internal_screenshots_path
57
+ ]
58
+
59
+ clear_device_previous_screenshots(device_serial, device_screenshots_paths)
60
+
61
+ app_apk_path ||= select_app_apk(discovered_apk_paths)
62
+ tests_apk_path ||= select_tests_apk(discovered_apk_paths)
63
+
64
+ validate_apk(app_apk_path)
65
+
66
+ install_apks(device_serial, app_apk_path, tests_apk_path)
67
+
68
+ grant_permissions(device_serial)
69
+
70
+ run_tests(device_serial, test_classes_to_use, test_packages_to_use)
71
+
72
+ number_of_screenshots = pull_screenshots_from_device(device_serial, device_screenshots_paths, device_type_dir_name)
73
+
74
+ open_screenshots_summary(device_type_dir_name)
75
+
76
+ UI.success "Captured #{number_of_screenshots} screenshots! 📷✨"
77
+ end
78
+
79
+ def select_device
80
+ devices = @executor.execute(command: "adb devices -l", print_all: true, print_command: true).split("\n")
81
+ # the first output by adb devices is "List of devices attached" so remove that and any adb startup output
82
+ # devices.reject! { |d| d.include?("List of devices attached") || d.include?("* daemon") || d.include?("unauthorized") || d.include?("offline") }
83
+ devices.reject! do |device|
84
+ ['unauthorized', 'offline', '* daemon', 'List of devices attached'].any? { |status| device.include? status }
85
+ end
86
+
87
+ UI.user_error! 'There are no connected and authorized devices or emulators' if devices.empty?
88
+
89
+ devices.select! { |d| d.include?(@config[:specific_device]) } if @config[:specific_device]
90
+
91
+ UI.user_error! "No connected devices matched your criteria: #{@config[:specific_device]}" if devices.empty?
92
+
93
+ if devices.length > 1
94
+ UI.important "Multiple connected devices, selecting the first one"
95
+ UI.important "To specify which connected device to use, use the -s (specific_device) config option"
96
+ end
97
+
98
+ # grab the serial number. the lines of output can look like these:
99
+ # 00c22d4d84aec525 device usb:2148663295X product:bullhead model:Nexus_5X device:bullhead
100
+ # 192.168.1.100:5555 device usb:2148663295X product:bullhead model:Nexus_5X device:genymotion
101
+ # emulator-5554 device usb:2148663295X product:bullhead model:Nexus_5X device:emulator
102
+ devices[0].match(/^\S+/)[0]
103
+ end
104
+
105
+ def select_app_apk(discovered_apk_paths)
106
+ UI.important "To not be asked about this value, you can specify it using 'app_apk_path'"
107
+ UI.select('Select your debug app APK', discovered_apk_paths)
108
+ end
109
+
110
+ def select_tests_apk(discovered_apk_paths)
111
+ UI.important "To not be asked about this value, you can specify it using 'tests_apk_path'"
112
+ UI.select('Select your debug tests APK', discovered_apk_paths)
113
+ end
114
+
115
+ def clear_local_previous_screenshots(device_type_dir_name)
116
+ if @config[:clear_previous_screenshots]
117
+ UI.message "Clearing #{device_type_dir_name} within #{@config[:output_directory]}"
118
+
119
+ # We'll clear the temporary directory where screenshots wind up after being pulled from
120
+ # the device as well, in case those got stranded on a previous run/failure
121
+ ['screenshots', device_type_dir_name].each do |dir_name|
122
+ files = screenshot_file_names_in(@config[:output_directory], dir_name)
123
+ File.delete(*files)
124
+ end
125
+ end
126
+ end
127
+
128
+ def screenshot_file_names_in(output_directory, device_type)
129
+ Dir.glob(File.join('.', output_directory, '**', device_type, '*.png'), File::FNM_CASEFOLD)
130
+ end
131
+
132
+ def determine_external_screenshots_path(device_serial)
133
+ device_ext_storage = @executor.execute(command: "adb -s #{device_serial} shell echo \\$EXTERNAL_STORAGE",
134
+ print_all: true,
135
+ print_command: true)
136
+ File.join(device_ext_storage, @config[:app_package_name], 'screengrab')
137
+ end
138
+
139
+ def determine_internal_screenshots_path
140
+ "/data/data/#{@config[:app_package_name]}/app_screengrab"
141
+ end
142
+
143
+ def clear_device_previous_screenshots(device_serial, device_screenshots_paths)
144
+ UI.message 'Cleaning screenshots on device'
145
+
146
+ device_screenshots_paths.each do |device_path|
147
+ if_device_path_exists(device_serial, device_path) do |path|
148
+ @executor.execute(command: "adb -s #{device_serial} shell rm -rf #{path}",
149
+ print_all: true,
150
+ print_command: true)
151
+ end
152
+ end
153
+ end
154
+
155
+ def validate_apk(app_apk_path)
156
+ unless @android_env.aapt_path
157
+ UI.important "The `aapt` command could not be found on your system, so your app APK could not be validated"
158
+ return
159
+ end
160
+
161
+ UI.message 'Validating app APK'
162
+ apk_permissions = @executor.execute(command: "#{@android_env.aapt_path} dump permissions #{app_apk_path}",
163
+ print_all: true,
164
+ print_command: true)
165
+
166
+ missing_permissions = NEEDED_PERMISSIONS.reject { |needed| apk_permissions.include?(needed) }
167
+
168
+ if missing_permissions.any?
169
+ UI.user_error! "The needed permission(s) #{missing_permissions.join(', ')} could not be found in your app APK"
170
+ end
171
+ end
172
+
173
+ def install_apks(device_serial, app_apk_path, tests_apk_path)
174
+ UI.message 'Installing app APK'
175
+ apk_install_output = @executor.execute(command: "adb -s #{device_serial} install -r #{app_apk_path}",
176
+ print_all: true,
177
+ print_command: true)
178
+ UI.user_error! "App APK could not be installed" if apk_install_output.include?("Failure [")
179
+
180
+ UI.message 'Installing tests APK'
181
+ apk_install_output = @executor.execute(command: "adb -s #{device_serial} install -r #{tests_apk_path}",
182
+ print_all: true,
183
+ print_command: true)
184
+ UI.user_error! "Tests APK could not be installed" if apk_install_output.include?("Failure [")
185
+ end
186
+
187
+ def grant_permissions(device_serial)
188
+ UI.message 'Granting the permission necessary to change locales on the device'
189
+ @executor.execute(command: "adb -s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.CHANGE_CONFIGURATION",
190
+ print_all: true,
191
+ print_command: true)
192
+
193
+ device_api_version = @executor.execute(command: "adb -s #{device_serial} shell getprop ro.build.version.sdk",
194
+ print_all: true,
195
+ print_command: true).to_i
196
+
197
+ if device_api_version >= 23
198
+ UI.message 'Granting the permissions necessary to access device external storage'
199
+ @executor.execute(command: "adb -s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.WRITE_EXTERNAL_STORAGE",
200
+ print_all: true,
201
+ print_command: true)
202
+ @executor.execute(command: "adb -s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.READ_EXTERNAL_STORAGE",
203
+ print_all: true,
204
+ print_command: true)
205
+ end
206
+ end
207
+
208
+ def run_tests(device_serial, test_classes_to_use, test_packages_to_use)
209
+ @config[:locales].each do |locale|
210
+ UI.message "Running tests for locale: #{locale}"
211
+
212
+ instrument_command = ["adb -s #{device_serial} shell am instrument --no-window-animation -w",
213
+ "-e testLocale #{locale.tr('-', '_')}",
214
+ "-e endingLocale #{@config[:ending_locale].tr('-', '_')}"]
215
+ instrument_command << "-e class #{test_classes_to_use.join(',')}" if test_classes_to_use
216
+ instrument_command << "-e package #{test_packages_to_use.join(',')}" if test_packages_to_use
217
+ instrument_command << "#{@config[:tests_package_name]}/#{@config[:test_instrumentation_runner]}"
218
+
219
+ test_output = @executor.execute(command: instrument_command.join(" \\\n"),
220
+ print_all: true,
221
+ print_command: true)
222
+
223
+ UI.user_error! "Tests failed" if test_output.include?("FAILURES!!!")
224
+ end
225
+ end
226
+
227
+ def pull_screenshots_from_device(device_serial, device_screenshots_paths, device_type_dir_name)
228
+ UI.message "Pulling captured screenshots from the device"
229
+ starting_screenshot_count = screenshot_file_names_in(@config[:output_directory], device_type_dir_name).length
230
+
231
+ device_screenshots_paths.each do |device_path|
232
+ if_device_path_exists(device_serial, device_path) do |path|
233
+ @executor.execute(command: "adb -s #{device_serial} pull #{path} #{@config[:output_directory]}",
234
+ print_all: false,
235
+ print_command: true)
236
+ end
237
+ end
238
+
239
+ # The SDK can't 100% determine what kind of device it is running on relative to the categories that
240
+ # supply and Google Play care about (phone, 7" tablet, TV, etc.).
241
+ #
242
+ # Therefore, we'll move the pulled screenshots from their genericly named folder to one named by the
243
+ # user provided device_type option value to match the directory structure that supply expects
244
+ move_pulled_screenshots(device_type_dir_name)
245
+
246
+ ending_screenshot_count = screenshot_file_names_in(@config[:output_directory], device_type_dir_name).length
247
+
248
+ # Because we can't guarantee the screenshot output directory will be empty when we pull, we determine
249
+ # success based on whether there are more screenshots there than when we started.
250
+ if starting_screenshot_count == ending_screenshot_count
251
+ UI.error "Make sure you've used Screengrab.screenshot() in your tests and that your expected tests are being run."
252
+ UI.user_error! "No screenshots were detected 📷❌"
253
+ end
254
+
255
+ ending_screenshot_count - starting_screenshot_count
256
+ end
257
+
258
+ def move_pulled_screenshots(device_type_dir_name)
259
+ # Glob pattern that finds the pulled screenshots directory for each locale
260
+ # (Matches: fastlane/metadata/android/en-US/images/screenshots)
261
+ screenshots_dir_pattern = File.join('.', @config[:output_directory], '**', "screenshots")
262
+
263
+ Dir.glob(screenshots_dir_pattern, File::FNM_CASEFOLD).each do |screenshots_dir|
264
+ src_screenshots = Dir.glob(File.join(screenshots_dir, '*.png'), File::FNM_CASEFOLD)
265
+
266
+ # We move the screenshots by replacing the last segment of the screenshots directory path with
267
+ # the device_type specific name
268
+ #
269
+ # (Moved to: fastlane/metadata/android/en-US/images/phoneScreenshots)
270
+ dest_dir = File.join(File.dirname(screenshots_dir), device_type_dir_name)
271
+
272
+ FileUtils.mkdir_p(dest_dir)
273
+ FileUtils.cp_r(src_screenshots, dest_dir)
274
+ FileUtils.rm_r(screenshots_dir)
275
+ UI.success "Screenshots copied to #{dest_dir}"
276
+ end
277
+ end
278
+
279
+ # Some device commands fail if executed against a device path that does not exist, so this helper method
280
+ # provides a way to conditionally execute a block only if the provided path exists on the device.
281
+ def if_device_path_exists(device_serial, device_path)
282
+ return if @executor.execute(command: "adb -s #{device_serial} shell ls #{device_path}",
283
+ print_all: false,
284
+ print_command: false).include?('No such file')
285
+
286
+ yield device_path
287
+ end
288
+
289
+ def open_screenshots_summary(device_type_dir_name)
290
+ unless @config[:skip_open_summary]
291
+ UI.message "Opening screenshots summary"
292
+ # MCF: this isn't OK on any platform except Mac
293
+ @executor.execute(command: "open #{@config[:output_directory]}/*/images/#{device_type_dir_name}/*.png",
294
+ print_all: false,
295
+ print_command: true)
296
+ end
297
+ end
298
+ end
299
+ end