class Calabash::Android::Operations::Device

Attributes

app_path[R]
serial[R]
server_port[R]
test_server_path[R]
test_server_port[R]

Public Class Methods

new(cucumber_world, serial, server_port, app_path, test_server_path, test_server_port = 7102) click to toggle source
# File lib/calabash-android/operations.rb, line 319
def initialize(cucumber_world, serial, server_port, app_path, test_server_path, test_server_port = 7102)

  @cucumber_world = cucumber_world
  @serial = serial || default_serial
  @server_port = server_port || default_server_port
  @app_path = app_path
  @test_server_path = test_server_path
  @test_server_port = test_server_port

  forward_cmd = "#{adb_command} forward tcp:#{@server_port} tcp:#{@test_server_port}"
  calabash_log forward_cmd
  calabash_log `#{forward_cmd}`
end

Public Instance Methods

_sdk_version() click to toggle source
# File lib/calabash-android/operations.rb, line 333
def _sdk_version
  `#{adb_command} shell getprop ro.build.version.sdk`.to_i
end
adb_command() click to toggle source
# File lib/calabash-android/operations.rb, line 631
def adb_command
  "\"#{Calabash::Android::Dependencies.adb_path}\" -s #{serial}"
end
app_running?() click to toggle source
# File lib/calabash-android/operations.rb, line 436
def app_running?
  begin
    http("/ping") == "pong"
  rescue
    false
  end
end
application_installed?(package_name) click to toggle source
# File lib/calabash-android/operations.rb, line 432
def application_installed?(package_name)
  (`#{adb_command} shell pm list packages`.lines.map{|line| line.chomp.sub("package:", "")}.include?(package_name))
end
clear_app_data() click to toggle source
# File lib/calabash-android/operations.rb, line 688
def clear_app_data
  unless application_installed?(package_name(@app_path))
    raise "Cannot clear data, application #{package_name(@app_path)} is not installed"
  end

  unless application_installed?(package_name(@test_server_path))
    raise "Cannot clear data, test-server #{package_name(@test_server_path)} is not installed"
  end

  cmd = "#{adb_command} shell am instrument #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.ClearAppData2"
  raise "Could not clear data" unless system(cmd)

  # Wait for the cleanup activity to finish. This is a hard sleep for now
  sleep 2

  true
end
clear_preferences(name) click to toggle source
# File lib/calabash-android/operations.rb, line 935
def clear_preferences(name)

  calabash_log "Clear preferences: #{name}, app running? #{app_running?}"

  if app_running?
    perform_action('clear_preferences', name);
  else

    logcat_id = get_logcat_id()
    cmd = "#{adb_command} shell am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.ClearPreferences"
    raise "Could not clear preferences" unless system(cmd)

    logcat_cmd = get_logcat_cmd(logcat_id)
    logcat_output = `#{logcat_cmd}`

    json = get_json_from_logcat(logcat_output)

    raise "Could not clear preferences" unless json != nil and json["success"]
  end
end
client_version() click to toggle source
# File lib/calabash-android/operations.rb, line 614
def client_version
  Calabash::Android::VERSION
end
configure_http(http, options) click to toggle source
# File lib/calabash-android/operations.rb, line 563
def configure_http(http, options)
  return unless http
  http.connect_timeout = options[:open_timeout] || 15
  http.send_timeout = options[:send_timeout] || 15
  http.receive_timeout = options[:read_timeout] || 15
  if options.has_key?(:debug) && options[:debug]
    http.debug_dev= $stdout
  else
    if ENV['DEBUG_HTTP'] and (ENV['DEBUG_HTTP'] != '0')
      http.debug_dev = $stdout
    else
      http.debug_dev= nil
    end
  end
  http
