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
@@ -0,0 +1,199 @@
1
+ # coding=utf-8
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Accessibility Servicer implementation."""
17
+
18
+ import asyncio
19
+ from collections.abc import AsyncIterator, Generator, Iterable
20
+ import threading
21
+
22
+ from absl import logging
23
+ from android_env.proto.a11y import a11y_pb2
24
+ from android_env.proto.a11y import a11y_pb2_grpc
25
+ from android_env.proto.a11y import android_accessibility_forest_pb2
26
+ import grpc
27
+
28
+
29
+ class A11yServicer(a11y_pb2_grpc.A11yServiceServicer):
30
+ """Services the A11yService requests."""
31
+
32
+ def __init__(self, latest_forest_only: bool = False):
33
+ self._received_forests: list[
34
+ android_accessibility_forest_pb2.AndroidAccessibilityForest
35
+ ] = []
36
+ self._received_events: list[a11y_pb2.EventRequest] = []
37
+ self._lock_forests = threading.Lock()
38
+ self._lock_events = threading.Lock()
39
+ self._latest_forest_only = latest_forest_only
40
+ self._paused = True
41
+
42
+ # A11y Forest bookkeeping.
43
+ self._get_forest = asyncio.Event() # Whether to request a forest.
44
+ self._forest_ready = asyncio.Event() # Whether the forest is ready.
45
+ self._latest_forest: (
46
+ android_accessibility_forest_pb2.AndroidAccessibilityForest | None
47
+ ) = None
48
+
49
+ def SendForest(
50
+ self,
51
+ request: android_accessibility_forest_pb2.AndroidAccessibilityForest,
52
+ context: grpc.ServicerContext,
53
+ ) -> a11y_pb2.ForestResponse:
54
+ self._process_forest(request)
55
+ return a11y_pb2.ForestResponse()
56
+
57
+ def SendEvent(
58
+ self,
59
+ request: a11y_pb2.EventRequest,
60
+ context: grpc.ServicerContext,
61
+ ) -> a11y_pb2.EventResponse:
62
+ self._process_event(request)
63
+ return a11y_pb2.EventResponse()
64
+
65
+ async def Bidi(
66
+ self,
67
+ request_iterator: AsyncIterator[a11y_pb2.ClientToServer],
68
+ context: grpc.aio.ServicerContext,
69
+ ) -> AsyncIterator[a11y_pb2.ServerToClient]:
70
+ """Processes incoming ClientToServer requests."""
71
+
72
+ logging.info('Starting A11yServicer.Bidi()')
73
+
74
+ # Send a dummy message to unblock clients in their loop.
75
+ yield a11y_pb2.ServerToClient()
76
+
77
+ # This block defines two coroutines:
78
+ #
79
+ # * `read_client_requests()`
80
+ # * `check_forest()`
81
+ #
82
+ # They cooperate with each other and both populate a queue `q` which is
83
+ # consumed in a loop below, which actually yields requests which are sent to
84
+ # the client. The processing finishes when the clients "closes" the
85
+ # connection, which causes `read_client_requests()` to put a special value,
86
+ # `STOP_ITERATION`, in the queue.
87
+
88
+ # Queue for communicating from coroutines to `Bidi()`.
89
+ q = asyncio.Queue()
90
+
91
+ should_run = True
92
+
93
+ async def read_client_requests():
94
+ """Coroutine for reading client requests."""
95
+
96
+ nonlocal should_run
97
+ async for request in request_iterator:
98
+ field_name = request.WhichOneof('payload')
99
+ match field_name:
100
+ case 'event':
101
+ self._process_event(request.event)
102
+ case 'forest':
103
+ self._latest_forest = request.forest
104
+ self._forest_ready.set()
105
+ self._get_forest.clear() # Reset the `Event`.
106
+ case _:
107
+ logging.error('Unknown field %r', field_name)
108
+ await q.put(a11y_pb2.ServerToClient())
109
+
110
+ # Send a special value to stop processing this `Bidi` connection.
111
+ await q.put('STOP_ITERATION')
112
+ should_run = False
113
+
114
+ async def check_forest():
115
+ """Coroutine for sending "get forest" requests."""
116
+
117
+ nonlocal should_run
118
+ while should_run:
119
+ await self._get_forest.wait()
120
+ await q.put(a11y_pb2.ServerToClient(get_forest={}))
121
+
122
+ tasks = asyncio.gather(read_client_requests(), check_forest())
123
+
124
+ while should_run:
125
+ v = await q.get()
126
+ if v == 'STOP_ITERATION':
127
+ break
128
+ else:
129
+ yield v
130
+
131
+ await tasks
132
+
133
+ logging.info('Finishing A11yServicer.Bidi()')
134
+
135
+ async def get_forest(
136
+ self,
137
+ ) -> android_accessibility_forest_pb2.AndroidAccessibilityForest | None:
138
+ """Issues a request to get the a11y forest from the client."""
139
+
140
+ self._get_forest.set() # Unblocks coroutine to send a request.
141
+ await self._forest_ready.wait() # Wait for forest to be ready.
142
+ self._forest_ready.clear() # Reset the `Event`.
143
+ return self._latest_forest
144
+
145
+ def gather_forests(
146
+ self,
147
+ ) -> list[android_accessibility_forest_pb2.AndroidAccessibilityForest]:
148
+ forests = []
149
+ with self._lock_forests:
150
+ forests = self._received_forests
151
+ self._received_forests = []
152
+ return forests
153
+
154
+ def gather_events(self) -> list[a11y_pb2.EventRequest]:
155
+ events = []
156
+ with self._lock_events:
157
+ events = self._received_events
158
+ self._received_events = []
159
+ return events
160
+
161
+ def pause_and_clear(self) -> None:
162
+ """Temporarily stop receiving events/forests and clear the queue.
163
+
164
+ Used when resetting the environment; in this case:
165
+ - all events/forests that have been received since last timestep are things
166
+ that happened in the last episode after its `LAST` timestep (so we should
167
+ ignore them, done by clearing the lists).
168
+ - we're about to receive a bunch of events/forests just as a result of
169
+ resetting the environment. We don't want to count these either; thus we
170
+ temporarily stop receiving new ones.
171
+ """
172
+ self._paused = True
173
+ with self._lock_forests:
174
+ self._received_forests = []
175
+ with self._lock_events:
176
+ self._received_events = []
177
+
178
+ def resume(self) -> None:
179
+ """Start receiving events/forests (e.g., after a reset)."""
180
+ self._paused = False
181
+
182
+ def _process_event(self, event: a11y_pb2.EventRequest) -> None:
183
+ """Adds the given event to the internal buffer of events."""
184
+
185
+ if not self._paused:
186
+ with self._lock_events:
187
+ self._received_events.append(event)
188
+
189
+ def _process_forest(
190
+ self, forest: android_accessibility_forest_pb2.AndroidAccessibilityForest
191
+ ) -> None:
192
+ """Adds the given forest to the internal buffer of forests."""
193
+
194
+ if not self._paused:
195
+ with self._lock_forests:
196
+ if self._latest_forest_only:
197
+ self._received_forests = [forest]
198
+ else:
199
+ self._received_forests.append(forest)
@@ -0,0 +1,224 @@
1
+ # coding=utf-8
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Tests for a11y_servicer."""
17
+
18
+ import asyncio
19
+ from collections.abc import AsyncIterator, Iterable
20
+ from typing import TypeVar
21
+ from unittest import IsolatedAsyncioTestCase, mock
22
+
23
+ from absl.testing import absltest
24
+ from absl.testing import parameterized
25
+ from android_env.components.a11y import a11y_servicer
26
+ from android_env.proto.a11y import a11y_pb2
27
+ from android_env.proto.a11y import android_accessibility_forest_pb2
28
+ import grpc
29
+
30
+
31
+ _T = TypeVar('_T')
32
+
33
+
34
+ async def _aiter(xs: Iterable[_T]) -> AsyncIterator[_T]:
35
+ """Utility to make an AsyncIterator from Iterable."""
36
+
37
+ for x in xs:
38
+ yield x
39
+
40
+
41
+ def one_window_one_node_forest() -> (
42
+ android_accessibility_forest_pb2.AndroidAccessibilityForest
43
+ ):
44
+ forest = android_accessibility_forest_pb2.AndroidAccessibilityForest()
45
+ window = forest.windows.add()
46
+ node = window.tree.nodes.add()
47
+ node.class_name = 'foo'
48
+ node.is_clickable = True
49
+ node.hint_text = 'Foo hint'
50
+ return forest
51
+
52
+
53
+ def one_window_two_nodes_forest() -> (
54
+ android_accessibility_forest_pb2.AndroidAccessibilityForest
55
+ ):
56
+ forest = android_accessibility_forest_pb2.AndroidAccessibilityForest()
57
+ window = forest.windows.add()
58
+ node = window.tree.nodes.add()
59
+ node.class_name = 'bar'
60
+ node.is_clickable = True
61
+ node.hint_text = 'Bar hint'
62
+ node = window.tree.nodes.add()
63
+ node.class_name = 'bar'
64
+ node.is_clickable = False
65
+ node.hint_text = 'Bar hint 2'
66
+ return forest
67
+
68
+
69
+ def empty_dict() -> dict[str, str]:
70
+ return {}
71
+
72
+
73
+ def single_item_dict_with_special_chars() -> dict[str, str]:
74
+ return {'foo': 'bar\r\t\nbaz'}
75
+
76
+
77
+ class A11yServicerTest(parameterized.TestCase, IsolatedAsyncioTestCase):
78
+
79
+ def test_servicer_sendforest(self):
80
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
81
+ servicer = a11y_servicer.A11yServicer()
82
+ servicer.resume()
83
+ response = servicer.SendForest(one_window_one_node_forest(), mock_context)
84
+ self.assertEqual(response.error, '')
85
+ response = servicer.SendForest(one_window_two_nodes_forest(), mock_context)
86
+ self.assertEqual(response.error, '')
87
+ forests = servicer.gather_forests()
88
+ self.assertLen(forests, 2)
89
+ self.assertEqual(forests[0], one_window_one_node_forest())
90
+ self.assertEqual(forests[1], one_window_two_nodes_forest())
91
+
92
+ async def test_servicer_bidi_forests(self):
93
+ """Checks that the bidirectional interface accepts forests."""
94
+
95
+ # Arrange.
96
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
97
+ servicer = a11y_servicer.A11yServicer()
98
+
99
+ # Act.
100
+ servicer.resume()
101
+ responses = [
102
+ x
103
+ async for x in servicer.Bidi(
104
+ _aiter([
105
+ a11y_pb2.ClientToServer(
106
+ event=a11y_pb2.EventRequest(
107
+ event=single_item_dict_with_special_chars()
108
+ )
109
+ ),
110
+ a11y_pb2.ClientToServer(forest=one_window_two_nodes_forest()),
111
+ ]),
112
+ mock_context,
113
+ )
114
+ ]
115
+ forest = await servicer.get_forest()
116
+
117
+ # Assert.
118
+ self.assertEqual(responses[0], a11y_pb2.ServerToClient())
119
+ self.assertEqual(responses[1], a11y_pb2.ServerToClient())
120
+ self.assertIsNotNone(forest)
121
+ self.assertEqual(forest, one_window_two_nodes_forest())
122
+
123
+ def test_servicer_sendforest_latest_only(self):
124
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
125
+ servicer = a11y_servicer.A11yServicer(latest_forest_only=True)
126
+ servicer.resume()
127
+ response = servicer.SendForest(one_window_one_node_forest(), mock_context)
128
+ self.assertEqual(response.error, '')
129
+ response = servicer.SendForest(one_window_two_nodes_forest(), mock_context)
130
+ self.assertEqual(response.error, '')
131
+ forests = servicer.gather_forests()
132
+ self.assertLen(forests, 1)
133
+ self.assertEqual(forests[0], one_window_two_nodes_forest())
134
+
135
+ def test_servicer_sendevent(self):
136
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
137
+ servicer = a11y_servicer.A11yServicer()
138
+ servicer.resume()
139
+ response = servicer.SendEvent(
140
+ a11y_pb2.EventRequest(event=empty_dict()), mock_context
141
+ )
142
+ self.assertEqual(response.error, '')
143
+ response = servicer.SendEvent(
144
+ a11y_pb2.EventRequest(event=single_item_dict_with_special_chars()),
145
+ mock_context,
146
+ )
147
+ self.assertEqual(response.error, '')
148
+ events = servicer.gather_events()
149
+ self.assertLen(events, 2)
150
+ self.assertEqual(events[0].event, empty_dict())
151
+ self.assertEqual(events[1].event, single_item_dict_with_special_chars())
152
+
153
+ async def test_servicer_bidi_events(self):
154
+ """Checks that the bidirectional interface accepts events."""
155
+
156
+ # Arrange.
157
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
158
+ servicer = a11y_servicer.A11yServicer()
159
+
160
+ # Act.
161
+ servicer.resume()
162
+ responses = [
163
+ x
164
+ async for x in servicer.Bidi(
165
+ _aiter([
166
+ a11y_pb2.ClientToServer(
167
+ event=a11y_pb2.EventRequest(event=empty_dict())
168
+ ),
169
+ a11y_pb2.ClientToServer(
170
+ event=a11y_pb2.EventRequest(
171
+ event=single_item_dict_with_special_chars()
172
+ )
173
+ ),
174
+ ]),
175
+ mock_context,
176
+ )
177
+ ]
178
+ events = servicer.gather_events()
179
+
180
+ # Assert.
181
+ self.assertEqual(responses[0], a11y_pb2.ServerToClient())
182
+ self.assertEqual(responses[1], a11y_pb2.ServerToClient())
183
+ self.assertLen(events, 2)
184
+ self.assertEqual(events[0].event, empty_dict())
185
+ self.assertEqual(events[1].event, single_item_dict_with_special_chars())
186
+
187
+ def test_servicer_pause_and_clear_pauses(self):
188
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
189
+ servicer = a11y_servicer.A11yServicer()
190
+ servicer.resume()
191
+ servicer.pause_and_clear()
192
+ response = servicer.SendEvent(
193
+ a11y_pb2.EventRequest(event=empty_dict()), mock_context
194
+ )
195
+ self.assertEqual(response.error, '')
196
+ response = servicer.SendForest(one_window_one_node_forest(), mock_context)
197
+ self.assertEqual(response.error, '')
198
+ events = servicer.gather_events()
199
+ self.assertEmpty(events)
200
+ forests = servicer.gather_forests()
201
+ self.assertEmpty(forests)
202
+
203
+ def test_servicer_pause_and_clear_clears(self):
204
+ mock_context = mock.create_autospec(grpc.ServicerContext, instance=True)
205
+ servicer = a11y_servicer.A11yServicer()
206
+ servicer.resume()
207
+ response = servicer.SendEvent(
208
+ a11y_pb2.EventRequest(event=empty_dict()), mock_context
209
+ )
210
+ self.assertEqual(response.error, '')
211
+ response = servicer.SendForest(one_window_one_node_forest(), mock_context)
212
+ self.assertEqual(
213
+ response.error,
214
+ '',
215
+ )
216
+ servicer.pause_and_clear()
217
+ events = servicer.gather_events()
218
+ self.assertEmpty(events)
219
+ forests = servicer.gather_forests()
220
+ self.assertEmpty(forests)
221
+
222
+
223
+ if __name__ == '__main__':
224
+ absltest.main()
@@ -0,0 +1,132 @@
1
+ # coding=utf-8
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Functions to convert actions between different components' formats."""
17
+
18
+ from absl import logging
19
+ from android_env.components import action_type as action_type_lib
20
+ from android_env.components import errors
21
+ from android_env.components import pixel_fns
22
+ from android_env.components.simulators import base_simulator
23
+ import numpy as np
24
+
25
+
26
+ def send_action_to_simulator(
27
+ action: dict[str, np.ndarray],
28
+ simulator: base_simulator.BaseSimulator,
29
+ screen_width: int,
30
+ screen_height: int,
31
+ num_fingers: int,
32
+ ) -> bool:
33
+ """Sends the selected action to the given simulator.
34
+
35
+ The simulator will interpret the action according to `action["action_type"]`.
36
+ The effect this action triggers in the Android OS will be determined by the
37
+ currently running application.
38
+
39
+ Args:
40
+ action: action which will get interpreted as a touchscreen event.
41
+ simulator: The simulator that will receive the action.
42
+ screen_width: The width of the touchscreen in pixels.
43
+ screen_height: The height of the touchscreen in pixels.
44
+ num_fingers: The number of fingers used in this simulator.
45
+ """
46
+
47
+ try:
48
+ match action['action_type']:
49
+ # If the action is a TOUCH or LIFT, send a touch event to the simulator.
50
+ case action_type_lib.ActionType.TOUCH | action_type_lib.ActionType.LIFT:
51
+ prepared_action = _prepare_touch_action(
52
+ action, screen_width, screen_height, num_fingers
53
+ )
54
+ simulator.send_touch(prepared_action)
55
+ # If the action is a key event, send a key event to the simulator.
56
+ case action_type_lib.ActionType.KEYDOWN:
57
+ simulator.send_key(action['keycode'].item(0), event_type='keydown')
58
+ case action_type_lib.ActionType.KEYUP:
59
+ simulator.send_key(action['keycode'].item(0), event_type='keyup')
60
+ case action_type_lib.ActionType.KEYPRESS:
61
+ simulator.send_key(action['keycode'].item(0), event_type='keypress')
62
+ except errors.SendActionError:
63
+ logging.exception('Unable to execute action: %r', action)
64
+ return False
65
+
66
+ return True
67
+
68
+
69
+ def _prepare_touch_action(
70
+ action: dict[str, np.ndarray],
71
+ screen_width: int,
72
+ screen_height: int,
73
+ num_fingers: int,
74
+ ) -> list[tuple[int, int, bool, int]]:
75
+ """Turns an AndroidEnv action into values that the simulator can interpret.
76
+
77
+ Converts float-valued 'touch_position' to integer coordinates corresponding
78
+ to specific pixels, and 'action_type' to booleans indicating whether the
79
+ screen is touched at said location or not. The result of this function can
80
+ be sent directly to the underlying simulator (e.g. the Android Emulator,
81
+ virtual machine, or a phone).
82
+
83
+ Args:
84
+ action: An action containing 'action_type' and 'touch_position'.
85
+
86
+ Returns:
87
+ A tuple with the format (x: int, y: int, down/up: bool, finger_index: int).
88
+ """
89
+
90
+ touch_events = []
91
+ for i, finger_action in enumerate(_split_touch_action(action, num_fingers)):
92
+ is_touch = finger_action['action_type'] == action_type_lib.ActionType.TOUCH
93
+ touch_position = finger_action['touch_position']
94
+ touch_pixels = pixel_fns.touch_position_to_pixel_position(
95
+ touch_position, width_height=(screen_width, screen_height)
96
+ )
97
+ touch_events.append((touch_pixels[0], touch_pixels[1], is_touch, i))
98
+ return touch_events
99
+
100
+
101
+ def _split_touch_action(
102
+ action: dict[str, np.ndarray], num_fingers: int
103
+ ) -> list[dict[str, np.ndarray]]:
104
+ """Splits a multitouch action into a list of single-touch actions."""
105
+
106
+ single_touch_actions = [{
107
+ 'action_type': action['action_type'],
108
+ 'touch_position': action['touch_position'],
109
+ }]
110
+ for i in range(2, num_fingers + 1):
111
+ single_touch_actions.append({
112
+ 'action_type': action[f'action_type_{i}'],
113
+ 'touch_position': action[f'touch_position_{i}'],
114
+ })
115
+ return single_touch_actions
116
+
117
+
118
+ def lift_all_fingers_action(num_fingers: int) -> dict[str, np.ndarray]:
119
+ """A lift action with each finger."""
120
+
121
+ # There's always at least one finger.
122
+ lift_action = {
123
+ 'action_type': np.array(action_type_lib.ActionType.LIFT),
124
+ 'touch_position': np.array([0, 0]),
125
+ }
126
+ # Subsequent fingers have separate dict entries.
127
+ for i in range(2, num_fingers + 1):
128
+ lift_action |= {
129
+ f'action_type_{i}': np.array(action_type_lib.ActionType.LIFT),
130
+ f'touch_position_{i}': np.array([0, 0]),
131
+ }
132
+ return lift_action