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.
- android_env/__init__.py +1 -1
- android_env/components/__init__.py +1 -1
- android_env/components/a11y/__init__.py +15 -0
- android_env/components/a11y/a11y_events.py +118 -0
- android_env/components/a11y/a11y_events_test.py +173 -0
- android_env/components/a11y/a11y_forests.py +128 -0
- android_env/components/a11y/a11y_forests_test.py +237 -0
- android_env/components/a11y/a11y_servicer.py +199 -0
- android_env/components/a11y/a11y_servicer_test.py +224 -0
- android_env/components/action_fns.py +132 -0
- android_env/components/action_fns_test.py +227 -0
- android_env/components/action_type.py +26 -3
- android_env/components/adb_call_parser.py +239 -196
- android_env/components/adb_call_parser_test.py +179 -209
- android_env/components/adb_controller.py +90 -52
- android_env/components/adb_controller_test.py +187 -16
- android_env/components/adb_log_stream.py +17 -5
- android_env/components/adb_log_stream_test.py +17 -3
- android_env/components/app_screen_checker.py +17 -15
- android_env/components/app_screen_checker_test.py +7 -8
- android_env/components/config_classes.py +203 -0
- android_env/components/coordinator.py +102 -338
- android_env/components/coordinator_test.py +59 -199
- android_env/components/device_settings.py +174 -0
- android_env/components/device_settings_test.py +228 -0
- android_env/components/dumpsys_thread.py +3 -4
- android_env/components/dumpsys_thread_test.py +1 -1
- android_env/components/errors.py +52 -10
- android_env/components/errors_test.py +110 -0
- android_env/components/log_stream.py +7 -5
- android_env/components/log_stream_test.py +1 -1
- android_env/components/logcat_thread.py +9 -8
- android_env/components/logcat_thread_test.py +3 -4
- android_env/components/{utils.py → pixel_fns.py} +20 -20
- android_env/components/{utils_test.py → pixel_fns_test.py} +20 -15
- android_env/components/setup_step_interpreter.py +47 -39
- android_env/components/setup_step_interpreter_test.py +4 -4
- android_env/components/simulators/__init__.py +1 -1
- android_env/components/simulators/base_simulator.py +116 -44
- android_env/components/simulators/base_simulator_test.py +131 -9
- android_env/components/simulators/emulator/__init__.py +1 -1
- android_env/components/simulators/emulator/emulator_launcher.py +67 -77
- android_env/components/simulators/emulator/emulator_launcher_test.py +153 -49
- android_env/components/simulators/emulator/emulator_simulator.py +276 -95
- android_env/components/simulators/emulator/emulator_simulator_test.py +314 -89
- android_env/components/simulators/fake/__init__.py +1 -1
- android_env/components/simulators/fake/fake_simulator.py +17 -25
- android_env/components/simulators/fake/fake_simulator_test.py +29 -12
- android_env/components/specs.py +18 -28
- android_env/components/specs_test.py +1 -44
- android_env/components/task_manager.py +48 -48
- android_env/components/task_manager_test.py +71 -60
- android_env/env_interface.py +37 -23
- android_env/environment.py +83 -51
- android_env/environment_test.py +68 -29
- android_env/loader.py +57 -43
- android_env/loader_test.py +115 -35
- android_env/proto/__init__.py +1 -1
- android_env/proto/a11y/__init__.py +15 -0
- android_env/proto/a11y/a11y.proto +75 -0
- android_env/proto/a11y/a11y_pb2.py +54 -0
- android_env/proto/a11y/a11y_pb2.pyi +49 -0
- android_env/proto/a11y/a11y_pb2_grpc.py +202 -0
- android_env/proto/a11y/android_accessibility_action.proto +32 -0
- android_env/proto/a11y/android_accessibility_action_pb2.py +37 -0
- android_env/proto/a11y/android_accessibility_action_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_action_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_forest.proto +29 -0
- android_env/proto/a11y/android_accessibility_forest_pb2.py +38 -0
- android_env/proto/a11y/android_accessibility_forest_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_forest_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_node_info.proto +122 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span.proto +49 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.py +39 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.pyi +28 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2.py +42 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2.pyi +75 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_tree.proto +29 -0
- android_env/proto/a11y/android_accessibility_tree_pb2.py +38 -0
- android_env/proto/a11y/android_accessibility_tree_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_tree_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_window_info.proto +84 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2.py +41 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2.pyi +48 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2_grpc.py +24 -0
- android_env/proto/a11y/rect.proto +30 -0
- android_env/proto/a11y/rect_pb2.py +37 -0
- android_env/proto/a11y/rect_pb2.pyi +17 -0
- android_env/proto/a11y/rect_pb2_grpc.py +24 -0
- android_env/proto/adb.proto +17 -6
- android_env/proto/adb_pb2.py +120 -107
- android_env/proto/adb_pb2.pyi +396 -0
- android_env/proto/adb_pb2_grpc.py +20 -0
- android_env/proto/emulator_controller.proto +68 -63
- android_env/proto/emulator_controller_pb2.py +142 -131
- android_env/proto/emulator_controller_pb2.pyi +672 -0
- android_env/proto/emulator_controller_pb2_grpc.py +505 -142
- android_env/proto/snapshot.proto +169 -0
- android_env/proto/snapshot_pb2.py +47 -0
- android_env/proto/snapshot_pb2.pyi +117 -0
- android_env/proto/snapshot_pb2_grpc.py +24 -0
- android_env/proto/snapshot_service.proto +289 -0
- android_env/proto/snapshot_service_pb2.py +54 -0
- android_env/proto/snapshot_service_pb2.pyi +86 -0
- android_env/proto/snapshot_service_pb2_grpc.py +487 -0
- android_env/proto/state.proto +63 -0
- android_env/proto/state_pb2.py +63 -0
- android_env/proto/state_pb2.pyi +85 -0
- android_env/proto/state_pb2_grpc.py +24 -0
- android_env/proto/task.proto +5 -1
- android_env/proto/task_pb2.py +42 -31
- android_env/proto/task_pb2.pyi +160 -0
- android_env/proto/task_pb2_grpc.py +20 -0
- android_env/wrappers/__init__.py +1 -1
- android_env/wrappers/a11y_grpc_wrapper.py +500 -0
- android_env/wrappers/a11y_grpc_wrapper_test.py +849 -0
- android_env/wrappers/base_wrapper.py +34 -13
- android_env/wrappers/base_wrapper_test.py +22 -16
- android_env/wrappers/discrete_action_wrapper.py +18 -17
- android_env/wrappers/discrete_action_wrapper_test.py +4 -4
- android_env/wrappers/flat_interface_wrapper.py +5 -5
- android_env/wrappers/flat_interface_wrapper_test.py +7 -11
- android_env/wrappers/float_pixels_wrapper.py +9 -10
- android_env/wrappers/float_pixels_wrapper_test.py +3 -3
- android_env/wrappers/gym_wrapper.py +19 -13
- android_env/wrappers/gym_wrapper_test.py +3 -5
- android_env/wrappers/image_rescale_wrapper.py +18 -21
- android_env/wrappers/image_rescale_wrapper_test.py +25 -37
- android_env/wrappers/last_action_wrapper.py +16 -13
- android_env/wrappers/last_action_wrapper_test.py +44 -51
- android_env/wrappers/rate_limit_wrapper.py +6 -3
- android_env/wrappers/rate_limit_wrapper_test.py +22 -1
- android_env/wrappers/tap_action_wrapper.py +16 -17
- android_env/wrappers/tap_action_wrapper_test.py +51 -16
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/METADATA +14 -18
- android_env-1.2.3.dist-info/RECORD +141 -0
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/WHEEL +1 -1
- android_env/proto/raw_observation.proto +0 -39
- android_env/proto/raw_observation_pb2.py +0 -27
- android_env/proto/raw_observation_pb2_grpc.py +0 -4
- android_env-1.2.1.dist-info/RECORD +0 -81
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info/licenses}/LICENSE +0 -0
- {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
|