android-env 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. android_env/__init__.py +1 -1
  2. android_env/components/__init__.py +1 -1
  3. android_env/components/a11y/__init__.py +15 -0
  4. android_env/components/a11y/a11y_events.py +118 -0
  5. android_env/components/a11y/a11y_events_test.py +173 -0
  6. android_env/components/a11y/a11y_forests.py +128 -0
  7. android_env/components/a11y/a11y_forests_test.py +237 -0
  8. android_env/components/a11y/a11y_servicer.py +199 -0
  9. android_env/components/a11y/a11y_servicer_test.py +224 -0
  10. android_env/components/action_fns.py +132 -0
  11. android_env/components/action_fns_test.py +227 -0
  12. android_env/components/action_type.py +26 -3
  13. android_env/components/adb_call_parser.py +239 -196
  14. android_env/components/adb_call_parser_test.py +179 -209
  15. android_env/components/adb_controller.py +90 -52
  16. android_env/components/adb_controller_test.py +187 -16
  17. android_env/components/adb_log_stream.py +17 -5
  18. android_env/components/adb_log_stream_test.py +17 -3
  19. android_env/components/app_screen_checker.py +17 -15
  20. android_env/components/app_screen_checker_test.py +7 -8
  21. android_env/components/config_classes.py +203 -0
  22. android_env/components/coordinator.py +102 -338
  23. android_env/components/coordinator_test.py +59 -199
  24. android_env/components/device_settings.py +174 -0
  25. android_env/components/device_settings_test.py +228 -0
  26. android_env/components/dumpsys_thread.py +3 -4
  27. android_env/components/dumpsys_thread_test.py +1 -1
  28. android_env/components/errors.py +52 -10
  29. android_env/components/errors_test.py +110 -0
  30. android_env/components/log_stream.py +7 -5
  31. android_env/components/log_stream_test.py +1 -1
  32. android_env/components/logcat_thread.py +9 -8
  33. android_env/components/logcat_thread_test.py +3 -4
  34. android_env/components/{utils.py → pixel_fns.py} +20 -20
  35. android_env/components/{utils_test.py → pixel_fns_test.py} +20 -15
  36. android_env/components/setup_step_interpreter.py +47 -39
  37. android_env/components/setup_step_interpreter_test.py +4 -4
  38. android_env/components/simulators/__init__.py +1 -1
  39. android_env/components/simulators/base_simulator.py +116 -44
  40. android_env/components/simulators/base_simulator_test.py +131 -9
  41. android_env/components/simulators/emulator/__init__.py +1 -1
  42. android_env/components/simulators/emulator/emulator_launcher.py +67 -77
  43. android_env/components/simulators/emulator/emulator_launcher_test.py +153 -49
  44. android_env/components/simulators/emulator/emulator_simulator.py +276 -95
  45. android_env/components/simulators/emulator/emulator_simulator_test.py +314 -89
  46. android_env/components/simulators/fake/__init__.py +1 -1
  47. android_env/components/simulators/fake/fake_simulator.py +17 -25
  48. android_env/components/simulators/fake/fake_simulator_test.py +29 -12
  49. android_env/components/specs.py +18 -28
  50. android_env/components/specs_test.py +1 -44
  51. android_env/components/task_manager.py +48 -48
  52. android_env/components/task_manager_test.py +71 -60
  53. android_env/env_interface.py +37 -23
  54. android_env/environment.py +83 -51
  55. android_env/environment_test.py +68 -29
  56. android_env/loader.py +57 -43
  57. android_env/loader_test.py +115 -35
  58. android_env/proto/__init__.py +1 -1
  59. android_env/proto/a11y/__init__.py +15 -0
  60. android_env/proto/a11y/a11y.proto +75 -0
  61. android_env/proto/a11y/a11y_pb2.py +54 -0
  62. android_env/proto/a11y/a11y_pb2.pyi +49 -0
  63. android_env/proto/a11y/a11y_pb2_grpc.py +202 -0
  64. android_env/proto/a11y/android_accessibility_action.proto +32 -0
  65. android_env/proto/a11y/android_accessibility_action_pb2.py +37 -0
  66. android_env/proto/a11y/android_accessibility_action_pb2.pyi +13 -0
  67. android_env/proto/a11y/android_accessibility_action_pb2_grpc.py +24 -0
  68. android_env/proto/a11y/android_accessibility_forest.proto +29 -0
  69. android_env/proto/a11y/android_accessibility_forest_pb2.py +38 -0
  70. android_env/proto/a11y/android_accessibility_forest_pb2.pyi +13 -0
  71. android_env/proto/a11y/android_accessibility_forest_pb2_grpc.py +24 -0
  72. android_env/proto/a11y/android_accessibility_node_info.proto +122 -0
  73. android_env/proto/a11y/android_accessibility_node_info_clickable_span.proto +49 -0
  74. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.py +39 -0
  75. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.pyi +28 -0
  76. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2_grpc.py +24 -0
  77. android_env/proto/a11y/android_accessibility_node_info_pb2.py +42 -0
  78. android_env/proto/a11y/android_accessibility_node_info_pb2.pyi +75 -0
  79. android_env/proto/a11y/android_accessibility_node_info_pb2_grpc.py +24 -0
  80. android_env/proto/a11y/android_accessibility_tree.proto +29 -0
  81. android_env/proto/a11y/android_accessibility_tree_pb2.py +38 -0
  82. android_env/proto/a11y/android_accessibility_tree_pb2.pyi +13 -0
  83. android_env/proto/a11y/android_accessibility_tree_pb2_grpc.py +24 -0
  84. android_env/proto/a11y/android_accessibility_window_info.proto +84 -0
  85. android_env/proto/a11y/android_accessibility_window_info_pb2.py +41 -0
  86. android_env/proto/a11y/android_accessibility_window_info_pb2.pyi +48 -0
  87. android_env/proto/a11y/android_accessibility_window_info_pb2_grpc.py +24 -0
  88. android_env/proto/a11y/rect.proto +30 -0
  89. android_env/proto/a11y/rect_pb2.py +37 -0
  90. android_env/proto/a11y/rect_pb2.pyi +17 -0
  91. android_env/proto/a11y/rect_pb2_grpc.py +24 -0
  92. android_env/proto/adb.proto +17 -6
  93. android_env/proto/adb_pb2.py +120 -107
  94. android_env/proto/adb_pb2.pyi +396 -0
  95. android_env/proto/adb_pb2_grpc.py +20 -0
  96. android_env/proto/emulator_controller.proto +68 -63
  97. android_env/proto/emulator_controller_pb2.py +142 -131
  98. android_env/proto/emulator_controller_pb2.pyi +672 -0
  99. android_env/proto/emulator_controller_pb2_grpc.py +505 -142
  100. android_env/proto/snapshot.proto +169 -0
  101. android_env/proto/snapshot_pb2.py +47 -0
  102. android_env/proto/snapshot_pb2.pyi +117 -0
  103. android_env/proto/snapshot_pb2_grpc.py +24 -0
  104. android_env/proto/snapshot_service.proto +289 -0
  105. android_env/proto/snapshot_service_pb2.py +54 -0
  106. android_env/proto/snapshot_service_pb2.pyi +86 -0
  107. android_env/proto/snapshot_service_pb2_grpc.py +487 -0
  108. android_env/proto/state.proto +63 -0
  109. android_env/proto/state_pb2.py +63 -0
  110. android_env/proto/state_pb2.pyi +85 -0
  111. android_env/proto/state_pb2_grpc.py +24 -0
  112. android_env/proto/task.proto +5 -1
  113. android_env/proto/task_pb2.py +42 -31
  114. android_env/proto/task_pb2.pyi +160 -0
  115. android_env/proto/task_pb2_grpc.py +20 -0
  116. android_env/wrappers/__init__.py +1 -1
  117. android_env/wrappers/a11y_grpc_wrapper.py +500 -0
  118. android_env/wrappers/a11y_grpc_wrapper_test.py +849 -0
  119. android_env/wrappers/base_wrapper.py +34 -13
  120. android_env/wrappers/base_wrapper_test.py +22 -16
  121. android_env/wrappers/discrete_action_wrapper.py +18 -17
  122. android_env/wrappers/discrete_action_wrapper_test.py +4 -4
  123. android_env/wrappers/flat_interface_wrapper.py +5 -5
  124. android_env/wrappers/flat_interface_wrapper_test.py +7 -11
  125. android_env/wrappers/float_pixels_wrapper.py +9 -10
  126. android_env/wrappers/float_pixels_wrapper_test.py +3 -3
  127. android_env/wrappers/gym_wrapper.py +19 -13
  128. android_env/wrappers/gym_wrapper_test.py +3 -5
  129. android_env/wrappers/image_rescale_wrapper.py +18 -21
  130. android_env/wrappers/image_rescale_wrapper_test.py +25 -37
  131. android_env/wrappers/last_action_wrapper.py +16 -13
  132. android_env/wrappers/last_action_wrapper_test.py +44 -51
  133. android_env/wrappers/rate_limit_wrapper.py +6 -3
  134. android_env/wrappers/rate_limit_wrapper_test.py +22 -1
  135. android_env/wrappers/tap_action_wrapper.py +16 -17
  136. android_env/wrappers/tap_action_wrapper_test.py +51 -16
  137. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/METADATA +14 -18
  138. android_env-1.2.3.dist-info/RECORD +141 -0
  139. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/WHEEL +1 -1
  140. android_env/proto/raw_observation.proto +0 -39
  141. android_env/proto/raw_observation_pb2.py +0 -27
  142. android_env/proto/raw_observation_pb2_grpc.py +0 -4
  143. android_env-1.2.1.dist-info/RECORD +0 -81
  144. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info/licenses}/LICENSE +0 -0
  145. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -17,42 +17,73 @@
