Allow custom error messages in snippet `waitForEvent`. (#989)

diff --git a/mobly/snippet/callback_handler_base.py b/mobly/snippet/callback_handler_base.py
index 95b5a4b..992d971 100644
--- a/mobly/snippet/callback_handler_base.py
+++ b/mobly/snippet/callback_handler_base.py
@@ -14,6 +14,7 @@
 """Module for the base class to handle Mobly Snippet Lib's callback events."""
 import abc
 import time
+from typing import Callable
 
 from mobly.snippet import callback_event
 from mobly.snippet import errors
@@ -178,7 +179,13 @@
     raw_event = self.callEventWaitAndGetRpc(self._id, event_name, timeout)
     return callback_event.from_dict(raw_event)
 
-  def waitForEvent(self, event_name, predicate, timeout=None):
+  def waitForEvent(
+      self,
+      event_name: str,
+      predicate: Callable[[callback_event.CallbackEvent], bool],
+      timeout: float | None = None,
+      message: str | None = None,
+  ) -> callback_event.CallbackEvent:
     """Waits for an event of the specific name that satisfies the predicate.
 
     This call will block until the expected event has been received or time
@@ -194,13 +201,13 @@
 
     Args:
       event_name: str, the name of the event to wait for.
-      predicate: function, a function that takes an event (dictionary) and
-        returns a bool.
+      predicate: function, the predicate used to test events.
       timeout: float, the number of seconds to wait before giving up. If None,
         it will be set to self.default_timeout_sec.
+      message: str, an optional error message to include if there is a timeout.
 
     Returns:
-      dictionary, the event that satisfies the predicate if received.
+      CallbackEvent, the event that satisfies the predicate if received.
 
     Raises:
       errors.CallbackHandlerTimeoutError: raised if no event that satisfies the
@@ -225,10 +232,11 @@
       if predicate(event):
         return event
 
+    custom_error = '' if message is None else f' Details: {message}.'
     raise errors.CallbackHandlerTimeoutError(
         self._device,
         f'Timed out after {timeout}s waiting for an "{event_name}" event that '
-        f'satisfies the predicate "{predicate.__name__}".',
+        f'satisfies the predicate "{predicate.__name__}".{custom_error}',
     )
 
   def getAll(self, event_name):
diff --git a/tests/mobly/snippet/callback_handler_base_test.py b/tests/mobly/snippet/callback_handler_base_test.py
index 32199b2..273df06 100644
--- a/tests/mobly/snippet/callback_handler_base_test.py
+++ b/tests/mobly/snippet/callback_handler_base_test.py
@@ -153,6 +153,24 @@
     event = handler.waitForEvent('AsyncTaskResult', some_condition, 0.01)
     self.assert_event_correct(event, MOCK_RAW_EVENT)
 
+  def test_wait_for_event_negative_with_custom_message(self):
+    handler = FakeCallbackHandler()
+    handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+        return_value=MOCK_RAW_EVENT
+    )
+
+    expected_msg = (
+        'Timed out after 0.01s waiting for an "AsyncTaskResult" event that'
+        ' satisfies the predicate "<lambda>". Details: Failed for test.'
+    )
+
+    with self.assertRaisesRegex(
+        errors.CallbackHandlerTimeoutError, expected_msg
+    ):
+      handler.waitForEvent(
+          'AsyncTaskResult', lambda _: False, 0.01, 'Failed for test'
+      )
+
   def test_wait_for_event_negative(self):
     handler = FakeCallbackHandler()
     handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(