From 4389b643d9e4e528a9c0b3a63d3c26c07dcbd192 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sat, 25 Apr 2026 16:28:44 -0500 Subject: [PATCH] feat(android): update meshchat_wrapper with server loop control and improve error handling for Android notification bridge --- .../app/src/main/python/meshchat_wrapper.py | 22 ++++++- meshchatx/android_push_bridge.py | 62 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/python/meshchat_wrapper.py b/android/app/src/main/python/meshchat_wrapper.py index 91813e2..488d4ae 100644 --- a/android/app/src/main/python/meshchat_wrapper.py +++ b/android/app/src/main/python/meshchat_wrapper.py @@ -3,6 +3,11 @@ import os import signal import sys +import threading + +# Prevents a second meshchat main() if Java starts two threads (e.g. activity edge cases). +_server_loop_lock = threading.Lock() +_server_loop_active = False def _ensure_android_reticulum_config(reticulum_config_dir): @@ -67,6 +72,12 @@ def _patch_aiohttp_run_app_for_android(): def start_server(port=8000, app_files_dir=None): + global _server_loop_active + with _server_loop_lock: + if _server_loop_active: + print("meshchat_wrapper: start_server ignored (server loop already active)") + return + _server_loop_active = True try: storage_dir = None reticulum_config_dir = None @@ -91,10 +102,14 @@ def start_server(port=8000, app_files_dir=None): signal.signal = _safe_signal asyncio_signal_patch = _patch_asyncio_signal_handlers_for_android() aiohttp_run_app_patch = _patch_aiohttp_run_app_for_android() - from meshchatx.android_push_bridge import install_websocket_hook from meshchatx.meshchat import ReticulumMeshChat, main - install_websocket_hook(ReticulumMeshChat) + try: + from meshchatx.android_push_bridge import install_websocket_hook + + install_websocket_hook(ReticulumMeshChat) + except Exception as hook_exc: + print(f"meshchat_wrapper: install_websocket_hook skipped: {hook_exc}") sys.argv = [ "meshchat", @@ -125,3 +140,6 @@ def start_server(port=8000, app_files_dir=None): traceback.print_exc() raise + finally: + with _server_loop_lock: + _server_loop_active = False diff --git a/meshchatx/android_push_bridge.py b/meshchatx/android_push_bridge.py index 6aa190b..0291f14 100644 --- a/meshchatx/android_push_bridge.py +++ b/meshchatx/android_push_bridge.py @@ -73,6 +73,42 @@ def _notify_java(title: str, body: str, dedupe_hex: str | None) -> None: logger.debug("showInboundMessage failed: %s", exc) +def _notify_incoming_call_java(caller_name: str, dedupe_hex: str | None) -> None: + try: + from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped] + except Exception as exc: + logger.debug("Android notification bridge unavailable: %s", exc) + return + try: + AndroidNotificationBridge.showIncomingCall(caller_name, dedupe_hex) + except Exception as exc: + logger.debug("showIncomingCall failed: %s", exc) + + +def _notify_missed_call_java(title: str, body: str, dedupe_hex: str | None) -> None: + try: + from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped] + except Exception as exc: + logger.debug("Android notification bridge unavailable: %s", exc) + return + try: + AndroidNotificationBridge.showMissedCall(title, body, dedupe_hex) + except Exception as exc: + logger.debug("showMissedCall failed: %s", exc) + + +def _cancel_incoming_call_notification_java() -> None: + try: + from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped] + except Exception as exc: + logger.debug("Android notification bridge unavailable: %s", exc) + return + try: + AndroidNotificationBridge.cancelIncomingCallNotification() + except Exception as exc: + logger.debug("cancelIncomingCallNotification failed: %s", exc) + + def _after_websocket_broadcast(data: object) -> None: if not isinstance(data, str): return @@ -82,6 +118,32 @@ def _after_websocket_broadcast(data: object) -> None: return if not isinstance(payload, dict): return + t = payload.get("type") + if t in ("telephone_call_ended", "telephone_call_established"): + _cancel_incoming_call_notification_java() + return + if t == "telephone_ringing": + ch = payload.get("remote_identity_hash") + name = (payload.get("remote_identity_name") or "").strip() or "Mesh" + ded = ch if isinstance(ch, str) and len(ch) >= 8 else None + _notify_incoming_call_java(name, ded) + return + if t == "telephone_missed_call": + sender = (payload.get("remote_identity_name") or "").strip() or "Mesh" + ch = payload.get("remote_identity_hash") + h = ch if isinstance(ch, str) and len(ch) >= 8 else None + if sender and sender != "Mesh": + title = "Missed call" + body = f"Missed call from {sender}" + elif isinstance(ch, str) and ch: + short_h = f"{ch[:6]}" if len(ch) > 6 else ch + title = "Missed call" + body = f"From {short_h}..." + else: + title = "Missed call" + body = "Missed call" + _notify_missed_call_java(title, body, h) + return pair = lxmf_delivery_notification_text(payload) if not pair: return