17
17
 
18
18
  import os
19
19
  import time
20
- from typing import Any, Dict, List, Optional, Tuple
20
+ from typing import Any
21
21
 
22
22
  from absl import logging
23
23
  from android_env.components import adb_controller
24
24
  from android_env.components import adb_log_stream
25
+ from android_env.components import config_classes
25
26
  from android_env.components import errors
26
27
  from android_env.components import log_stream
27
28
  from android_env.components.simulators import base_simulator
28
29
  from android_env.components.simulators.emulator import emulator_launcher
30
+ from android_env.proto import state_pb2
29
31
  import grpc
30
32
  import numpy as np
31
33
  import portpicker
32
34
 
33
35
  from android_env.proto import emulator_controller_pb2
34
36
  from android_env.proto import emulator_controller_pb2_grpc
37
+ from android_env.proto import snapshot_service_pb2
38
+ from android_env.proto import snapshot_service_pb2_grpc
35
39
  from google.protobuf import empty_pb2
36
40
 
37
41
 
38
- def is_existing_emulator_provided(launcher_args: Dict[str, Any]) -> bool:
42
+ _DEFAULT_SNAPSHOT_NAME: str = 'default_snapshot'
43
+
44
+
45
+ def _is_existing_emulator_provided(
46
+ launcher_config: config_classes.EmulatorLauncherConfig,
47
+ ) -> bool:
39
48
  """Returns true if all necessary args were provided."""