end
connected_devices() click to toggle source
# File lib/calabash-android/operations.rb, line 668
def connected_devices
  # Run empty ADB command to remove eventual first-run messages
  `"#{Calabash::Android::Dependencies.adb_path}" devices`

  lines = `"#{Calabash::Android::Dependencies.adb_path}" devices`.split("\n")
  start_index = lines.index{ |x| x =~ /List of devices attached/ } + 1
  lines[start_index..-1].collect { |l| l.split("\t").first }
end
default_serial() click to toggle source
# File lib/calabash-android/operations.rb, line 635
def default_serial
  devices = connected_devices
  calabash_log "connected_devices: #{devices}"
  raise "No connected devices" if devices.empty?
  raise "More than one device connected. Specify device serial using ADB_DEVICE_ARG" if devices.length > 1
  devices.first
end
default_server_port() click to toggle source
# File lib/calabash-android/operations.rb, line 643
def default_server_port
  require 'yaml'
  File.open(File.expand_path(server_port_configuration), File::RDWR|File::CREAT) do |f|
    f.flock(File::LOCK_EX)
    state = YAML::load(f) || {}
    ports = state['server_ports'] ||= {}
    return ports[serial] if ports.has_key?(serial)

    port = 34777
    port += 1 while ports.has_value?(port)
    ports[serial] = port

    f.rewind
    f.write(YAML::dump(state))
    f.truncate(f.pos)

    calabash_log "Persistently allocated port #{port} to #{serial}"
    return port
  end
end
ensure_apps_installed() click to toggle source
# File lib/calabash-android/operations.rb, line 351
def ensure_apps_installed
  apps = [@app_path, @test_server_path]

  apps.each do |app|
    package = package_name(app)
    md5 = Digest::MD5.file(File.expand_path(app))

    if !application_installed?(package) || (!@@installed_apps.keys.include?(package) || @@installed_apps[package] != md5)
      calabash_log "MD5 checksum for app '#{app}' (#{package}): #{md5}"
      uninstall_app(package)
      install_app(app)
      @@installed_apps[package] = md5
    end
  end
end
get_json_from_logcat(logcat_output) click to toggle source
# File lib/calabash-android/operations.rb, line 956
def get_json_from_logcat(logcat_output)

  logcat_output.split(/\r?\n/).each do |line|
    begin
      json = JSON.parse(line)
      return json
    rescue
      # nothing to do here, just discarding logcat rubbish
    end
  end

  return nil
end
get_logcat_cmd(tag) click to toggle source
# File lib/calabash-android/operations.rb, line 979
def get_logcat_cmd(tag)
  # returns raw logcat output for our tag
  # filtering out everthing else

  "#{adb_command} logcat -d -v raw #{tag}:* *:S"
end
get_logcat_id() click to toggle source
# File lib/calabash-android/operations.rb, line 970
def get_logcat_id()
  # we need a unique logcat tag so we can later
  # query the logcat output and filter out everything
  # but what we are interested in

  random = (0..10000).to_a.sample
  "#{Time.now.strftime("%s")}_#{random}"
end
get_preferences(name) click to toggle source
# File lib/calabash-android/operations.rb, line 876
def get_preferences(name)

  calabash_log "Get preferences: #{name}, app running? #{app_running?}"
  preferences = {}

  if app_running?
    json = perform_action('get_preferences', name);
  else

    logcat_id = get_logcat_id()
    cmd = "#{adb_command} shell am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.GetPreferences"

    raise "Could not get preferences" unless system(cmd)

    logcat_cmd = get_logcat_cmd(logcat_id)
    logcat_output = `#{logcat_cmd}`

    json = get_json_from_logcat(logcat_output)

    raise "Could not get preferences" unless json != nil and json["success"]
  end

  # at this point we have valid json, coming from an action
  # or instrumentation, but we don't care, just parse
  if json["bonusInformation"].length > 0
    json["bonusInformation"].each do |item|
      json_item = JSON.parse(item)
      preferences[json_item["key"]] = json_item["value"]
    end
  end

  preferences
