| # 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 time |
| |
| from mobly.controllers.android_device_lib import errors |
| from mobly.controllers.android_device_lib import snippet_event |
| |
| # 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 |
| |
| |
| class Error(errors.DeviceError): |
| pass |
| |
| |
| class TimeoutError(Error): |
| pass |
| |
| |
| class CallbackHandler(object): |
| """The class used to handle a specific group of callback events. |
| |
| 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 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)) |
| # Convert to milliseconds for java side. |
| timeout_ms = int(timeout * 1000) |
| try: |
| raw_event = self._event_client.eventWaitAndGet(self._id, event_name, |
| timeout_ms) |
| 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.time() + timeout |
| while time.time() <= deadline: |
| # Calculate the max timeout for the next event rpc call. |
| rpc_timeout = deadline - time.time() |
| 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._event_client.eventGetAll(self._id, event_name) |
| return [snippet_event.from_dict(msg) for msg in raw_events] |