40
- return bool(launcher_args.get('adb_port') and
41
- launcher_args.get('emulator_console_port') and
42
- launcher_args.get('grpc_port'))
43
49
 
50
+ return bool(
51
+ launcher_config.adb_port
52
+ and launcher_config.emulator_console_port
53
+ and launcher_config.grpc_port
54
+ )
44
55
 
45
- def _reconnect_on_grpc_error(func):
46
- """Decorator function for reconnecting to emulator upon grpc errors."""
47
- def wrapper(*args, **kwargs):
48
- try:
49
- return func(*args, **kwargs)
50
- except grpc.RpcError:
51
- logging.exception('RpcError caught. Reconnecting to emulator...')
52
- emu = args[0] # The first arg of the function is "self"
53
- emu._emulator_stub = emu._connect_to_emulator(emu._grpc_port) # pylint: disable=protected-access
54
- return func(*args, **kwargs)
55
- return wrapper
56
+
57
+ def _pick_adb_port() -> int:
58
+ """Tries to pick a port in the recommended range 5555-5585.
59
+
60
+ If no such port can be found, will return a random unused port. More info:
61
+ https://developer.android.com/studio/command-line/adb#howadbworks.
62
+
63
+ Returns:
64
+ port: an available port for adb.
65
+ """
66
+
67
+ for p in range(5555, 5587, 2):
68
+ if portpicker.is_port_free(p):
69
+ return p
70
+ return portpicker.pick_unused_port()
71
+
72
+
73
+ def _pick_emulator_grpc_port() -> int:
74
+ """Tries to pick the recommended port for grpc.
75
+
76
+ If no such port can be found, will return a random unused port. More info:
77
+ https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/android-grpc/docs/.
78
+
79
+ Returns:
80
+ port: an available port for emulator grpc.
81
+ """
82
+
83
+ if portpicker.is_port_free(8554):
84
+ return 8554
85
+ else:
86
+ return portpicker.pick_unused_port()
56
87
 
