Class: Fastlane::Helper::Android::EmulatorHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb

Overview

Helper methods to manipulate System Images, AVDs and Android Emulators

Constant Summary collapse

BOOT_WAIT =
2
BOOT_TIMEOUT =
60
SHUTDOWN_WAIT =
2
SHUTDOWN_TIMEOUT =
60

Instance Method Summary collapse

Constructor Details

#initializeEmulatorHelper

Returns a new instance of EmulatorHelper.



13
14
15
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 13

def initialize
  @tools = Fastlane::Helper::Android::ToolsPathHelper.new
end

Instance Method Details

#create_avd(api:, device:, system_image: nil, name: nil, sdcard: '512M') ⇒ String

Create an emulator (AVD) for a given ‘api` number and `device` model

Parameters:

  • api (Integer)

    The Android API level to use for this AVD

  • device (String)

    The Device Model to use for this AVD. Valid values can be found using ‘avdmanager list devices`

  • name (String) (defaults to: nil)

    The name to give for the created AVD. Defaults to ‘<device>API<api>`.

  • sdcard (String) (defaults to: '512M')

    The size of the SD card for this device. Defaults to ‘512M`.

Returns:

  • (String)

    The device name (i.e. either ‘name` if provided, or the derived `<device>API<api>` if provided `name` was `nil“)



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 41

def create_avd(api:, device:, system_image: nil, name: nil, sdcard: '512M')
  package = system_image || system_image_package(api: api)
  device_name = name || "#{device.gsub(' ', '_').capitalize}_API_#{api}"

  UI.message("Creating AVD `#{device_name}` (#{device}, API #{api})")

  Actions.sh(
    @tools.avdmanager, 'create', 'avd',
    '--force',
    '--package', package,
    '--device', device,
    '--sdcard', sdcard,
    '--name', device_name
  )

  UI.success("AVD `#{device_name}` successfully created.")

  device_name
end

#find_serial(avd_name:) ⇒ Object



129
130
131
132
133
134
135
136
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 129

def find_serial(avd_name:)
  running_emulators.find do |candidate|
    command = [@tools.adb, '-s', candidate.serial, 'emu', 'avd', 'name']
    UI.command(command.shelljoin)
    candidate_name = Actions.sh(*command, log: false).split("\n").first.chomp
    candidate_name == avd_name
  end&.serial
end

#install_system_image(api:) ⇒ String

Installs the system-image suitable for a given Android ‘api`, with Google APIs, and for the current machine’s architecture

Parameters:

  • api (Integer)

    The Android API level to use

Returns:

  • (String)

    The ‘sdkmanager` package specifier that has been installed



23
24
25
26
27
28
29
30
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 23

def install_system_image(api:)
  package = system_image_package(api: api)

  UI.message("Installing System Image for Android #{api} (#{package})")
  Actions.sh(@tools.sdkmanager, '--install', package)
  UI.success("System Image #{package} successfully installed.")
  package
end

#launch_avd(name:, port: nil, cold_boot: true, wipe_data: true) ⇒ String

Launch the emulator for the given AVD, then return the emulator serial

Parameters:

  • name (String)

    name of the AVD to launch

  • port (Int) (defaults to: nil)

    the TCP port to use to connect to the emulator via adb. If nil (default), will let ‘emulator` pick the first free one.

  • cold_boot (Boolean) (defaults to: true)

    if true, will do a cold boot, if false will try to use a previous snapshot of the device

  • wipe_data (Boolean) (defaults to: true)

    if true, will wipe the emulator (i.e. reset the user data image)

Returns:

  • (String)

    emulator serial number corresponding to the launched AVD



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 70

def launch_avd(name:, port: nil, cold_boot: true, wipe_data: true)
  UI.message("Launching emulator for #{name}")

  params = ['-avd', name]
  params << ['-port', port.to_s] unless port.nil?
  params << '-no-snapshot' if cold_boot
  params << '-wipe-data' if wipe_data

  UI.command([@tools.emulator, *params].shelljoin)
  # We want to launch emulator in the background to not block the rest of the code, so we can't use `Actions.sh` here
  # We also want to filter the `stdout`+`stderr` emitted by the `emulator` process in the background,
  # to limit verbosity and only print error lines, and also prefix those clearly (because they might happen
  # at any moment in the background, so in parallel/the middle of other fastlane logs).
  t = Thread.new do
    Open3.popen2e(@tools.emulator, *params) do |i, oe, wait_thr|
      i.close
      until oe.eof?
        line = oe.readline
        UI.error("📱 [emulator]: #{line}") if line.start_with?(/ERROR|PANIC/)
        next unless line.include?('PANIC: Broken AVD system path')

        UI.user_error! <<~HINT
          #{line}
          Verify that your `sdkmanager/avdmanager` tools are not installed in a different SDK root than your `emulator` tool
          (which can happen if you installed Android's command-line tools via `brew`, but the `emulator` via Android Studio, or vice-versa)
        HINT
      end
      UI.error("📱 [emulator]: exited with non-zero status code: #{wait_thr.value.exitstatus}") unless wait_thr.value.success?
    end
  end
  t.abort_on_exception = true # To bubble up any exception like `UI.user_error!` back to the main thread here

  UI.message('Waiting for emulator to start...')
  # Loop until the emulator has started and shows up in `adb devices -l` so we can find its serial
  serial = nil
  retry_loop(time_between_retries: BOOT_WAIT, timeout: BOOT_TIMEOUT, description: 'waiting for emulator to start') do
    serial = find_serial(avd_name: name)
    !serial.nil?
  end
  UI.message("Found device `#{name}` with serial `#{serial}`")

  # Once the emulator has started, wait for the device in the emulator to finish booting
  UI.message('Waiting for device to finish booting...')
  retry_loop(time_between_retries: BOOT_WAIT, timeout: BOOT_TIMEOUT, description: 'waiting for device to finish booting') do
    Actions.sh(@tools.adb, '-s', serial, 'shell', 'getprop', 'sys.boot_completed').chomp == '1'
  end

  UI.success("Emulator #{name} successfully booted as `#{serial}`.")

  serial
