1# Copyright 2017 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import logging
16import time
17
18from mobly.controllers.android_device_lib import snippet_event
19from mobly.snippet import errors
20
21logging.warning(
22    'The module mobly.controllers.android_device_lib.callback_handler is '
23    'deprecated and will be removed in a future version. Use module '
24    'mobly.controllers.android_device_lib.callback_handler_v2 instead.'
25)
26
27# The max timeout cannot be larger than the max time the socket waits for a
28# response message. Otherwise, the socket would timeout before the Rpc call
29# does, leaving both server and client in unknown states.
30MAX_TIMEOUT = 60 * 10
31DEFAULT_TIMEOUT = 120  # two minutes
32
33# Aliases of error types for backward compatibility.
34Error = errors.CallbackHandlerBaseError
35TimeoutError = errors.CallbackHandlerTimeoutError
36
37
38class CallbackHandler:
39  """The class used to handle a specific group of callback events.
40
41  DEPRECATED: Use
42  mobly.controllers.android_device_lib.callback_handler_v2.CallbackHandlerV2
43  instead.
44
45  All the events handled by a CallbackHandler are originally triggered by one
46  async Rpc call. All the events are tagged with a callback_id specific to a
47  call to an AsyncRpc method defined on the server side.
48
49  The raw message representing an event looks like:
50
51  .. code-block:: python
52
53    {
54      'callbackId': <string, callbackId>,
55      'name': <string, name of the event>,
56      'time': <long, epoch time of when the event was created on the
57        server side>,
58      'data': <dict, extra data from the callback on the server side>
59    }
60
61  Each message is then used to create a SnippetEvent object on the client
62  side.
63
64  Attributes:
65    ret_value: The direct return value of the async Rpc call.
66  """
67
68  def __init__(self, callback_id, event_client, ret_value, method_name, ad):
69    self._id = callback_id
70    self._event_client = event_client
71    self.ret_value = ret_value
72    self._method_name = method_name
73    self._ad = ad
74
75  @property
76  def callback_id(self):
77    return self._id
78
79  def _callEventWaitAndGet(self, callback_id, event_name, timeout):
80    """Calls snippet lib's eventWaitAndGet.
81
82    Override this method to use this class with various snippet lib
83    implementations.
84
85    Args:
86      callback_id: The callback identifier.
87      event_name: The callback name.
88      timeout: The number of seconds to wait for the event.
89
90    Returns:
91      The event dictionary.
92    """
93    # Convert to milliseconds for Java side.
94    timeout_ms = int(timeout * 1000)
95    return self._event_client.eventWaitAndGet(
96        callback_id, event_name, timeout_ms
97    )
98
99  def _callEventGetAll(self, callback_id, event_name):
100    """Calls snippet lib's eventGetAll.
101
102    Override this method to use this class with various snippet lib
103    implementations.
104
105    Args:
106      callback_id: The callback identifier.
107      event_name: The callback name.
108
109    Returns:
110      A list of event dictionaries.
111    """
112    return self._event_client.eventGetAll(callback_id, event_name)
113
114  def waitAndGet(self, event_name, timeout=DEFAULT_TIMEOUT):
115    """Blocks until an event of the specified name has been received and
116    return the event, or timeout.
117
118    Args:
119      event_name: string, name of the event to get.
120      timeout: float, the number of seconds to wait before giving up.
121
122    Returns:
123      SnippetEvent, the oldest entry of the specified event.
124
125    Raises:
126      Error: If the specified timeout is longer than the max timeout
127        supported.
128      TimeoutError: The expected event does not occur within time limit.
129    """
130    if timeout:
131      if timeout > MAX_TIMEOUT:
132        raise Error(
133            self._ad,
134            'Specified timeout %s is longer than max timeout %s.'
135            % (timeout, MAX_TIMEOUT),
136        )
137    try:
138      raw_event = self._callEventWaitAndGet(self._id, event_name, timeout)
139    except Exception as e:
140      if 'EventSnippetException: timeout.' in str(e):
141        raise TimeoutError(
142            self._ad,
143            'Timed out after waiting %ss for event "%s" triggered by %s (%s).'
144            % (timeout, event_name, self._method_name, self._id),
145        )
146      raise
147    return snippet_event.from_dict(raw_event)
148
149  def waitForEvent(self, event_name, predicate, timeout=DEFAULT_TIMEOUT):
150    """Wait for an event of a specific name that satisfies the predicate.
151
152    This call will block until the expected event has been received or time
153    out.
154
155    The predicate function defines the condition the event is expected to
156    satisfy. It takes an event and returns True if the condition is
157    satisfied, False otherwise.
158
159    Note all events of the same name that are received but don't satisfy
160    the predicate will be discarded and not be available for further
161    consumption.
162
163    Args:
164      event_name: string, the name of the event to wait for.
165      predicate: function, a function that takes an event (dictionary) and
166        returns a bool.
167      timeout: float, default is 120s.
168
169    Returns:
170      dictionary, the event that satisfies the predicate if received.
171
172    Raises:
173      TimeoutError: raised if no event that satisfies the predicate is
174        received after timeout seconds.
175    """
176    deadline = time.perf_counter() + timeout
177    while time.perf_counter() <= deadline:
178      # Calculate the max timeout for the next event rpc call.
179      rpc_timeout = deadline - time.perf_counter()
180      if rpc_timeout < 0:
181        break
182      # A single RPC call cannot exceed MAX_TIMEOUT.
183      rpc_timeout = min(rpc_timeout, MAX_TIMEOUT)
184      try:
185        event = self.waitAndGet(event_name, rpc_timeout)
186      except TimeoutError:
187        # Ignoring TimeoutError since we need to throw one with a more
188        # specific message.
189        break
190      if predicate(event):
191        return event
192    raise TimeoutError(
193        self._ad,
194        'Timed out after %ss waiting for an "%s" event that satisfies the '
195        'predicate "%s".' % (timeout, event_name, predicate.__name__),
196    )
197
198  def getAll(self, event_name):
199    """Gets all the events of a certain name that have been received so
200    far. This is a non-blocking call.
201
202    Args:
203      callback_id: The id of the callback.
204      event_name: string, the name of the event to get.
205
206    Returns:
207      A list of SnippetEvent, each representing an event from the Java
208      side.
209    """
210    raw_events = self._callEventGetAll(self._id, event_name)
211    return [snippet_event.from_dict(msg) for msg in raw_events]
212