57
88
 
58
89
  class EmulatorBootError(errors.SimulatorError):
@@ -66,69 +97,86 @@ class EmulatorCrashError(errors.SimulatorError):
66
97
  class EmulatorSimulator(base_simulator.BaseSimulator):
67
98
  """Controls an Android Emulator."""
68
99
 
69
- def __init__(self,
70
- emulator_launcher_args: Dict[str, Any],
71
- adb_controller_args: Dict[str, Any],
72
- tmp_dir: str = '/tmp/android_env/simulator',
73
- logfile_path: Optional[str] = None,
74
- **kwargs):
75
- """Instantiates an EmulatorSimulator.
100
+ def __init__(self, config: config_classes.EmulatorConfig):
101
+ """Instantiates an EmulatorSimulator."""
76
102
 
77
- Args:
78
- emulator_launcher_args: Arguments for EmulatorLauncher.
79
- adb_controller_args: Arguments for AdbController.
80
- tmp_dir: Temporary directory to hold simulator files.
81
- logfile_path: Path to file which holds emulator logs. If not provided,
82
- it will be determined by the EmulatorLauncher.
83
- **kwargs: keyword arguments for base class.
84
- """
103
+ super().__init__(config)
104
+ self._config = config
85
105
 
86
106
  # If adb_port, console_port and grpc_port are all already provided,
87
107
  # we assume the emulator already exists and there's no need to launch.
88
- if is_existing_emulator_provided(emulator_launcher_args):
108
+ if _is_existing_emulator_provided(self._config.emulator_launcher):
89
109
  self._existing_emulator_provided = True
90
- self._adb_port = emulator_launcher_args['adb_port']
91
- self._console_port = emulator_launcher_args['emulator_console_port']
92
- self._grpc_port = emulator_launcher_args['grpc_port']
93
- logging.info('Connecting to existing emulator "%r"', self._adb_port)
110
+ logging.info('Connecting to existing emulator "%r"',
111
+ self.adb_device_name())
94
112
  else:
95
113
  self._existing_emulator_provided = False
96
- self._adb_port = portpicker.pick_unused_port()
97
- self._console_port = portpicker.pick_unused_port()
98
- self._grpc_port = portpicker.pick_unused_port()
114
+ self._config.emulator_launcher.adb_port = _pick_adb_port()
115
+ self._config.emulator_launcher.emulator_console_port = (
116
+ portpicker.pick_unused_port()
117
+ )
118
+ self._config.emulator_launcher.grpc_port = _pick_emulator_grpc_port()
99
119
 
100
120
  self._channel = None
101
- self._emulator_stub = None
121
+ self._emulator_stub: emulator_controller_pb2_grpc.EmulatorControllerStub | None = (
122
+ None
123
+ )
124
+ self._snapshot_stub = None
102
125
  # Set the image format to RGBA. The width and height of the returned
103
126
  # screenshots will use the device's width and height.
104
127
  self._image_format = emulator_controller_pb2.ImageFormat(
105
128
  format=emulator_controller_pb2.ImageFormat.ImgFormat.RGBA8888)
106
129
 
107
- super().__init__(**kwargs)
130
+ if (
131
+ self._config.launch_n_times_without_reboot
132
+ > self._config.launch_n_times_without_reinstall
133
+ ):
134
+ raise ValueError(
135
+ 'Number of launch attempts before reboot'
136
+ f' ({self._config.launch_n_times_without_reboot}) should not be'
137
+ ' greater than number of launch attempts before reinstall'
138
+ f' ({self._config.launch_n_times_without_reinstall})'
139
+ )
108
140
 
109
141
  # Initialize own ADB controller.
110
- self._adb_controller_args = adb_controller_args
142
+ self._config.adb_controller.device_name = self.adb_device_name()
111
143
  self._adb_controller = self.create_adb_controller()
112
144
  self._adb_controller.init_server()