end
http(path, data = {}, options = {}) click to toggle source
# File lib/calabash-android/operations.rb, line 474
def http(path, data = {}, options = {})
  begin

    configure_http(@http, options)
    make_http_request(
        :method => :post,
        :body => data.to_json,
        :uri => url_for(path),
        :header => {"Content-Type" => "application/json;charset=utf-8"})

  rescue HTTPClient::TimeoutError,
      HTTPClient::KeepAliveDisconnected,
      Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED,
      Errno::ETIMEDOUT => e
    calabash_log "It looks like your app is no longer running. \nIt could be because of a crash or because your test script shut it down."
    raise e
  end
end
http_put(path, data = {}, options = {}) click to toggle source
# File lib/calabash-android/operations.rb, line 493
def http_put(path, data = {}, options = {})
  begin

    configure_http(@http, options)
    make_http_request(
        :method => :put,
        :body => data,
        :uri => url_for(path),
        :header => {"Content-Type" => "application/octet-stream"})

  rescue HTTPClient::TimeoutError,
      HTTPClient::KeepAliveDisconnected,
      Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED,
      Errno::ETIMEDOUT => e
    calabash_log "It looks like your app is no longer running. \nIt could be because of a crash or because your test script shut it down."
    raise e
  end
end
init_request(options) click to toggle source
# File lib/calabash-android/operations.rb, line 558
def init_request(options)
  http = HTTPClient.new
  configure_http(http, options)
end
install_app(app_path) click to toggle source
# File lib/calabash-android/operations.rb, line 367
def install_app(app_path)
  if _sdk_version >= 23
    cmd = "#{adb_command} install -g -t \"#{app_path}\""
  else
    cmd = "#{adb_command} install -t \"#{app_path}\""
  end

  calabash_log "Installing: #{app_path}"
  result = `#{cmd}`
  calabash_log result
  pn = package_name(app_path)
  succeeded = `#{adb_command} shell pm list packages`.lines.map{|line| line.chomp.sub("package:", "")}.include?(pn)

  unless succeeded
    ::Cucumber.wants_to_quit = true
    raise "#{pn} did not get installed. Reason: '#{result.lines.last.chomp}'. Aborting!"
  end

  # Enable GPS location mocking on Android Marshmallow+
  if _sdk_version >= 23
    cmd = "#{adb_command} shell appops set #{package_name(app_path)} 58 allow"
    calabash_log("Enabling GPS mocking using '#{cmd}'")
    `#{cmd}`
  end

  true
end
keyguard_enabled?() click to toggle source
# File lib/calabash-android/operations.rb, line 444
def keyguard_enabled?
  dumpsys = `#{adb_command} shell dumpsys window windows`
  #If a line containing mCurrentFocus and Keyguard exists the keyguard is enabled
  dumpsys.lines.any? { |l| l.include?("mCurrentFocus") and l.include?("Keyguard")}
end
make_http_request(options) click to toggle source
# File lib/calabash-android/operations.rb, line 530
def make_http_request(options)
  begin
    unless @http
      @http = init_request(options)
    end
    header = options[:header] || {}
    header["Content-Type"] = "application/json;charset=utf-8"
    options[:header] = header


    response = if options[:method] == :post
                 @http.post(options[:uri], options)
               elsif options[:method] == :put
                 @http.put(options[:uri], options)
               else
                 @http.get(options[:uri], options)
               end
    raise Errno::ECONNREFUSED if response.status_code == 502
    response.body
  rescue => e
    if @http
      @http.reset_all
      @http=nil
    end
    raise e
  end
end
perform_action(action, *arguments) click to toggle source
# File lib/calabash-android/operations.rb, line 450
def perform_action(action, *arguments)
  calabash_log "Action: #{action} - Params: #{arguments.join(', ')}"

  params = {"command" => action, "arguments" => arguments}

  Timeout.timeout(300) do
    begin
      result = http("/", params, {:read_timeout => 350})
    rescue => e
      calabash_log "Error communicating with test server: #{e}"
      raise e
    end
    calabash_log "Result:'" + result.strip + "'"
    raise "Empty result from TestServer" if result.chomp.empty?
    result = JSON.parse(result)
    if not result["success"] then
      raise "Action '#{action}' unsuccessful: #{result["message"]}"
    end
    result
  end