end

#retry_loop(time_between_retries:, timeout:, description:) ⇒ Object



188
189
190
191
192
193
194
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 188

def retry_loop(time_between_retries:, timeout:, description:)
  Timeout.timeout(timeout) do
    sleep(time_between_retries) until yield
  end
rescue Timeout::Error
  UI.user_error!("Timed out #{description}")
end

#running_emulatorsArray<Fastlane::Helper::AdbDevice>

Returns List of currently booted emulators.

Returns:

  • (Array<Fastlane::Helper::AdbDevice>)

    List of currently booted emulators



124
125
126
127
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 124

def running_emulators
  helper = Fastlane::Helper::AdbHelper.new(adb_path: @tools.adb)
  helper.load_all_devices.select { |device| device.serial.include?('emulator') }
end

#shut_down_emulators!(serials: nil) ⇒ Object

Trigger a shutdown for all running emulators, and wait until there is no more emulators running.

Parameters:

  • serials (Array<String>) (defaults to: nil)

    List of emulator serials to shut down. Will shut down all of them if ‘nil`.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 142

def shut_down_emulators!(serials: nil)
  UI.message("Shutting down #{serials || 'all'} emulator(s)...")

  emulators_list = running_emulators.map(&:serial)
  # Get the intersection of the set of running emulators with the ones we want to shut down
  emulators_list &= serials unless serials.nil?
  emulators_list.each do |e|
    Actions.sh(@tools.adb, '-s', e, 'emu', 'kill') { |_| } # ignore error if no emulator with specified serial is running

    # NOTE: Alternative way of shutting down emulator would be to call the following command instead, which shuts down the emulator more gracefully:
    # `adb -s #{e} shell reboot -p` # In case you're wondering, `-p` is for "power-off"
    # But this alternate command:
    #  - Requires that `-no-snapshot` was used on boot (to avoid being prompted to save current state on shutdown)
    #  - Disconnects the emulator from `adb` (and thus disappear from `adb devices -l`) for a short amount of time,
    #    before reconnecting to it but in an `offline` state, until `emulator` finally completely quits and it disappears
    #    again (for good) from `adb devices --list`.
    # This means that so if we used alternative, we couldn't really retry_loop until emulator disappears from `running_emulators` to detect
    # that the shutdown was really complete, as we might as well accidentally detect the intermediate disconnect instead.
  end

  # Wait until all emulators are killed
  retry_loop(time_between_retries: SHUTDOWN_WAIT, timeout: SHUTDOWN_TIMEOUT, description: 'waiting for devices to shutdown') do
    !emulators_list.intersect?(running_emulators.map(&:serial))
  end

  UI.success('All emulators are now shut down.')
end

#system_image_package(api:) ⇒ String

Note:

Results from this method are memoized, to avoid repeating calls to ‘sdkmanager` when querying for the same api level multiple times.

Find the system-images package for the provided ‘api`, with Google APIs, and matching the current platform/architecture this lane is called from.

Parameters:

  • api (Integer)

    The Android API level to use for this AVD

Returns:

  • (String)

    The ‘system-images;android-<N>;google_apis;<platform>` package specifier for `sdkmanager` to use in its install command



177
178
179
180
181
182
183
184
185
186
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb', line 177

def system_image_package(api:)
  @system_image_packages ||= {}
  @system_image_packages[api] ||= begin
    platform = `uname -m`.chomp
    all_packages = `#{@tools.sdkmanager} --list`
    package = all_packages.match(/^ *(system-images;android-#{api};google_apis;#{platform}(-[^ ]*)?)/)&.captures&.first
    UI.user_error!("Could not find system-image for API `#{api}` and your platform `#{platform}` in `sdkmanager --list`. Maybe Google removed it for download and it's time to update to a newer API?") if package.nil?
    package
  end
end