113
- logging.info('Initialized simulator with ADB server port %r.',
114
- self._adb_controller_args['adb_server_port'])
145
+ logging.info(
146
+ 'Initialized simulator with ADB server port %r.',
147
+ self._config.adb_controller.adb_server_port,
148
+ )
115
149
 
116
150
  # If necessary, create EmulatorLauncher.
117
151
  if self._existing_emulator_provided:
118
- self._logfile_path = logfile_path or None
152
+ self._logfile_path = self._config.logfile_path or None
119
153
  self._launcher = None
120
154
  else:
121
- emulator_launcher_args.update({
122
- 'adb_port': self._adb_port,
123
- 'adb_server_port': self._adb_controller_args['adb_server_port'],
124
- 'emulator_console_port': self._console_port,
125
- 'grpc_port': self._grpc_port,
126
- 'tmp_dir': tmp_dir,
127
- })
128
- logging.info('emulator_launcher_args: %r', emulator_launcher_args)
155
+ logging.info(
156
+ 'emulator_launcher config: %r', self._config.emulator_launcher
157
+ )
129
158
  self._launcher = emulator_launcher.EmulatorLauncher(
130
- **emulator_launcher_args)
131
- self._logfile_path = logfile_path or self._launcher.logfile_path()
159
+ config=self._config.emulator_launcher,
160
+ adb_controller_config=self._config.adb_controller,
161
+ )
162
+ self._logfile_path = (
163
+ self._config.logfile_path or self._launcher.logfile_path()
164
+ )
165
+
166
+ def _reconnect_on_grpc_error(func):
167
+ """Decorator function for reconnecting to emulator upon grpc errors."""
168
+
169
+ def wrapper(self, *args, **kwargs):
170
+ try:
171
+ return func(self, *args, **kwargs)
172
+ except grpc.RpcError:
173
+ logging.exception('RpcError caught. Reconnecting to emulator...')
174
+ self._emulator_stub, self._snapshot_stub = self._connect_to_emulator(
175
+ self._config.emulator_launcher.grpc_port
176
+ )
177
+ return func(self, *args, **kwargs)
178
+
179
+ return wrapper
132
180
 
133
181
  def get_logs(self) -> str:
134
182
  """Returns logs recorded by the emulator."""
@@ -139,51 +187,155 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
139
187
  return f'Logfile does not exist: {self._logfile_path}.'
140
188
 
141
189
  def adb_device_name(self) -> str:
142
- return 'emulator-%s' % (self._adb_port - 1)
190
+ return 'emulator-%s' % (self._config.emulator_launcher.adb_port - 1)
143
191
 
144
192
  def create_adb_controller(self):
145
193
  """Returns an ADB controller which can communicate with this simulator."""
146
- return adb_controller.AdbController(
147
- device_name=self.adb_device_name(),
148
- **self._adb_controller_args)
194
+ return adb_controller.AdbController(self._config.adb_controller)
149
195
 
150
196
  def create_log_stream(self) -> log_stream.LogStream:
151
197
  return adb_log_stream.AdbLogStream(
152
198
  adb_command_prefix=self._adb_controller.command_prefix(),
153
- verbose=self._verbose_logs)
154
-
155
- def _restart_impl(self) -> None:
156
- if self._launcher is not None:
157
- logging.info('Restarting the emulator...')
158
- self._shutdown_emulator()
159
- self._launcher.launch_emulator_process()
160
- self._emulator_stub = self._connect_to_emulator(self._grpc_port)
161
- self._confirm_booted()
162
- logging.info('Done restarting the emulator.')
199
+ verbose=self._config.verbose_logs,
200
+ )
163
201
 
164
202
  def _launch_impl(self) -> None:
165
- """Establishes a grpc connection to an emulator process."""
203
+ """Prepares an Android Emulator for RL interaction.
204
+
205
+ The behavior depends on `self._num_launch_attempts`'s value:
206
+ * <= self._config.launch_n_times_without_reboot -> Normal boot behavior.
207
+ * > self._config.launch_n_times_without_reboot but <=
208
+ self._config.launch_n_times_without_reinstall -> reboot (i.e. process
209
+ is killed and started again).
210
+ * > self._config.launch_n_times_without_reinstall -> reinstall (i.e.
211
+ process is killed, emulator files are deleted and the process started
212
+ again).
213
+ """
214
+
215
+ logging.info('Attempt %r at launching the Android Emulator (%r)',
216
+ self._num_launch_attempts, self.adb_device_name())
166
217
 
