Implemented voice call support on Android

This commit is contained in:
Mark Qvist 2025-11-06 12:11:54 +01:00
parent 7d0c9e8c4d
commit cf0d64a746
3 changed files with 119 additions and 42 deletions

View file

@ -1385,9 +1385,8 @@ class SidebandApp(MDApp):
dialog = MDDialog( dialog = MDDialog(
title="Error", title="Error",
text=info_text, text=info_text,
buttons=[ ok_button ], buttons=[ ok_button ])
# elevation=0,
)
def dl_ok(s): def dl_ok(s):
dialog.dismiss() dialog.dismiss()
self.quit_action(s) self.quit_action(s)
@ -1434,10 +1433,16 @@ class SidebandApp(MDApp):
self.hw_error_dialog.open() self.hw_error_dialog.open()
self.hw_error_dialog.is_open = True self.hw_error_dialog.is_open = True
if RNS.vendor.platformutils.is_android():
service_voice_running = self.sideband.service_voice_running()
if service_voice_running: self.sideband.voice_running = True
else: self.sideband.voice_running = False
incoming_call = self.sideband.getstate("voice.incoming_call") incoming_call = self.sideband.getstate("voice.incoming_call")
if incoming_call: if incoming_call:
self.sideband.setstate("voice.incoming_call", None) self.sideband.setstate("voice.incoming_call", None)
toast(f"Call from {incoming_call}", duration=7) if RNS.vendor.platformutils.is_android(): toast(f"Call from {incoming_call}")
else: toast(f"Call from {incoming_call}", duration=7)
if self.root.ids.screen_manager.current == "messages_screen": if self.root.ids.screen_manager.current == "messages_screen":
self.messages_view.update() self.messages_view.update()
@ -3521,10 +3526,8 @@ class SidebandApp(MDApp):
self.sideband.config["voice_enabled"] = self.settings_screen.ids.settings_voice_enabled.active self.sideband.config["voice_enabled"] = self.settings_screen.ids.settings_voice_enabled.active
self.sideband.save_configuration() self.sideband.save_configuration()
if self.sideband.config["voice_enabled"] == True: if self.sideband.config["voice_enabled"] == True: self.sideband.start_voice()
self.sideband.start_voice() else: self.sideband.stop_voice()
else:
self.sideband.stop_voice()
def save_print_command(sender=None, event=None): def save_print_command(sender=None, event=None):
if not sender.focus: if not sender.focus:
@ -5784,7 +5787,9 @@ class SidebandApp(MDApp):
def voice_answer_action(self, sender=None): def voice_answer_action(self, sender=None):
if self.sideband.voice_running: if self.sideband.voice_running:
if self.sideband.telephone.is_ringing: self.sideband.telephone.answer() if self.sideband.telephone.is_ringing:
self.sideband.telephone.answer()
toast("Call answered")
def voice_reject_action(self, sender=None): def voice_reject_action(self, sender=None):
if self.sideband.voice_running: if self.sideband.voice_running:

View file

