Added simple call log to voice call screen

This commit is contained in:
Mark Qvist 2025-11-09 13:58:40 +01:00
commit 8121de96d7
5 changed files with 266 additions and 59 deletions

View file

@ -60,7 +60,7 @@ public class PythonActivity extends SDLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
try { this.startIntent = getIntent(); }
catch { Log.e(TAG, "Failed to get pending intent on activity create"); }
catch (Exception e) { Log.e(TAG, "Failed to get pending intent on activity create"); }
Log.v(TAG, "PythonActivity onCreate running");
resourceManager = new ResourceManager(this);

View file

@ -2138,20 +2138,21 @@ class SidebandCore():
elif "get_service_log" in call: connection.send(self.get_service_log())
elif "start_voice" in call: connection.send(self.start_voice())
elif "stop_voice" in call: connection.send(self.stop_voice())
elif "telephone_is_available" in call: connection.send(self.telephone.is_available) if self.telephone else False
elif "telephone_is_in_call" in call: connection.send(self.telephone.is_in_call) if self.telephone else False
elif "telephone_call_is_connecting" in call: connection.send(self.telephone.call_is_connecting) if self.telephone else False
elif "telephone_is_ringing" in call: connection.send(self.telephone.is_ringing) if self.telephone else False
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
elif "telephone_set_low_latency_output" in call: connection.send(self.telephone.set_low_latency_output(call["telephone_set_low_latency_output"])) if self.telephone else False
elif "telephone_announce" in call: connection.send(self.telephone.announce()) if self.telephone else False
elif "telephone_is_available" in call: connection.send(self.telephone.is_available if self.telephone else False)
elif "telephone_is_in_call" in call: connection.send(self.telephone.is_in_call if self.telephone else False)
elif "telephone_call_is_connecting" in call: connection.send(self.telephone.call_is_connecting if self.telephone else False)
elif "telephone_is_ringing" in call: connection.send(self.telephone.is_ringing if self.telephone else False)
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)
elif "telephone_set_low_latency_output" in call: connection.send(self.telephone.set_low_latency_output(call["telephone_set_low_latency_output"]) if self.telephone else False)
elif "telephone_announce" in call: connection.send(self.telephone.announce() if self.telephone else False)
elif "telephone_get_call_log" in call: connection.send(self.telephone.get_call_log() if self.telephone else [])
else:
connection.send(None)
@ -5518,9 +5519,10 @@ class SidebandCore():
RNS.log("Starting voice service", RNS.LOG_DEBUG)
self.voice_running = True
self.setstate("voice.running", self.voice_running)
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"])
ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus")
call_log_path = os.path.join(self.app_dir, "app_storage", "lxst_call_log")
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"], logpath=call_log_path)
self.telephone.set_ringtone(ringtone_path)
self.telephone.set_low_latency_output(self.config["voice_low_latency"])
return True
@ -5584,6 +5586,13 @@ class SidebandCore():
if self.gui_foreground(): RNS.log("Squelching call notification since GUI is in foreground", RNS.LOG_DEBUG)
else: self.notify(title="Incoming voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
def missed_call(self, remote_identity):
display_name = self.voice_display_name(remote_identity.hash)
self.setstate("voice.incoming_call", display_name)
# if self.gui_foreground(): RNS.log("Squelching call notification since GUI is in foreground", RNS.LOG_DEBUG)
# else: self.notify(title="Missed voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
self.notify(title="Missed voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
rns_config = """# This template is used to generate a
# running configuration for Sideband's
# internal RNS instance. Incorrect changes

View file

@ -6,6 +6,7 @@ import time
from LXST._version import __version__
from LXST.Primitives.Telephony import Telephone
from RNS.vendor.configobj import ConfigObj
import RNS.vendor.umsgpack as msgpack
class ReticulumTelephone():
STATE_AVAILABLE = 0x00
@ -22,7 +23,9 @@ class ReticulumTelephone():
WAIT_TIME = 60
PATH_TIME = 10
def __init__(self, identity, owner = None, service = False, speaker=None, microphone=None, ringer=None):
CALL_LOG_KEEP = 30*24*60*60
def __init__(self, identity, owner = None, service = False, speaker=None, microphone=None, ringer=None, logpath=None):
self.identity = identity
self.service = service
self.owner = owner
@ -40,6 +43,8 @@ class ReticulumTelephone():
self.speaker_device = speaker
self.microphone_device = microphone
self.ringer_device = ringer
self.logpath = logpath
self.call_log = None
self.phonebook = {}
self.aliases = {}
self.names = {}
@ -113,6 +118,54 @@ class ReticulumTelephone():
def set_busy(self, busy): self.telephone.set_busy(busy)
def set_low_latency_output(self, enabled): self.telephone.set_low_latency_output(enabled)
def get_call_log(self):
if self.call_log: return self.call_log
else:
call_log = []
try:
if os.path.isfile(self.logpath):
with open(self.logpath, "rb") as logfile:
read_call_log = msgpack.unpackb(logfile.read())
for entry in read_call_log:
age = time.time()-entry["time"]
if age < self.CALL_LOG_KEEP: call_log.append(entry)
except Exception as e: RNS.log(f"Could not read call log file: {e}", RNS.LOG_ERROR)
self.call_log = call_log
return self.call_log
def log_call(self, event, identity):
RNS.log(f"Logging call event {event} for {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG)
if self.logpath:
try:
if not os.path.isfile(self.logpath):
try:
with open(self.logpath, "wb") as logfile: logfile.write(msgpack.packb([]))
except Exception as e: raise OSError("Could not create call log file")
call_log = []
read_call_log = []
try:
with open(self.logpath, "rb") as logfile: read_call_log = msgpack.unpackb(logfile.read())
except Exception as e:
RNS.log(f"Error while reading call log file: {e}", RNS.LOG_ERROR)
RNS.log(f"Call log file will be re-created", RNS.LOG_ERROR)
for entry in read_call_log:
age = time.time()-entry["time"]
if age < self.CALL_LOG_KEEP: call_log.append(entry)
entry = {"time": time.time(), "event": event, "identity": identity.hash}
call_log.append(entry)
with open(self.logpath, "wb") as logfile: logfile.write(msgpack.packb(call_log))
self.call_log = call_log
except Exception as e:
RNS.log(f"An error occurred while updating call log: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
def dial(self, identity_hash):
self.last_dialled_identity_hash = identity_hash
destination_hash = RNS.Destination.hash_from_name_and_identity("lxst.telephony", identity_hash)
@ -145,6 +198,10 @@ class ReticulumTelephone():
self.owner.incoming_call(remote_identity)
def call_ended(self, remote_identity):
call_was_connecting = self.call_is_connecting
was_ringing = self.is_ringing
was_in_call = self.is_in_call
if self.is_in_call or self.is_ringing or self.call_is_connecting:
if self.is_in_call: RNS.log(f"Call with {RNS.prettyhexrep(self.caller.hash)} ended\n", RNS.LOG_DEBUG)
if self.is_ringing: RNS.log(f"Call {self.direction} {RNS.prettyhexrep(self.caller.hash)} was not answered\n", RNS.LOG_DEBUG)
@ -152,11 +209,23 @@ class ReticulumTelephone():
self.direction = None
self.state = self.STATE_AVAILABLE
if call_was_connecting: self.log_call("outgoing-failure", remote_identity)
elif was_in_call: self.log_call("ongoing-ended", remote_identity)
elif was_ringing:
self.log_call("incoming-missed", remote_identity)
self.owner.missed_call(remote_identity)
def call_established(self, remote_identity):
call_was_connecting = self.call_is_connecting
was_ringing = self.is_ringing
if self.call_is_connecting or self.is_ringing:
self.state = self.STATE_IN_CALL
RNS.log(f"Call established with {RNS.prettyhexrep(self.caller.hash)}", RNS.LOG_DEBUG)
if call_was_connecting: self.log_call("outgoing-success", remote_identity)
elif was_ringing: self.log_call("incoming-success", remote_identity)
def __is_allowed(self, identity_hash):
if self.owner.config["voice_trusted_only"]:
return self.owner.voice_is_trusted(identity_hash)
@ -202,4 +271,5 @@ class ReticulumTelephoneProxy():
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 })
def set_low_latency_output(self, enabled): return self.owner.service_rpc_request({"telephone_set_low_latency_output": enabled})
def announce(self): return self.owner.service_rpc_request({"telephone_announce": True})
def announce(self): return self.owner.service_rpc_request({"telephone_announce": True})
def get_call_log(self): return self.owner.service_rpc_request({"telephone_get_call_log": True})

View file

@ -6,6 +6,7 @@ from kivymd.uix.list import OneLineIconListItem, MDList, IconLeftWidget, IconRig
from kivy.properties import StringProperty
import re
ts_format_date = "%Y-%m-%d"
ts_format = "%Y-%m-%d %H:%M:%S"
file_ts_format = "%Y_%m_%d_%H_%M_%S"

View file

@ -12,6 +12,8 @@ from kivymd.uix.pickers import MDColorPicker
from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.button import MDRectangleFlatIconButton
from kivymd.uix.dialog import MDDialog
from kivy.properties import StringProperty, BooleanProperty, OptionProperty, ColorProperty, Property
from kivymd.uix.list import MDList, IconLeftWidget, IconRightWidget, OneLineAvatarIconListItem
from kivymd.icon_definitions import md_icons
from kivymd.toast import toast
from kivy.properties import StringProperty, BooleanProperty
@ -22,10 +24,16 @@ import threading
from datetime import datetime
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import ts_format
from ui.helpers import ts_format_date
from android.permissions import request_permissions, check_permission
else:
from .helpers import ts_format
from .helpers import ts_format_date
from kivy.utils import escape_markup
if RNS.vendor.platformutils.get_platform() == "android":
from ui.helpers import multilingual_markup
else:
from .helpers import multilingual_markup
class Voice():
def __init__(self, app):
@ -37,6 +45,9 @@ class Voice():
self.path_requesting = None
self.output_devices = []
self.input_devices = []
self.log_list = None
self.last_log_update = 0
self.log_name_cache = {}
self.listed_output_devices = []
self.listed_input_devices = []
self.listed_ringer_devices = []
@ -46,9 +57,8 @@ class Voice():
self.screen.app = self.app
self.screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.screen)
self.update_call_log()
self.screen.ids.voice_scrollview.effect_cls = ScrollEffect
def update_call_status(self, dt=None):
if self.app.root.ids.screen_manager.current == "voice_screen":
if self.ui_updater == None: self.ui_updater = Clock.schedule_interval(self.update_call_status, 0.5)
@ -89,6 +99,8 @@ class Voice():
db.disabled = True; db.text = "Voice calls disabled"
ih.disabled = True
if time.time() > self.last_log_update+3: self.update_call_log()
def target_valid(self):
if self.app.sideband.voice_running:
db = self.screen.ids.dial_button
@ -170,7 +182,7 @@ class Voice():
self.update_call_status()
### settings screen
### Settings screen
######################################
def settings_action(self, sender=None):
@ -297,6 +309,116 @@ class Voice():
self.app.sideband.telephone.set_ringer(self.app.sideband.config["voice_ringer"])
### Call log
######################################
def update_call_log(self):
if self.log_list == None:
self.log_list = CallList()
self.screen.ids.log_list_container.add_widget(self.log_list)
self.update_log_list()
self.last_log_update = time.time()
def update_log_list(self):
call_log = self.app.sideband.telephone.get_call_log()
call_log.sort(key=lambda e: e["time"], reverse=True)
data = []
for entry in call_log:
try:
at = entry["time"]
td = int(time.time())-int(at)
evt = entry["event"]
idnt = entry["identity"]
if not idnt in self.log_name_cache: self.log_name_cache[idnt] = self.app.sideband.voice_display_name(idnt)
name = multilingual_markup(escape_markup(str(self.log_name_cache[idnt])).encode("utf-8")).decode("utf-8")
icon = None
if evt == "incoming-missed": icon = "phone-missed"
elif evt == "outgoing-failure": icon = "phone-cancel"
elif evt == "incoming-success": icon = "phone-incoming"
elif evt == "outgoing-success": icon = "phone-outgoing"
time_str = None
if td < 60: time_str = "Just now"
elif td < 60*60: td = int((td//60)*60)
elif td < 60*60*24: td = int((td//60)*60)
elif td < 60*60*24*7: td = int((td//(60*60*24))*(60*60*24))
else: time_str = time.strftime(ts_format_date, time.localtime(at))
if time_str == None: time_str = f"{RNS.prettytime(td)} ago"
if icon:
info = f"{name} • [i]{time_str}[/i]"
entry = {"icon": icon, "text": f"{info}"}
data.append(entry)
except Exception as e:
RNS.log(f"An error occurred while updating the call log list: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
self.log_list.data = data
class LogEntry(OneLineAvatarIconListItem):
app = None
owner_screen = None
conversation_dropdown = None
voice_dropdown = None
clear_dialog = None
clear_telemetry_dialog = None
delete_dialog = None
icon = StringProperty()
# ti_color = OptionProperty(None, options=theme_text_color_options)
# icon_fg = Property(None, allownone=True)
# icon_bg = Property(None, allownone=True)
def __init__(self):
super().__init__()
# self.bind(on_release=self.app.conversation_action)
# self.ids.left_icon.bind(on_release=self.left_icon_action)
# self.ids.right_icon.bind(on_release=self.right_icon_action)
def left_icon_action(self, sender):
pass
def right_icon_action(self, sender):
pass
class CallList(MDRecycleView):
def __init__(self):
super().__init__()
self.data = []
Builder.load_string("""
<LogEntry>
IconLeftWidget:
id: left_icon
# theme_icon_color: root.ti_color
# icon_color: root.icon_fg
# md_bg_color: root.icon_bg
icon: root.icon
_default_icon_pad: dp(14)
icon_size: dp(24)
# IconRightWidget:
# id: right_icon
# icon: "dots-vertical"
<CallList>:
id: calls_scrollview
viewclass: "LogEntry"
effect_cls: "ScrollEffect"
RecycleBoxLayout:
default_size: None, dp(57)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: "vertical"
""")
layout_voice_screen = """
MDScreen:
name: "voice_screen"
@ -316,48 +438,53 @@ MDScreen:
['close', lambda x: root.app.close_any_action(self)],
]
ScrollView:
id: voice_scrollview
MDBoxLayout:
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
padding: [dp(28), dp(32), dp(28), dp(16)]
MDBoxLayout:
orientation: "vertical"
# spacing: "24dp"
size_hint_y: None
height: self.minimum_height
padding: [dp(28), dp(32), dp(28), dp(16)]
padding: [dp(0), dp(12), dp(0), dp(0)]
MDBoxLayout:
orientation: "vertical"
# spacing: "24dp"
size_hint_y: None
height: self.minimum_height
padding: [dp(0), dp(12), dp(0), dp(0)]
MDTextField:
id: identity_hash
hint_text: "Identity hash"
mode: "rectangle"
# size_hint: [1.0, None]
pos_hint: {"center_x": .5}
max_text_length: 32
on_text: root.delegate.target_input_action(self)
MDTextField:
id: identity_hash
hint_text: "Identity hash"
mode: "rectangle"
# size_hint: [1.0, None]
pos_hint: {"center_x": .5}
max_text_length: 32
on_text: root.delegate.target_input_action(self)
MDBoxLayout:
orientation: "vertical"
spacing: "24dp"
size_hint_y: None
height: self.minimum_height
padding: [dp(0), dp(35), dp(0), dp(35)]
MDBoxLayout:
orientation: "vertical"
spacing: "24dp"
size_hint_y: None
height: self.minimum_height
padding: [dp(0), dp(35), dp(0), dp(35)]
MDRectangleFlatIconButton:
id: dial_button
icon: "phone-outgoing"
text: "Call"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.dial_action(self)
disabled: True
MDRectangleFlatIconButton:
id: dial_button
icon: "phone-outgoing"
text: "Call"
padding: [dp(0), dp(14), dp(0), dp(14)]
icon_size: dp(24)
font_size: dp(16)
size_hint: [1.0, None]
on_release: root.delegate.dial_action(self)
disabled: True
MDSeparator:
orientation: "horizontal"
height: dp(1)
MDBoxLayout:
orientation: "vertical"
id: log_list_container
"""
layout_voice_settings_screen = """