167
- # If required, launch an emulator process.
168
218
  if self._launcher is not None:
219
+ # If not the first time, then shutdown the emulator first.
220
+ if (
221
+ self._emulator_stub is not None
222
+ and self._num_launch_attempts
223
+ > self._config.launch_n_times_without_reboot
224
+ ):
225
+ self._shutdown_emulator()
226
+ # Subsequent attempts cause the emulator files to be reinstalled.
227
+ if (
228
+ self._num_launch_attempts
229
+ > self._config.launch_n_times_without_reinstall
230
+ ):
231
+ logging.info('Closing emulator (%r)', self.adb_device_name())
232
+ self._launcher.close()
233
+ self._launcher = emulator_launcher.EmulatorLauncher(
234
+ config=self._config.emulator_launcher,
235
+ adb_controller_config=self._config.adb_controller,
236
+ )
169
237
  self._launcher.launch_emulator_process()
170
-
171
238
  # Establish grpc connection to emulator process.
172
- self._emulator_stub = self._connect_to_emulator(self._grpc_port)
239
+ self._emulator_stub, self._snapshot_stub = self._connect_to_emulator(
240
+ self._config.emulator_launcher.grpc_port
241
+ )
173
242
 
174
243
  # Confirm booted status.
175
244
  try:
176
245
  self._confirm_booted()
177
246
  except EmulatorCrashError:
178
- logging.exception(
179
- 'Failed to confirm booted status of emulator. Restarting...')
180
- self.restart()
247
+ logging.exception('Failed to confirm booted status of emulator.')
248
+
249
+ logging.info('Done booting the Android Emulator.')
250
+
251
+ def load_state(
252
+ self, request: state_pb2.LoadStateRequest
253
+ ) -> state_pb2.LoadStateResponse:
254
+ """Loads a state using the emulator's snapshotting mechanism.
255
+
256
+ Args:
257
+ request: The `LoadStateRequest`. In this case, `args` should be a dict
258
+ containing the key 'snapshot_name', representing the name of the
259
+ snapshot to load. If `request.args.snapshot_name` is `None`, a default
260
+ snapshot name is used.
261
+
262
+ Returns:
263
+ A response indicating whether the snapshot was successfully loaded.
264
+ * If the snapshot was loaded successfully, the status will be `OK`.
265
+ * If no snapshot of the given name was found, the status will be
266
+ `NOT_FOUND`.
267
+ * If an error occurred during the snapshot loading process, the status
268
+ will be `ERROR` and the `error_message` field will be filled.
269
+ """
270
+ assert self._snapshot_stub is not None
271
+ snapshot_name = request.args.get('snapshot_name', _DEFAULT_SNAPSHOT_NAME)
272
+ snapshot_list = self._snapshot_stub.ListSnapshots(
273
+ snapshot_service_pb2.SnapshotFilter(
274
+ statusFilter=snapshot_service_pb2.SnapshotFilter.LoadStatus.All
275
+ )
276
+ )
277
+ if any(
278
+ snapshot.snapshot_id == snapshot_name
279
+ for snapshot in snapshot_list.snapshots
280
+ ):
281
+ snapshot_result = self._snapshot_stub.LoadSnapshot(
282
+ snapshot_service_pb2.SnapshotPackage(snapshot_id=snapshot_name)
283
+ )
284
+ if snapshot_result.success:
285
+ return state_pb2.LoadStateResponse(
286
+ status=state_pb2.LoadStateResponse.Status.OK
287
+ )
288
+ else:
289
+ return state_pb2.LoadStateResponse(
290
+ status=state_pb2.LoadStateResponse.Status.ERROR,
291
+ error_message=snapshot_result.err.decode('utf-8'),
292
+ )
293
+
294
+ else:
295
+ return state_pb2.LoadStateResponse(
296
+ status=state_pb2.LoadStateResponse.Status.NOT_FOUND
297
+ )
298
+
299
+ def save_state(
300
+ self, request: state_pb2.SaveStateRequest
301
+ ) -> state_pb2.SaveStateResponse:
302
+ """Saves a state using the emulator's snapshotting mechanism.
303
+
304
+ Args:
305
+ request: The `SaveStateRequest`. In this case, `args` should be a dict
306
+ containing the key 'snapshot_name', representing the name of the
307
+ snapshot to save. If `request.args.snapshot_name` is `None`, a default
308
+ snapshot name is used.
309
+
310
+ Returns:
311
+ A response indicating whether the snapshot was successfully saved.
312
+ * If the snapshot was saved successfully, the status will be `OK`.
313
+ * If an error occurred during the snapshot saving process, the status
314
+ will be `ERROR` and the `error_message` field will be filled.
315
+ """
316
+ assert self._snapshot_stub is not None
317
+ snapshot_name = request.args.get('snapshot_name', _DEFAULT_SNAPSHOT_NAME)
318
+ snapshot_result = self._snapshot_stub.SaveSnapshot(
319
+ snapshot_service_pb2.SnapshotPackage(snapshot_id=snapshot_name)
320
+ )
321
+ if snapshot_result.success:
322
+ return state_pb2.SaveStateResponse(
323
+ status=state_pb2.SaveStateResponse.Status.OK
324
+ )
325
+ else:
326
+ return state_pb2.SaveStateResponse(
327
+ status=state_pb2.SaveStateResponse.Status.ERROR,
328
+ error_message=snapshot_result.err.decode('utf-8'),
329
+ )
181
330
 