rescue Timeout::Error
  raise "Step timed out"
end
pull(remote, local) click to toggle source
# File lib/calabash-android/operations.rb, line 706
def pull(remote, local)
  cmd = "#{adb_command} pull #{remote} #{local}"
  raise "Could not pull #{remote} to #{local}" unless system(cmd)
end
push(local, remote) click to toggle source
# File lib/calabash-android/operations.rb, line 711
def push(local, remote)
  cmd = "#{adb_command} push #{local} #{remote}"
  raise "Could not push #{local} to #{remote}" unless system(cmd)
end
reinstall_apps() click to toggle source
# File lib/calabash-android/operations.rb, line 337
def reinstall_apps
  uninstall_app(package_name(@app_path))
  uninstall_app(package_name(@test_server_path))
  install_app(@app_path)
  install_app(@test_server_path)
end
reinstall_test_server() click to toggle source
# File lib/calabash-android/operations.rb, line 344
def reinstall_test_server
  uninstall_app(package_name(@test_server_path))
  install_app(@test_server_path)
end
screenshot(options={:prefix => nil, :name => nil}) click to toggle source
# File lib/calabash-android/operations.rb, line 580
def screenshot(options={:prefix => nil, :name => nil})
  prefix = options[:prefix] || ENV['SCREENSHOT_PATH'] || ""
  name = options[:name]

  if name.nil?
    name = "screenshot"
  else
    if File.extname(name).downcase == ".png"
      name = name.split(".png")[0]
    end
  end

  @@screenshot_count ||= 0
  path = "#{prefix}#{name}_#{@@screenshot_count}.png"

  if ENV["SCREENSHOT_VIA_USB"] == "false"
    begin
      res = http("/screenshot")
    rescue EOFError
      raise "Could not take screenshot. App is most likely not running anymore."
    end
    File.open(path, 'wb') do |f|
      f.write res
    end
  else
    screenshot_cmd = "java -jar \"#{File.join(File.dirname(__FILE__), 'lib', 'screenshotTaker.jar')}\" #{serial} \"#{path}\""
    calabash_log screenshot_cmd
    raise "Could not take screenshot" unless system(screenshot_cmd)
  end

  @@screenshot_count += 1
  path
end
server_port_configuration() click to toggle source
# File lib/calabash-android/operations.rb, line 664
def server_port_configuration
  File.expand_path(ENV['CALABASH_SERVER_PORTS'] || "~/.calabash.yaml")
end
server_version() click to toggle source
# File lib/calabash-android/operations.rb, line 618
def server_version
  begin
    response = perform_action('version')
    raise 'Invalid response' unless response['success']
  rescue => e
    calabash_log("Could not contact server")
    calabash_log(e && e.backtrace && e.backtrace.join("\n"))
    raise "The server did not respond. Make sure the server is running."
  end

  response['message']
end
set_gps_coordinates(latitude, longitude) click to toggle source
# File lib/calabash-android/operations.rb, line 872
def set_gps_coordinates(latitude, longitude)
  perform_action('set_gps_coordinates', latitude, longitude)
end
set_gps_coordinates_from_location(location) click to toggle source

location

# File lib/calabash-android/operations.rb, line 863
def set_gps_coordinates_from_location(location)
  require 'geocoder'
  results = Geocoder.search(location)
  raise "Got no results for #{location}" if results.empty?

  best_result = results.first
  set_gps_coordinates(best_result.latitude, best_result.longitude)
end
set_http(http) click to toggle source
# File lib/calabash-android/operations.rb, line 512
def set_http(http)
  @http = http
