adb_driver 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc283d720879afb304052b394d127b396c971ff784cb0d6dab39051cc50f88c7
4
+ data.tar.gz: ad04b403c6f63cce562a78150d7db53f7b8eabb8e89d5aeb1d27732377c3e6ba
5
+ SHA512:
6
+ metadata.gz: 790517ac22172520ee9b870cc5a290ae7cb32668b2a5e9be231db9996d9a51f0a5733bbf2245a5439c942b7603977bb0954b38adaf35bfd6f9d38b6a39869581
7
+ data.tar.gz: d446dd2027f102329d4c00ac0f432e476eb965136a6328681830bc714ec92c571e5dca6fc5ae2ac8e5d8f4251cf2abd5c4d4c21c9e136291acde77a6d2c52a73
data/lib/adb_driver.rb ADDED
@@ -0,0 +1,11 @@
1
+ require_relative 'adb_driver/wait'
2
+
3
+ require_relative 'adb_driver/element'
4
+ require_relative 'adb_driver/finder'
5
+ require_relative 'adb_driver/driver'
6
+ require_relative 'adb_driver/navigation'
7
+ require_relative 'adb_driver/error'
8
+
9
+ require_relative 'adb_driver/dsl'
10
+ require_relative 'adb_driver/source_cleaner'
11
+ require_relative 'adb_driver/adb'
@@ -0,0 +1,123 @@
1
+ require 'timeout'
2
+
3
+ module Adb
4
+ extend Timeout
5
+ extend self
6
+
7
+ def execute_command(command, timeout_in_seconds = 10)
8
+ timeout(timeout_in_seconds) { `adb #{command}` }
9
+ end
10
+
11
+ def app_version(package_name)
12
+ output = execute_command("shell dumpsys package #{package_name}")
13
+ version_line = output.lines.grep(/versionName/).first.strip
14
+ version_line[/=(.*)/, 1]
15
+ end
16
+
17
+ def package_exists?(package_name)
18
+ execute_command('shell pm list packages').lines.grep(/#{package_name}\s*$/).one?
19
+ end
20
+
21
+ def android_5_or_greater?
22
+ android_version[0].to_i >= 5
23
+ end
24
+
25
+ def android_6_or_greater?
26
+ android_version[0].to_i >= 6
27
+ end
28
+
29
+ def android_6?
30
+ android_version.start_with?('6')
31
+ end
32
+
33
+ def android_7?
34
+ android_version.start_with?('7')
35
+ end
36
+
37
+ def real_device?
38
+ output = execute_command('devices -l')
39
+ output = output.lines.grep(/#{ENV['ANDROID_SERIAL']}/).first
40
+ output.include?('usb')
41
+ end
42
+
43
+ def emulator?
44
+ !real_device?
45
+ end
46
+
47
+ def samsung?
48
+ brand == 'samsung'
49
+ end
50
+
51
+ def htc?
52
+ brand == 'htc'
53
+ end
54
+
55
+ def asus?
56
+ brand == 'asus'
57
+ end
58
+
59
+ def lenovo?
60
+ brand == 'Lenovo'
61
+ end
62
+
63
+ def density
64
+ @density ||= execute_command('shell getprop ro.sf.lcd_density').to_i
65
+ end
66
+
67
+ def brand
68
+ execute_command('shell getprop ro.product.brand').strip
69
+ end
70
+
71
+ def model
72
+ execute_command('shell getprop ro.product.model').strip
73
+ end
74
+
75
+ def remove_package(name)
76
+ return unless package_exists?(name)
77
+ `adb uninstall #{name}`
78
+ fail "#{name} package wasn't removed" if package_exists?(name)
79
+ end
80
+
81
+ def restart_adb
82
+ `adb kill-server; adb start-server`
83
+ end
84
+
85
+ def portrait?
86
+ @orientation_enum ||= `adb shell dumpsys input`.lines.find { |l| l =~ /SurfaceOrientation/ }.strip[-1].to_i
87
+ @orientation_enum.even?
88
+ end
89
+
90
+ def connected_device_udid
91
+ case
92
+ when devices.none? then fail('No devices detected')
93
+ when devices.one? then devices.first.udid
94
+ when devices.count > 1 && emulators.none? then fail('Several devices detected. Set ANDROID_SERIAL to pick one')
95
+ when devices.count > 1 && emulators.one? then emulators.first.udid
96
+ when devices.count > 1 && emulators.count > 1 then fail('Several emulators detected. Close all but one')
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def android_version
103
+ execute_command('shell getprop ro.build.version.release').lines.last.strip
104
+ end
105
+
106
+ def devices
107
+ @device_list ||= begin
108
+ execute_command('devices -l').lines.grep(/model/).inject([]) do |dl, device_string|
109
+ dl << Device.new(device_string[/^\S+/])
110
+ end
111
+ end
112
+ end
113
+
114
+ def emulators
115
+ @emulators_list ||= begin
116
+ execute_command('devices -l').lines.grep(/model/).grep_v(/usb/).inject([]) do |dl, device_string|
117
+ dl << Device.new(device_string[/^\S+/])
118
+ end
119
+ end
120
+ end
121
+
122
+ Device = Struct.new(:udid)
123
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+
3
+ module AdbDriver
4
+ class Driver
5
+ include Finder
6
+ include Wait
7
+
8
+ SCREENSHOT_TIMEOUT = 5
9
+
10
+ attr_reader :logger
11
+
12
+ def initialize
13
+ @logger = Logger.new('adb_driver.log')
14
+ @logger.level = Logger::DEBUG
15
+ @logger.info 'Initializing Adb driver'
16
+ end
17
+
18
+ def save_screenshot(filepath)
19
+ if Adb.android_5_or_greater?
20
+ wait(SCREENSHOT_TIMEOUT) { `adb exec-out screencap -p > #{filepath}` }
21
+ else
22
+ wait(SCREENSHOT_TIMEOUT) { `adb shell screencap -p /sdcard/screenshot.png; adb pull /sdcard/screenshot.png #{filepath}` }
23
+ end
24
+ rescue Wait::Error => e
25
+ raise e.class, 'Cannot take a screenshot'
26
+ end
27
+
28
+ def navigate
29
+ @navigation ||= Navigation.new
30
+ end
31
+
32
+ def quit
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,225 @@
1
+ module DSL
2
+ def active?
3
+ new.active?
4
+ end
5
+
6
+ def button(name, options)
7
+ define_active_method(name) if options.delete(:distinctive)
8
+ define_scroll_to_bottom_method(name) if options.delete(:bottom)
9
+ wait_for_class = options.delete(:wait_for_class)
10
+ wait_for_class_timeout = options.delete(:wait_for_class_timeout)
11
+ index = options.delete(:index)
12
+
13
+ locator = options
14
+ define_name_method(name, wait_for_class, wait_for_class_timeout)
15
+ define_query_method(name, locator, index)
16
+ define_button_method(name, locator, index)
17
+ end
18
+
19
+ def buttons(locator, &block)
20
+ ButtonsBuilder.new(locator, &block)
21
+ end
22
+
23
+ def radio_buttons(locator, &block)
24
+ ButtonsBuilder.new(locator, &block)
25
+ define_selected_method
26
+ end
27
+
28
+ def text_field(name, options)
29
+ define_active_method(name) if options.delete(:distinctive)
30
+
31
+ locator = options
32
+ define_text_field_method(name, locator)
33
+ define_query_method(name, locator)
34
+ define_setter(name, locator)
35
+ define_getter(name, locator)
36
+ end
37
+
38
+ def switch(name, options)
39
+ define_active_method(name) if options.delete(:distinctive)
40
+ wait_for_class = options.delete(:wait_for_class)
41
+
42
+ locator = options
43
+ define_query_method(name, locator)
44
+ define_switch_methods(name, locator, wait_for_class)
45
+ end
46
+ alias checkbox switch
47
+
48
+ def view(name, options)
49
+ define_active_method(name) if options.delete(:distinctive)
50
+
51
+ locator = options
52
+ define_query_method(name, locator)
53
+ define_view_method(name, locator)
54
+ end
55
+
56
+ private
57
+
58
+ def define_active_method(name)
59
+ define_method(:active?) do
60
+ send("#{name}?")
61
+ end
62
+ end
63
+
64
+ def define_scroll_to_bottom_method(name)
65
+ define_method(:scroll_to_bottom) do
66
+ start = Time.now
67
+ until send("#{name}?")
68
+ fling_down
69
+ sleep 0.1
70
+ fail "Unable to scroll to the bottom (#{name} button)" if Time.now > start + 60
71
+ end
72
+ end
73
+ end
74
+
75
+ def define_name_method(name, wait_for_class, wait_for_class_timeout)
76
+ wait_for_class_timeout = 10 unless wait_for_class_timeout
77
+
78
+ define_method(name) do
79
+ send("#{name}_button").click
80
+ if wait_for_class
81
+ wait(wait_for_class_timeout, "Screen #{wait_for_class} hasn't became active") do
82
+ self.class.const_get(wait_for_class).active?
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def define_text_field_method(name, locator)
89
+ define_method("#{name}_text_field") do
90
+ begin
91
+ find_element(locator)
92
+ rescue Selenium::WebDriver::Error::NoSuchElementError => e
93
+ fail e.class, "'#{name}' text field cannot be found using #{locator}", caller.reject { |line| line =~ /#{__FILE__}/ }
94
+ end
95
+ end
96
+ end
97
+
98
+ def define_query_method(name, locator, index = nil)
99
+ define_method("#{name}?") do
100
+ if index
101
+ !!find_elements(locator)[index] || false
102
+ else
103
+ begin
104
+ !!find_element(locator)
105
+ rescue Selenium::WebDriver::Error::NoSuchElementError
106
+ false
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ def define_button_method(name, locator, index)
113
+ define_method("#{name}_button") do
114
+ if index
115
+ button = find_elements(locator)[index]
116
+ unless button
117
+ fail Selenium::WebDriver::Error::NoSuchElementError,
118
+ "'#{name}' button cannot be found using #{locator} with index #{index}",
119
+ caller.reject { |line| line =~ /#{__FILE__}/ }
120
+ end
121
+ button
122
+ else
123
+ begin
124
+ find_element(locator)
125
+ rescue Selenium::WebDriver::Error::NoSuchElementError => e
126
+ fail e.class, "'#{name}' button cannot be found using #{locator}", caller.reject { |line| line =~ /#{__FILE__}/ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def define_view_method(name, locator)
133
+ define_method("#{name}_view") do
134
+ begin
135
+ find_element(locator)
136
+ rescue Selenium::WebDriver::Error::NoSuchElementError
137
+ fail %(Element "#{name}" with locator "#{locator}" wasn't found)
138
+ end
139
+ end
140
+ end
141
+
142
+ def define_setter(name, locator)
143
+ define_method("#{name}=") do |text|
144
+ find_element(locator).send_keys text
145
+ end
146
+ end
147
+
148
+ def define_getter(name, locator, index = nil)
149
+ define_method(name) do
150
+ if index
151
+ find_elements(locator)[index].name
152
+ else
153
+ find_element(locator).name
154
+ end
155
+ end
156
+ end
157
+
158
+ def define_switch_methods(name, locator, wait_for_class)
159
+ define_method("#{name}_switch") do
160
+ find_element(locator)
161
+ end
162
+
163
+ define_method("toggle_#{name}") do
164
+ find_element(locator).click
165
+ end
166
+
167
+ define_method("#{name}_on?") do
168
+ find_element(locator).attribute(:checked) == 'true'
169
+ end
170
+ alias_method "#{name}_selected?".to_sym, "#{name}_on?".to_sym
171
+
172
+ define_method("turn_on_#{name}") do
173
+ unless send("#{name}_on?")
174
+ send("toggle_#{name}")
175
+ if wait_for_class
176
+ wait(5, "Screen #{wait_for_class} hasn't became active") do
177
+ Module.const_get(wait_for_class).active?
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ define_method("turn_off_#{name}") do
184
+ if send("#{name}_on?")
185
+ send("toggle_#{name}")
186
+ if wait_for_class
187
+ wait(5, "Screen #{wait_for_class} hasn't became active") do
188
+ Module.const_get(wait_for_class).active?
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def define_selected_method
196
+ button_methods = instance_methods.grep(/_button/)
197
+ define_method(:selected) do
198
+ result = button_methods.map do |button_method|
199
+ send(button_method).checked? && button_method.to_s.sub(/_button/, '').to_sym
200
+ end
201
+ result.grep_v(false).first
202
+ end
203
+ end
204
+
205
+ class ButtonsBuilder
206
+ def initialize(locator, &block)
207
+ @locator = locator
208
+ @block = block
209
+ @index = 0
210
+ instance_exec(&block)
211
+ end
212
+
213
+ def method_missing(method, *args)
214
+ button_name = args[0]
215
+ button_params = args[1] || {}
216
+
217
+ unless button_params.key?(:index)
218
+ button_params[:index] = @index
219
+ @index += 1
220
+ end
221
+
222
+ @block.binding.receiver.button button_name, @locator.merge(button_params)
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,72 @@
1
+ class Element
2
+ Point = Struct.new(:x, :y)
3
+ Size = Struct.new(:width, :height)
4
+
5
+ def initialize(xml_representation)
6
+ @xml_representation = xml_representation
7
+ end
8
+
9
+ def click
10
+ `adb shell input tap #{center.x} #{center.y}`
11
+ end
12
+
13
+ def long_click
14
+ `adb shell input swipe #{center.x} #{center.y} #{center.x} #{center.y} 800`
15
+ end
16
+
17
+ def send_keys(text)
18
+ click
19
+ sleep 2
20
+ text.scan(/.{1,10}/).each do |part|
21
+ `adb shell "input text '#{part}'"`
22
+ end
23
+ end
24
+
25
+ def text
26
+ @xml_representation.attributes['text']
27
+ end
28
+
29
+ def checked?
30
+ @xml_representation.attributes['checked'] == 'true'
31
+ end
32
+
33
+ def content_desc
34
+ @xml_representation.attributes['content-desc']
35
+ end
36
+
37
+ def top_left
38
+ Point.new(coordinates[0], coordinates[1])
39
+ end
40
+ alias_method :location, :top_left
41
+
42
+ def lower_right
43
+ Point.new(coordinates[2], coordinates[3])
44
+ end
45
+
46
+ def width
47
+ lower_right.x - top_left.x
48
+ end
49
+
50
+ def height
51
+ lower_right.y - top_left.y
52
+ end
53
+
54
+ def center
55
+ Point.new(top_left.x + width / 2, top_left.y + height / 2)
56
+ end
57
+
58
+ def size
59
+ Size.new(width, height)
60
+ end
61
+
62
+ def class_name
63
+ @xml_representation.attributes['class']
64
+ end
65
+
66
+ private
67
+
68
+ def coordinates
69
+ @coordinates ||= @xml_representation.attributes['bounds'].scan(/\d+/).map(&:to_i)
70
+ end
71
+ end
72
+
@@ -0,0 +1,16 @@
1
+ unless defined?(Selenium)
2
+ module Selenium
3
+ module WebDriver
4
+ module Error
5
+ class NoSuchElementError < StandardError; end
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ module AdbDriver
12
+ module Error
13
+ class NoSuchElementError < Selenium::WebDriver::Error::NoSuchElementError; end
14
+ class TimeOutError < StandardError; end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ module AdbDriver
2
+ module Finder
3
+ include Wait
4
+
5
+ FIND_ELEMENT_TIMEOUT = 1
6
+ PAGE_SOURCE_TIMEOUT = 15
7
+ MIN_TIME_BETWEEN_FIND_ATTEMPTS = 0.250
8
+
9
+ def find_element(locator)
10
+ wait(FIND_ELEMENT_TIMEOUT) { find_elements(locator).first }
11
+ rescue Wait::Error
12
+ logger.info "Element with locator '#{locator}' has not been found"
13
+ logger.debug "Current hierarchy: #{@last_view_hierarchy}"
14
+ raise Error::NoSuchElementError
15
+ end
16
+
17
+ def find_elements(locator)
18
+ logger.info "Searching for element by: #{locator}"
19
+
20
+ locator_type = locator.first.first
21
+ locator_value = locator.first.last
22
+
23
+ case locator_type
24
+ when :xpath then find_elements_by_xpath(locator_value)
25
+ when :id then find_elements_by_xpath("//*[contains(@resource-id,'#{locator_value}')]")
26
+ when :class_name then find_elements_by_xpath("//*[@class='#{locator_value}']")
27
+ when :content_desc then find_elements_by_xpath("//*[@content-desc='#{locator_value}']")
28
+ end
29
+ end
30
+
31
+ def find_elements_by_xpath(xpath)
32
+ sleep 0.05 until Time.now >= (@last_find_attempt_time ||= Time.now) + MIN_TIME_BETWEEN_FIND_ATTEMPTS
33
+
34
+ view_hierarchy = REXML::Document.new(page_source)
35
+ @last_view_hierarchy = view_hierarchy
36
+
37
+ result = view_hierarchy.get_elements(xpath).map { |element| Element.new(element) }
38
+ @last_find_attempt_time = Time.now
39
+ logger.debug "Returning element(s): #{result}"
40
+ result
41
+ end
42
+
43
+ def page_source
44
+ adb_command = Adb.android_7? ? 'exec-out uiautomator dump /dev/tty' : 'shell uiautomator dump /dev/tty'
45
+ logger.info "Getting view hierarchy"
46
+ result = Adb.execute_command(adb_command, PAGE_SOURCE_TIMEOUT)
47
+
48
+ if result.empty? || result.strip == 'Killed'
49
+ logger.info "Result is empty or 'Killed'. Restarting adb..."
50
+ Adb.restart_adb
51
+ logger.info "Adb restarted. Getting result..."
52
+ result = Adb.execute_command(adb_command, PAGE_SOURCE_TIMEOUT)
53
+ end
54
+
55
+ logger.debug "Result received: #{result}"
56
+ result
57
+ rescue Wait::Error
58
+ raise TimeOutError, "Couldn't get page_source in reasonable time (#{PAGE_SOURCE_TIMEOUT} seconds)"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ module AdbDriver
2
+ class Navigation
3
+ def back
4
+ `adb shell input keyevent KEYCODE_BACK`
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,60 @@
1
+ require 'rexml/document'
2
+ require 'rexml/sax2listener'
3
+ require 'rexml/parsers/sax2parser'
4
+
5
+ module SourceCleaner
6
+ def self.call(page_source)
7
+ p = REXML::Parsers::SAX2Parser.new(page_source)
8
+ p.listen(MrProper.new)
9
+ p.parse
10
+ end
11
+
12
+ class MrProper
13
+ include REXML::SAX2Listener
14
+
15
+ BORING_ATTRS = %w(class index package
16
+ checkable checked clickable
17
+ enabled focusable focused
18
+ scrollable long-clickable password
19
+ selected bounds instance)
20
+
21
+ INDENT_WIDTH = 2
22
+ ATTRIBUTES_INDENT = 4
23
+
24
+ def initialize
25
+ @indent = 0
26
+ end
27
+
28
+ def start_element(uri, localname, qname, attributes)
29
+ return if localname == 'hierarchy'
30
+
31
+ @indent += 1
32
+
33
+ node = ' ' * INDENT_WIDTH * @indent
34
+ node << "class_name: '#{attributes['class']}'"
35
+
36
+ clean_attributes = attributes.delete_if { |k, v| BORING_ATTRS.include?(k) || v.empty? }
37
+
38
+ if value = clean_attributes.delete('resource-id')
39
+ clean_attributes[:id] = value
40
+ end
41
+
42
+ if value = clean_attributes.delete('content-desc')
43
+ clean_attributes[:content_desc] = value
44
+ end
45
+
46
+ unless clean_attributes.empty?
47
+ node << ' ' * ATTRIBUTES_INDENT
48
+ node << clean_attributes.map { |k, v| "#{k}: '#{v}'" }.join(', ')
49
+ end
50
+
51
+ puts node
52
+ end
53
+
54
+ def end_element(uri, localname, qname)
55
+ @indent -= 1
56
+ end
57
+ end
58
+ end
59
+
60
+ SourceCleaner.call(ARGF.read) if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,24 @@
1
+ module Wait
2
+ Error = Class.new(StandardError)
3
+
4
+ def self.included(mod)
5
+ mod.extend self
6
+ end
7
+
8
+ def wait(duration = 10, message = nil, &block)
9
+ polling_interaval = 0.1 # 100 msec
10
+ start = Time.now
11
+
12
+ loop do
13
+ sleep polling_interaval
14
+
15
+ result = block.call
16
+ break result if result
17
+
18
+ next if Time.now < start + duration
19
+
20
+ cleaned_stacktrace = caller.reverse.take_while { |line| line !~ /#{__FILE__}.*#{__method__}/ }.reverse
21
+ raise Error, (message || 'Wait::Error'), cleaned_stacktrace
22
+ end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adb_driver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Anton Petrov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A driver with Seleium-like interface to interact with Android devices
14
+ directly via ADB
15
+ email: givemeletter@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/adb_driver.rb
21
+ - lib/adb_driver/adb.rb
22
+ - lib/adb_driver/driver.rb
23
+ - lib/adb_driver/dsl.rb
24
+ - lib/adb_driver/element.rb
25
+ - lib/adb_driver/error.rb
26
+ - lib/adb_driver/finder.rb
27
+ - lib/adb_driver/navigation.rb
28
+ - lib/adb_driver/source_cleaner.rb
29
+ - lib/adb_driver/wait.rb
30
+ homepage: https://github.com/sofaking/adb_driver
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.7.4
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: A driver with Seleium-like interface to interact with Android devices directly
54
+ via ADB
55
+ test_files: []