182
331
  def _connect_to_emulator(
183
332
  self,
184
333
  grpc_port: int,
185
- timeout_sec: int = 30,
186
- ) -> emulator_controller_pb2_grpc.EmulatorControllerStub:
334
+ timeout_sec: int = 100,
335
+ ) -> tuple[
336
+ emulator_controller_pb2_grpc.EmulatorControllerStub,
337
+ snapshot_service_pb2_grpc.SnapshotServiceStub,
338
+ ]:
187
339
  """Connects to an emulator and returns a corresponsing stub."""
188
340
 
189
341
  logging.info('Creating gRPC channel to the emulator on port %r', grpc_port)
@@ -201,18 +353,26 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
201
353
  'Failed to connect to the emulator.') from grpc_error
202
354
 
203
355
  logging.info('Added gRPC channel for the Emulator on port %s', port)
204
- return emulator_controller_pb2_grpc.EmulatorControllerStub(self._channel)
356
+ emulator_controller_stub = (
357
+ emulator_controller_pb2_grpc.EmulatorControllerStub(self._channel)
358
+ )
359
+ snapshot_stub = snapshot_service_pb2_grpc.SnapshotServiceStub(self._channel)
360
+ return emulator_controller_stub, snapshot_stub
205
361
 
206
362
  @_reconnect_on_grpc_error
207
363
  def _confirm_booted(self, startup_wait_time_sec: int = 300):
208
364
  """Waits until the emulator is fully booted."""
209
365
 
366
+ assert (
367
+ self._emulator_stub is not None
368
+ ), 'Emulator stub has not been initialized yet.'
210
369
  start_time = time.time()
211
370
  deadline = start_time + startup_wait_time_sec
212
371
  success = False
213
372
  while time.time() < deadline:
214
373
  emu_status = self._emulator_stub.getStatus(empty_pb2.Empty())
215
- logging.info('Waiting for emulator to start... (%rms)', emu_status.uptime)
374
+ logging.info('Waiting for emulator (%r) to start... (%rms)',
375
+ self.adb_device_name(), emu_status.uptime)
216
376
  if emu_status.booted:
217
377
  success = True
218
378
  break
@@ -224,18 +384,18 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
224
384
  f'The emulator failed to boot after {startup_wait_time_sec} seconds')
225
385
 
226
386
  logging.info('Done booting the emulator (in %f seconds).', elapsed_time)
227
- logging.info('********** Emulator logs (last 20 lines) **********')
228
- for line in self.get_logs().splitlines()[-20:]:
387
+ logging.info('********** Emulator logs **********')
388
+ for line in self.get_logs().splitlines():
229
389
  logging.info(line)
230
390
  logging.info('******* End of emulator logs *******')
231
391
  logging.info('See the full emulator logs at %r', self._logfile_path)
232
392
 
233
393
  @_reconnect_on_grpc_error