end
set_preferences(name, hash) click to toggle source
# File lib/calabash-android/operations.rb, line 910
def set_preferences(name, hash)

  calabash_log "Set preferences: #{name}, #{hash}, app running? #{app_running?}"

  if app_running?
    perform_action('set_preferences', name, hash);
  else

    params = hash.map {|k,v| "-e \"#{k}\" \"#{v}\""}.join(" ")

    logcat_id = get_logcat_id()
    am_cmd = Shellwords.escape("am instrument -e logcat #{logcat_id} -e name \"#{name}\" #{params} #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.SetPreferences")
    cmd = "#{adb_command} shell #{am_cmd}"

    raise "Could not set preferences" unless system(cmd)

    logcat_cmd = get_logcat_cmd(logcat_id)
    logcat_output = `#{logcat_cmd}`

    json = get_json_from_logcat(logcat_output)

    raise "Could not set preferences" unless json != nil and json["success"]
  end
end
shutdown_test_server() click to toggle source
# File lib/calabash-android/operations.rb, line 843
def shutdown_test_server
  begin
    unless @adb_shell_pid.nil?
      Process.kill("HUP",@adb_shell_pid)
      @adb_shell_pid = nil
    end
    http("/kill")
    Timeout::timeout(3) do
      sleep 0.3 while app_running?
    end
  rescue HTTPClient::KeepAliveDisconnected
    calabash_log ("Server not responding. Moving on.")
  rescue Timeout::Error
    calabash_log ("Could not kill app. Waited to 3 seconds.")
  rescue EOFError
    calabash_log ("Could not kill app. App is most likely not running anymore.")
  end
end
start_application(intent) click to toggle source
# File lib/calabash-android/operations.rb, line 829
def start_application(intent)
  begin
    result = JSON.parse(http("/start-application", {intent: intent}, {read_timeout: 60}))
  rescue HTTPClient::ReceiveTimeoutError => e
    raise "Failed to start application. Starting took more than 60 seconds: #{e.class} - #{e.message}"
  end

  if result['outcome'] != 'SUCCESS'
    raise result['detail']
  end

  result['result']
end
start_test_server_in_background(options={}, &block) click to toggle source
# File lib/calabash-android/operations.rb, line 716
      def start_test_server_in_background(options={}, &block)
        raise "Will not start test server because of previous failures." if ::Cucumber.wants_to_quit

        if keyguard_enabled?
          wake_up
        end

        env_options = options.clone
        env_options.delete(:intent)

        env_options[:main_activity] ||= ENV['MAIN_ACTIVITY'] || 'null'
        env_options[:test_server_port] ||= @test_server_port
        env_options[:class] ||= "sh.calaba.instrumentationbackend.InstrumentationBackend"

        cmd_arr = [adb_command, "shell am instrument"]

        env_options.each_pair do |key, val|
          cmd_arr << "-e"
          cmd_arr << key.to_s
          cmd_arr << val.to_s
        end

        cmd_arr << "#{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner"

        if options[:with_uiautomator]
          cmd_arr.insert(2, "-w")
          shutdown_test_server
          @adb_shell_pid = Process.spawn(cmd_arr.join(" "), :in => '/dev/null') rescue "Could not execute command to start test server with uiautomator"
        else
          cmd = cmd_arr.join(" ")

          calabash_log "Starting test server using:"
          calabash_log cmd
          raise "Could not execute command to start test server" unless system("#{cmd} 2>&1")
        end

        Calabash::Android::Retry.retry :tries => 600, :interval => 0.1 do
          raise "App did not start see adb logcat for details" unless app_running?
        end

        begin
          Calabash::Android::Retry.retry :tries => 300, :interval => 0.1 do
            calabash_log "Checking if instrumentation backend is ready"

            calabash_log "Is app running? #{app_running?}"
            ready = http("/ready", {}, {:read_timeout => 1})
            if ready != "true"
              calabash_log "Instrumentation backend not yet ready"
              raise "Not ready"
            else
              calabash_log "Instrumentation backend is ready!"
            end
          end
        rescue => e

          msg = "Unable to make connection to Calabash Test Server at http://127.0.0.1:#{@server_port}/\n"
          msg << "Please check the logcat output for more info about what happened\n"
          raise msg
        end

        begin
          server_version = server_version()
        rescue
          msg = ["Unable to obtain Test Server version. "]
          msg << "Please run 'reinstall_test_server' to make sure you have the correct version"
          msg_s = msg.join("\n")
          calabash_log(msg_s)
          raise msg_s
        end

        client_version = client_version()

        if Calabash::Android::Environment.skip_version_check?
          calabash_log(%Q[
     Client version #{client_version}
Test-server version #{server_version}

])
          $stdout.flush
        else
          calabash_log "Checking client-server version match..."

          if server_version != client_version
             calabash_log(%Q[
Calabash Client and Test-server version mismatch.

              Client version #{client_version}
         Test-server version #{server_version}
Expected Test-server version #{client_version}

Solution:

Run 'reinstall_test_server' to make sure you have the correct version

])
          else
            calabash_log("Client and server versions match (client: #{client_version}, server: #{server_version}). Proceeding...")
          end
        end

        block.call if block

        start_application(options[:intent])

        # What was Calabash tracking? Read this post for information
        # No private data (like ip addresses) were collected
        # https://github.com/calabash/calabash-android/issues/655
        #
        # Removing usage tracking to avoid problems with EU General Data
        # Protection Regulation which takes effect in 2018.
        # Calabash::Android::UsageTracker.new.post_usage_async
      end