@ -1767,6 +1767,9 @@ class SidebandCore():
def gui_conversation(self): def gui_conversation(self):
return self.getstate("app.active_conversation") return self.getstate("app.active_conversation")
def service_voice_running(self):
return self.getstate("voice.running")
def setstate(self, prop, val): def setstate(self, prop, val):
with self.state_lock: with self.state_lock:
if not self.service_stopped: if not self.service_stopped:
@ -2083,16 +2086,6 @@ class SidebandCore():
elif "set_ui_recording" in call: elif "set_ui_recording" in call:
self.service_rpc_set_ui_recording(call["set_ui_recording"]) self.service_rpc_set_ui_recording(call["set_ui_recording"])
connection.send(True) connection.send(True)
elif "get_plugins_info" in call:
connection.send(self._get_plugins_info())
elif "get_destination_establishment_rate" in call:
connection.send(self._get_destination_establishment_rate(call["get_destination_establishment_rate"]))
elif "get_destination_mtu" in call:
connection.send(self._get_destination_mtu(call["get_destination_mtu"]))
elif "get_destination_edr" in call:
connection.send(self._get_destination_edr(call["get_destination_edr"]))
elif "get_destination_lmd" in call:
connection.send(self._get_destination_lmd(call["get_destination_lmd"]))
elif "send_message" in call: elif "send_message" in call:
args = call["send_message"] args = call["send_message"]
send_result = self.send_message( send_result = self.send_message(
@ -2128,25 +2121,32 @@ class SidebandCore():
is_authorized_telemetry_request=args["is_authorized_telemetry_request"] is_authorized_telemetry_request=args["is_authorized_telemetry_request"]
) )
connection.send(send_result) connection.send(send_result)
elif "get_lxm_progress" in call: elif "get_plugins_info" in call: connection.send(self._get_plugins_info())
args = call["get_lxm_progress"] elif "get_destination_establishment_rate" in call: connection.send(self._get_destination_establishment_rate(call["get_destination_establishment_rate"]))
connection.send(self.get_lxm_progress(args["lxm_hash"])) elif "get_destination_mtu" in call: connection.send(self._get_destination_mtu(call["get_destination_mtu"]))
elif "get_lxm_stamp_cost" in call: elif "get_destination_edr" in call: connection.send(self._get_destination_edr(call["get_destination_edr"]))
args = call["get_lxm_stamp_cost"] elif "get_destination_lmd" in call: connection.send(self._get_destination_lmd(call["get_destination_lmd"]))
connection.send(self.get_lxm_stamp_cost(args["lxm_hash"])) elif "get_lxm_progress" in call: connection.send(self.get_lxm_progress(call["get_lxm_progress"]["lxm_hash"]))
elif "get_lxm_propagation_cost" in call: elif "get_lxm_stamp_cost" in call: connection.send(self.get_lxm_stamp_cost(call["get_lxm_stamp_cost"]["lxm_hash"]))
args = call["get_lxm_propagation_cost"] elif "get_lxm_propagation_cost" in call: connection.send(self.get_lxm_propagation_cost(call["get_lxm_propagation_cost"]["lxm_hash"]))
connection.send(self.get_lxm_propagation_cost(args["lxm_hash"])) elif "is_tracking" in call: connection.send(self.is_tracking(call["is_tracking"]))
elif "is_tracking" in call: elif "start_tracking" in call: connection.send(self.start_tracking(object_addr=call["start_tracking"]["object_addr"], interval=args["interval"], duration=args["duration"]))
connection.send(self.is_tracking(call["is_tracking"])) elif "stop_tracking" in call: connection.send(self.stop_tracking(object_addr=call["stop_tracking"]["object_addr"]))
elif "start_tracking" in call: elif "get_service_log" in call: connection.send(self.get_service_log())
args = call["start_tracking"] elif "start_voice" in call: connection.send(self.start_voice())
connection.send(self.start_tracking(object_addr=args["object_addr"], interval=args["interval"], duration=args["duration"])) elif "stop_voice" in call: connection.send(self.stop_voice())
elif "stop_tracking" in call: elif "telephone_is_available" in call: connection.send(self.telephone.is_available) if self.telephone else False
args = call["stop_tracking"] elif "telephone_is_in_call" in call: connection.send(self.telephone.is_in_call) if self.telephone else False
connection.send(self.stop_tracking(object_addr=args["object_addr"])) elif "telephone_call_is_connecting" in call: connection.send(self.telephone.call_is_connecting) if self.telephone else False
elif "get_service_log" in call: elif "telephone_is_ringing" in call: connection.send(self.telephone.is_ringing) if self.telephone else False
connection.send(self.get_service_log()) elif "telephone_caller_info" in call: connection.send(self.telephone.caller.hash) if self.telephone and self.telephone.caller else None
elif "telephone_set_busy" in call: connection.send(self.telephone.set_busy(call["telephone_set_busy"])) if self.telephone else False
elif "telephone_dial" in call: connection.send(self.telephone.dial(call["telephone_dial"])) if self.telephone else False
elif "telephone_hangup" in call: connection.send(self.telephone.hangup()) if self.telephone else False
elif "telephone_answer" in call: connection.send(self.telephone.answer()) if self.telephone else False
elif "telephone_set_speaker" in call: connection.send(self.telephone.set_speaker(call["telephone_set_speaker"])) if self.telephone else False
elif "telephone_set_microphone" in call: connection.send(self.telephone.set_microphone(call["telephone_set_microphone"])) if self.telephone else False
elif "telephone_set_ringer" in call: connection.send(self.telephone.set_ringer(call["telephone_set_ringer"])) if self.telephone else False
else: else:
connection.send(None) connection.send(None)
@ -3896,8 +3896,14 @@ class SidebandCore():
if self.is_standalone or self.is_client: if self.is_standalone or self.is_client:
if self.config["telemetry_enabled"]: self.run_telemetry() if self.config["telemetry_enabled"]: self.run_telemetry()
if self.config["voice_enabled"]: self.start_voice() if self.config["voice_enabled"]:
if not RNS.vendor.platformutils.is_android(): self.start_voice()
else:
from .voice import ReticulumTelephoneProxy
self.telephone = ReticulumTelephoneProxy(owner=self)
elif self.is_service: elif self.is_service:
if self.config["voice_enabled"]: self.start_voice()
self.run_service_telemetry() self.run_service_telemetry()
def __add_localinterface(self, delay=None): def __add_localinterface(self, delay=None):
@ -5501,22 +5507,40 @@ class SidebandCore():
if not self.reticulum.is_connected_to_shared_instance: if not self.reticulum.is_connected_to_shared_instance:
RNS.Transport.detach_interfaces() RNS.Transport.detach_interfaces()
def start_voice(self): def _start_voice(self):
try: try:
if not self.voice_running: if not self.voice_running:
RNS.log("Starting voice service", RNS.LOG_DEBUG) RNS.log("Starting voice service", RNS.LOG_DEBUG)
self.voice_running = True self.voice_running = True
self.setstate("voice.running", self.voice_running)
from .voice import ReticulumTelephone from .voice import ReticulumTelephone
self.telephone = ReticulumTelephone(self.identity, owner=self, speaker=self.config["voice_output"], microphone=self.config["voice_input"], ringer=self.config["voice_ringer"]) self.telephone = ReticulumTelephone(self.identity, owner=self, speaker=self.config["voice_output"], microphone=self.config["voice_input"], ringer=self.config["voice_ringer"])
ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus") ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus")
self.telephone.set_ringtone(ringtone_path) self.telephone.set_ringtone(ringtone_path)
return True
except Exception as e: except Exception as e:
self.voice_running = False self.voice_running = False
RNS.log(f"An error occurred while starting voice services, the contained exception was: {e}", RNS.LOG_ERROR) RNS.log(f"An error occurred while starting voice services, the contained exception was: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e) RNS.trace_exception(e)
return False
def stop_voice(self): def start_voice(self):
if not RNS.vendor.platformutils.is_android(): return self._start_voice()
else:
if self.is_service: return self._start_voice()
else:
try:
if self.service_rpc_request({"start_voice": True}):
from .voice import ReticulumTelephoneProxy
self.telephone = ReticulumTelephoneProxy(owner=self)
self.voice_running = True
except Exception as e:
RNS.log("Error while starting voice service over RPC: "+str(e), RNS.LOG_DEBUG)
return False
def _stop_voice(self):
try: try:
if self.voice_running: if self.voice_running:
RNS.log("Stopping voice service", RNS.LOG_DEBUG) RNS.log("Stopping voice service", RNS.LOG_DEBUG)
@ -5526,10 +5550,27 @@ class SidebandCore():
self.telephone = None self.telephone = None
self.voice_running = False self.voice_running = False
self.setstate("voice.running", self.voice_running)
return True
except Exception as e: except Exception as e:
RNS.log(f"An error occurred while stopping voice services, the contained exception was: {e}", RNS.LOG_ERROR) RNS.log(f"An error occurred while stopping voice services, the contained exception was: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e) RNS.trace_exception(e)
return False
def stop_voice(self):
if not RNS.vendor.platformutils.is_android(): return self._stop_voice()
else:
if self.is_service: return self._stop_voice()
else:
try:
if self.service_rpc_request({"stop_voice": True}):
self.telephone = None
self.voice_running = False
except Exception as e:
RNS.log("Error while stopping voice service over RPC: "+str(e), RNS.LOG_DEBUG)
return False
def incoming_call(self, remote_identity): def incoming_call(self, remote_identity):
display_name = self.voice_display_name(remote_identity.hash) display_name = self.voice_display_name(remote_identity.hash)

View file

@ -44,7 +44,7 @@ class ReticulumTelephone():
self.aliases = {} self.aliases = {}
self.names = {} self.names = {}
self.telephone = Telephone(self.identity, ring_time=self.RING_TIME, wait_time=self.WAIT_TIME) self.telephone = Telephone(self.identity, ring_time=self.RING_TIME, wait_time=self.WAIT_TIME)
self.telephone.set_ringing_callback(self.ringing) self.telephone.set_ringing_callback(self.ringing)
self.telephone.set_established_callback(self.call_established) self.telephone.set_established_callback(self.call_established)
self.telephone.set_ended_callback(self.call_ended) self.telephone.set_ended_callback(self.call_ended)
@ -169,3 +169,34 @@ class ReticulumTelephone():
return False return False
else: else:
return True return True
class CallerProxy():
def __init__(self, hash=None):
self.hash = hash
class ReticulumTelephoneProxy():
PATH_TIME = ReticulumTelephone.PATH_TIME
def __init__(self, owner=None): self.owner = owner
@property
def is_available(self): return self.owner.service_rpc_request({"telephone_is_available": True })
@property
def is_in_call(self): return self.owner.service_rpc_request({"telephone_is_in_call": True })
@property
def call_is_connecting(self): return self.owner.service_rpc_request({"telephone_call_is_connecting": True })
@property
def is_ringing(self): return self.owner.service_rpc_request({"telephone_is_ringing": True })
@property
def caller(self): return CallerProxy(hash=self.owner.service_rpc_request({"telephone_caller_info": True }))
def set_busy(self, busy): return self.owner.service_rpc_request({"telephone_set_busy": busy })
def dial(self, dial_target): return self.owner.service_rpc_request({"telephone_dial": dial_target })
def hangup(self): return self.owner.service_rpc_request({"telephone_hangup": True })
def answer(self): return self.owner.service_rpc_request({"telephone_answer": True })
def set_speaker(self, speaker): return self.owner.service_rpc_request({"telephone_set_speaker": speaker })
def set_microphone(self, microphone): return self.owner.service_rpc_request({"telephone_set_microphone": microphone })
def set_ringer(self, ringer): return self.owner.service_rpc_request({"telephone_set_ringer": ringer })