aboutsummaryrefslogtreecommitdiff
path: root/mobly/controllers/android_device_lib/callback_handler.py
blob: c783b717be8f1dfc2d61531fa47442e7b6cf3c03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import time

from mobly.controllers.android_device_lib import snippet_event
from mobly.snippet import errors

logging.warning(
    'The module mobly.controllers.android_device_lib.callback_handler is '
    'deprecated and will be removed in a future version. Use module '
    'mobly.controllers.android_device_lib.callback_handler_v2 instead.')

# The max timeout cannot be larger than the max time the socket waits for a
# response message. Otherwise, the socket would timeout before the Rpc call
# does, leaving both server and client in unknown states.
MAX_TIMEOUT = 60 * 10
DEFAULT_TIMEOUT = 120  # two minutes

# Aliases of error types for backward compatibility.
Error = errors.CallbackHandlerBaseError
TimeoutError = errors.CallbackHandlerTimeoutError


class CallbackHandler:
  """The class used to handle a specific group of callback events.

  DEPRECATED: Use
  mobly.controllers.android_device_lib.callback_handler_v2.CallbackHandlerV2
  instead.

  All the events handled by a CallbackHandler are originally triggered by one
  async Rpc call. All the events are tagged with a callback_id specific to a
  call to an AsyncRpc method defined on the server side.

  The raw message representing an event looks like:

  .. code-block:: python

    {
      'callbackId': <string, callbackId>,
      'name': <string, name of the event>,
      'time': <long, epoch time of when the event was created on the
        server side>,
      'data': <dict, extra data from the callback on the server side>
    }

  Each message is then used to create a SnippetEvent object on the client
  side.

  Attributes:
    ret_value: The direct return value of the async Rpc call.
  """

  def __init__(self, callback_id, event_client, ret_value, method_name, ad):
    self._id = callback_id
    self._event_client = event_client
    self.ret_value = ret_value
    self._method_name = method_name
    self._ad = ad

  @property
  def callback_id(self):
    return self._id

  def _callEventWaitAndGet(self, callback_id, event_name, timeout):
    """Calls snippet lib's eventWaitAndGet.

    Override this method to use this class with various snippet lib
    implementations.

    Args:
      callback_id: The callback identifier.
      event_name: The callback name.
      timeout: The number of seconds to wait for the event.

    Returns:
      The event dictionary.
    """
    # Convert to milliseconds for Java side.
    timeout_ms = int(timeout * 1000)
    return self._event_client.eventWaitAndGet(callback_id, event_name,
                                              timeout_ms)

  def _callEventGetAll(self, callback_id, event_name):
    """Calls snippet lib's eventGetAll.

    Override this method to use this class with various snippet lib
    implementations.

    Args:
      callback_id: The callback identifier.
      event_name: The callback name.

    Returns:
      A list of event dictionaries.
    """
    return self._event_client.eventGetAll(callback_id, event_name)

  def waitAndGet(self, event_name, timeout=DEFAULT_TIMEOUT):
    """Blocks until an event of the specified name has been received and
    return the event, or timeout.

    Args:
      event_name: string, name of the event to get.
      timeout: float, the number of seconds to wait before giving up.

    Returns:
      SnippetEvent, the oldest entry of the specified event.

    Raises:
      Error: If the specified timeout is longer than the max timeout
        supported.
      TimeoutError: The expected event does not occur within time limit.
    """
    if timeout:
      if timeout > MAX_TIMEOUT:
        raise Error(
            self._ad, 'Specified timeout %s is longer than max timeout %s.' %
            (timeout, MAX_TIMEOUT))
    try:
      raw_event = self._callEventWaitAndGet(self._id, event_name, timeout)
    except Exception as e:
      if 'EventSnippetException: timeout.' in str(e):
        raise TimeoutError(
            self._ad, 'Timed out after waiting %ss for event "%s" triggered by'
            ' %s (%s).' % (timeout, event_name, self._method_name, self._id))
      raise
    return snippet_event.from_dict(raw_event)

  def waitForEvent(self, event_name, predicate, timeout=DEFAULT_TIMEOUT):
    """Wait for an event of a specific name that satisfies the predicate.

    This call will block until the expected event has been received or time
    out.

    The predicate function defines the condition the event is expected to
    satisfy. It takes an event and returns True if the condition is
    satisfied, False otherwise.

    Note all events of the same name that are received but don't satisfy
    the predicate will be discarded and not be available for further
    consumption.

    Args:
      event_name: string, the name of the event to wait for.
      predicate: function, a function that takes an event (dictionary) and
        returns a bool.
      timeout: float, default is 120s.

    Returns:
      dictionary, the event that satisfies the predicate if received.

    Raises:
      TimeoutError: raised if no event that satisfies the predicate is
        received after timeout seconds.
    """
    deadline = time.perf_counter() + timeout
    while time.perf_counter() <= deadline:
      # Calculate the max timeout for the next event rpc call.
      rpc_timeout = deadline - time.perf_counter()
      if rpc_timeout < 0:
        break
      # A single RPC call cannot exceed MAX_TIMEOUT.
      rpc_timeout = min(rpc_timeout, MAX_TIMEOUT)
      try:
        event = self.waitAndGet(event_name, rpc_timeout)
      except TimeoutError:
        # Ignoring TimeoutError since we need to throw one with a more
        # specific message.
        break
      if predicate(event):
        return event
    raise TimeoutError(
        self._ad,
        'Timed out after %ss waiting for an "%s" event that satisfies the '
        'predicate "%s".' % (timeout, event_name, predicate.__name__))

  def getAll(self, event_name):
    """Gets all the events of a certain name that have been received so
    far. This is a non-blocking call.

    Args:
      callback_id: The id of the callback.
      event_name: string, the name of the event to get.

    Returns:
      A list of SnippetEvent, each representing an event from the Java
      side.
    """
    raw_events = self._callEventGetAll(self._id, event_name)
    return [snippet_event.from_dict(msg) for msg in raw_events]