uninstall_app(package_name) click to toggle source
# File lib/calabash-android/operations.rb, line 414
def uninstall_app(package_name)
  exists = application_installed?(package_name)
  
  if exists
    calabash_log "Uninstalling: #{package_name}"
    calabash_log `#{adb_command} uninstall #{package_name}`

    succeeded = !application_installed?(package_name)

    unless succeeded
      ::Cucumber.wants_to_quit = true
      raise "#{package_name} was not uninstalled. Aborting!"
    end
  else
    calabash_log "Package not installed: #{package_name}. Skipping uninstall."
  end
end
update_app(app_path) click to toggle source
# File lib/calabash-android/operations.rb, line 395
def update_app(app_path)
  if _sdk_version >= 23
    cmd = "#{adb_command} install -r -g \"#{app_path}\""
  else
    cmd = "#{adb_command} install -r \"#{app_path}\""
  end

  calabash_log "Updating: #{app_path}"
  result = `#{cmd}`
  calabash_log "result: #{result}"
  succeeded = result.include?("Success")

  unless succeeded
    ::Cucumber.wants_to_quit = true
    pn = package_name(app_path)
    raise "#{pn} did not get updated. Aborting!"
  end
end
url_for(method) click to toggle source
# File lib/calabash-android/operations.rb, line 516
def url_for(method)
  url = URI.parse(ENV['DEVICE_ENDPOINT']|| "http://127.0.0.1:#{@server_port}")
  path = url.path
  if path.end_with? "/"
    path = "#{path}#{method}"
  else
    path = "#{path}/#{method}"
  end
  url.path = path
  url
end
wake_up() click to toggle source
# File lib/calabash-android/operations.rb, line 677
def wake_up
  wake_up_cmd = "#{adb_command} shell am start -a android.intent.action.MAIN -n #{package_name(@test_server_path)}/sh.calaba.instrumentationbackend.WakeUp"
  calabash_log "Waking up device using:"
  calabash_log wake_up_cmd
  raise "Could not wake up the device" unless system(wake_up_cmd)

  Calabash::Android::Retry.retry :tries => 10, :interval => 1 do
    raise "Could not remove the keyguard" if keyguard_enabled?
  end
end