234
- def send_touch(self, touches: List[Tuple[int, int, bool, int]]) -> None:
394
+ def send_touch(self, touches: list[tuple[int, int, bool, int]]) -> None:
235
395
  """Sends a touch event to be executed on the simulator.
236
396
 
237
397
  Args:
238
- touches: A list of touch events. Each elemet in the list corresponds to a
398
+ touches: A list of touch events. Each element in the list corresponds to a
239
399
  single touch event. Each touch event tuple should have:
240
400
  0 x: The horizontal coordinate of this event.
241
401
  1 y: The vertical coordinate of this event.
@@ -243,7 +403,9 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
243
403
  3 identifier: Identifies a particular finger in a multitouch event.
244
404
  """
245
405
 
246
- assert self._emulator_stub, 'Emulator stub has not been initialized yet.'
406
+ assert (
407
+ self._emulator_stub is not None
408
+ ), 'Emulator stub has not been initialized yet.'
247
409
  touch_events = [
248
410
  emulator_controller_pb2.Touch(
249
411
  x=t[0], y=t[1], pressure=int(t[2]), identifier=t[3])
@@ -261,23 +423,32 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
261
423
  See the emulator_controller_pb2 for details.
262
424
  event_type: Type of key event to be sent.
263
425
  """
426
+
264
427
  event_types = emulator_controller_pb2.KeyboardEvent.KeyEventType.keys()
265
428
  if event_type not in event_types:
266
429
  raise ValueError(
267
430
  f'Event type must be one of {event_types} but is {event_type}.')
268
431
 
432
+ assert (
433
+ self._emulator_stub is not None
434
+ ), 'Emulator stub has not been initialized yet.'
269
435
  self._emulator_stub.sendKey(
270
436
  emulator_controller_pb2.KeyboardEvent(
271
437
  codeType=emulator_controller_pb2.KeyboardEvent.KeyCodeType.XKB,
272
- eventType=emulator_controller_pb2.KeyboardEvent
273
- .KeyEventType.Value(event_type),
274
- keyCode=np.int32(keycode),
275
- ))
438
+ eventType=emulator_controller_pb2.KeyboardEvent.KeyEventType.Value(
439
+ event_type
440
+ ),
441
+ keyCode=int(keycode),
442
+ )
443
+ )
276
444
 
277
445
  @_reconnect_on_grpc_error
278
- def get_screenshot(self) -> np.ndarray:
446
+ def _get_screenshot_impl(self) -> np.ndarray:
279
447
  """Fetches the latest screenshot from the emulator."""
280
- assert self._emulator_stub, 'Emulator stub has not been initialized yet.'
448
+
449
+ assert (
450
+ self._emulator_stub is not None
451
+ ), 'Emulator stub has not been initialized yet.'
281
452
  assert self._image_format, 'ImageFormat has not been initialized yet.'
282
453
  image_proto = self._emulator_stub.getScreenshot(self._image_format)
283
454
  h, w = image_proto.format.height, image_proto.format.width
@@ -288,18 +459,28 @@ class EmulatorSimulator(base_simulator.BaseSimulator):
288
459
  @_reconnect_on_grpc_error
289
460
  def _shutdown_emulator(self):
290
461
  """Sends a signal to trigger emulator shutdown."""
291
- logging.info('Shutting down the emulator...')
462
+
463
+ if self._emulator_stub is None:
464
+ logging.info('Emulator (%r) is not up.', self.adb_device_name())
465
+ return
466
+
467
+ assert self._launcher is not None, 'Launcher is already down.'
468
+
469
+ logging.info('Shutting down the emulator (%r)...', self.adb_device_name())
292
470
  self._emulator_stub.setVmState(
293
471
  emulator_controller_pb2.VmRunState(
294
472
  state=emulator_controller_pb2.VmRunState.RunState.SHUTDOWN))
295
473
  self._launcher.confirm_shutdown()
296
474
 
297
475
  def close(self):
476
+ super().close()
477
+
298
478
  if self._launcher is not None:
299
479
  self._shutdown_emulator()
480
+ logging.info('Closing emulator (%r)', self.adb_device_name())
300
481
  self._launcher.close()
301
- if hasattr(self, '_emulator_stub'):
302
- del self._emulator_stub
482
+ self._emulator_stub = None
483
+ self._snapshot_stub = None
303
484
  if self._channel is not None:
304
485
  self._channel.close()
305
486
  super().close()