2025-03-09 14:29:02 +01:00
import time
import RNS
from typing import Union
from kivy . metrics import dp , sp
from kivy . lang . builder import Builder
from kivy . core . clipboard import Clipboard
from kivy . utils import escape_markup
from kivymd . uix . recycleview import MDRecycleView
from kivymd . uix . list import OneLineIconListItem
from kivymd . uix . pickers import MDColorPicker
from kivymd . uix . button import MDRectangleFlatButton
2025-03-14 14:09:38 +01:00
from kivymd . uix . button import MDRectangleFlatIconButton
2025-03-09 14:29:02 +01:00
from kivymd . uix . dialog import MDDialog
from kivymd . icon_definitions import md_icons
from kivymd . toast import toast
from kivy . properties import StringProperty , BooleanProperty
from kivy . effects . scroll import ScrollEffect
from kivy . clock import Clock
from sideband . sense import Telemeter
import threading
from datetime import datetime
if RNS . vendor . platformutils . get_platform ( ) == " android " :
from ui . helpers import ts_format
from android . permissions import request_permissions , check_permission
else :
from . helpers import ts_format
class Voice ( ) :
def __init__ ( self , app ) :
self . app = app
self . screen = None
2025-03-09 18:32:31 +01:00
self . settings_screen = None
self . dial_target = None
self . ui_updater = None
self . path_requesting = None
2025-03-14 14:09:38 +01:00
self . output_devices = [ ]
self . input_devices = [ ]
self . listed_output_devices = [ ]
self . listed_input_devices = [ ]
self . listed_ringer_devices = [ ]
2025-03-09 14:29:02 +01:00
if not self . app . root . ids . screen_manager . has_screen ( " voice_screen " ) :
self . screen = Builder . load_string ( layout_voice_screen )
self . screen . app = self . app
self . screen . delegate = self
self . app . root . ids . screen_manager . add_widget ( self . screen )
self . screen . ids . voice_scrollview . effect_cls = ScrollEffect
2025-03-09 18:32:31 +01:00
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 )
else :
2025-03-10 17:25:40 +01:00
if self . ui_updater :
self . ui_updater . cancel ( )
self . ui_updater = None
2025-03-09 18:32:31 +01:00
db = self . screen . ids . dial_button
ih = self . screen . ids . identity_hash
if self . app . sideband . voice_running :
telephone = self . app . sideband . telephone
if self . path_requesting :
db . disabled = True
ih . disabled = True
else :
if telephone . is_available :
ih . disabled = False
self . target_input_action ( ih )
else :
ih . disabled = True
if telephone . is_in_call or telephone . call_is_connecting :
ih . disabled = True
db . disabled = False
db . text = " Hang up "
db . icon = " phone-hangup "
elif telephone . is_ringing :
ih . disabled = True
db . disabled = False
db . text = " Answer "
db . icon = " phone-ring "
if telephone . caller : ih . text = RNS . hexrep ( telephone . caller . hash , delimit = False )
else :
db . disabled = True ; db . text = " Voice calls disabled "
ih . disabled = True
def target_valid ( self ) :
if self . app . sideband . voice_running :
db = self . screen . ids . dial_button
db . disabled = False ; db . text = " Call "
db . icon = " phone-outgoing "
def target_invalid ( self ) :
if self . app . sideband . voice_running :
db = self . screen . ids . dial_button
db . disabled = True ; db . text = " Call "
db . icon = " phone-outgoing "
def target_input_action ( self , sender ) :
if sender :
target_hash = sender . text
if len ( target_hash ) == RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 * 2 :
try :
identity_hash = bytes . fromhex ( target_hash )
self . dial_target = identity_hash
self . target_valid ( )
except Exception as e : self . target_invalid ( )
else : self . target_invalid ( )
def request_path ( self , destination_hash ) :
if not self . path_requesting :
self . app . sideband . telephone . set_busy ( True )
toast ( " Requesting path... " )
self . screen . ids . dial_button . disabled = True
self . path_requesting = destination_hash
RNS . Transport . request_path ( destination_hash )
threading . Thread ( target = self . _path_wait_job , daemon = True ) . start ( )
else :
toast ( " Waiting for path request answer... " )
def _path_wait_job ( self ) :
timeout = time . time ( ) + self . app . sideband . telephone . PATH_TIME
while not RNS . Transport . has_path ( self . path_requesting ) and time . time ( ) < timeout :
time . sleep ( 0.25 )
self . app . sideband . telephone . set_busy ( False )
if RNS . Transport . has_path ( self . path_requesting ) :
RNS . log ( f " Calling { RNS . prettyhexrep ( self . dial_target ) } ... " , RNS . LOG_DEBUG )
self . app . sideband . telephone . dial ( self . dial_target )
Clock . schedule_once ( self . update_call_status , 0.1 )
else :
Clock . schedule_once ( self . _path_request_failed , 0.05 )
Clock . schedule_once ( self . update_call_status , 0.1 )
self . path_requesting = None
self . update_call_status ( )
def _path_request_failed ( self , dt ) :
toast ( " Path request timed out " )
def dial_action ( self , sender = None ) :
if self . app . sideband . voice_running :
if self . app . sideband . telephone . is_available :
destination_hash = RNS . Destination . hash_from_name_and_identity ( " lxst.telephony " , self . dial_target )
if not RNS . Transport . has_path ( destination_hash ) :
self . request_path ( destination_hash )
else :
RNS . log ( f " Calling { RNS . prettyhexrep ( self . dial_target ) } ... " , RNS . LOG_DEBUG )
self . app . sideband . telephone . dial ( self . dial_target )
self . update_call_status ( )
elif self . app . sideband . telephone . is_in_call or self . app . sideband . telephone . call_is_connecting :
RNS . log ( f " Hanging up " , RNS . LOG_DEBUG )
self . app . sideband . telephone . hangup ( )
self . update_call_status ( )
elif self . app . sideband . telephone . is_ringing :
RNS . log ( f " Answering " , RNS . LOG_DEBUG )
self . app . sideband . telephone . answer ( )
self . update_call_status ( )
2025-03-09 14:29:02 +01:00
2025-03-14 14:09:38 +01:00
### settings screen
######################################
def settings_action ( self , sender = None ) :
if not self . app . root . ids . screen_manager . has_screen ( " voice_settings_screen " ) :
self . voice_settings_screen = Builder . load_string ( layout_voice_settings_screen )
self . voice_settings_screen . app = self . app
self . voice_settings_screen . delegate = self
self . app . root . ids . screen_manager . add_widget ( self . voice_settings_screen )
self . app . root . ids . screen_manager . transition . direction = " left "
self . app . root . ids . screen_manager . current = " voice_settings_screen "
self . voice_settings_screen . ids . voice_settings_scrollview . effect_cls = ScrollEffect
self . app . sideband . setstate ( " app.displaying " , self . app . root . ids . screen_manager . current )
self . update_settings_screen ( )
def update_devices ( self ) :
import LXST
self . output_devices = [ ] ; self . input_devices = [ ]
for device in LXST . Sources . Backend ( ) . soundcard . all_speakers ( ) : self . output_devices . append ( device . name )
for device in LXST . Sinks . Backend ( ) . soundcard . all_microphones ( ) : self . input_devices . append ( device . name )
if self . app . sideband . config [ " voice_output " ] != None :
if not self . app . sideband . config [ " voice_output " ] in self . output_devices : self . output_devices . append ( self . app . sideband . config [ " voice_output " ] )
if self . app . sideband . config [ " voice_input " ] != None :
if not self . app . sideband . config [ " voice_input " ] in self . input_devices : self . input_devices . append ( self . app . sideband . config [ " voice_input " ] )
if self . app . sideband . config [ " voice_ringer " ] != None :
if not self . app . sideband . config [ " voice_ringer " ] in self . output_devices : self . output_devices . append ( self . app . sideband . config [ " voice_ringer " ] )
def update_settings_screen ( self , sender = None ) :
2025-03-14 15:05:51 +01:00
self . voice_settings_screen . ids . voice_trusted_only . active = self . app . sideband . config [ " voice_trusted_only " ]
self . voice_settings_screen . ids . voice_trusted_only . bind ( active = self . settings_save_action )
2025-03-14 14:09:38 +01:00
bp = 6 ; ml = 45 ; fs = 16 ; ics = 14
self . update_devices ( )
# Output devices
if not " system_default " in self . listed_output_devices :
default_output_button = MDRectangleFlatIconButton ( text = " System Default " , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . output_device_action )
default_output_button . device = None ; default_output_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_output " ] == None : default_output_button . icon = " check "
self . voice_settings_screen . ids . output_devices . add_widget ( default_output_button )
self . listed_output_devices . append ( " system_default " )
for device in self . output_devices :
if not device in self . listed_output_devices :
label = device if len ( device ) < ml else device [ : ml - 3 ] + " ... "
device_button = MDRectangleFlatIconButton ( text = label , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . output_device_action )
device_button . padding = [ dp ( bp ) , dp ( bp ) , dp ( bp ) , dp ( bp ) ] ; device_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_output " ] == device : device_button . icon = " check "
device_button . device = device
self . voice_settings_screen . ids . output_devices . add_widget ( device_button )
self . listed_output_devices . append ( device )
# Input devices
if not " system_default " in self . listed_input_devices :
default_input_button = MDRectangleFlatIconButton ( text = " System Default " , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . input_device_action )
default_input_button . device = None ; default_input_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_output " ] == None : default_input_button . icon = " check "
self . voice_settings_screen . ids . input_devices . add_widget ( default_input_button )
self . listed_input_devices . append ( " system_default " )
for device in self . input_devices :
if not device in self . listed_input_devices :
label = device if len ( device ) < ml else device [ : ml - 3 ] + " ... "
device_button = MDRectangleFlatIconButton ( text = label , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . input_device_action )
device_button . padding = [ dp ( bp ) , dp ( bp ) , dp ( bp ) , dp ( bp ) ] ; device_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_input " ] == device : device_button . icon = " check "
device_button . device = device
self . voice_settings_screen . ids . input_devices . add_widget ( device_button )
self . listed_input_devices . append ( device )
# Ringer devices
if not " system_default " in self . listed_ringer_devices :
default_ringer_button = MDRectangleFlatIconButton ( text = " System Default " , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . ringer_device_action )
default_ringer_button . device = None ; default_ringer_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_ringer " ] == None : default_ringer_button . icon = " check "
self . voice_settings_screen . ids . ringer_devices . add_widget ( default_ringer_button )
self . listed_ringer_devices . append ( " system_default " )
for device in self . output_devices :
if not device in self . listed_ringer_devices :
label = device if len ( device ) < ml else device [ : ml - 3 ] + " ... "
device_button = MDRectangleFlatIconButton ( text = label , font_size = dp ( fs ) , icon_size = dp ( ics ) , on_release = self . ringer_device_action )
device_button . padding = [ dp ( bp ) , dp ( bp ) , dp ( bp ) , dp ( bp ) ] ; device_button . size_hint = [ 1.0 , None ]
if self . app . sideband . config [ " voice_ringer " ] == device : device_button . icon = " check "
device_button . device = device
self . voice_settings_screen . ids . ringer_devices . add_widget ( device_button )
self . listed_ringer_devices . append ( device )
2025-03-14 15:05:51 +01:00
def settings_save_action ( self , sender = None , event = None ) :
self . app . sideband . config [ " voice_trusted_only " ] = self . voice_settings_screen . ids . voice_trusted_only . active
self . app . sideband . save_configuration ( )
2025-03-14 14:09:38 +01:00
def output_device_action ( self , sender = None ) :
self . app . sideband . config [ " voice_output " ] = sender . device
self . app . sideband . save_configuration ( )
for w in self . voice_settings_screen . ids . output_devices . children : w . icon = " "
sender . icon = " check "
if self . app . sideband . telephone :
self . app . sideband . telephone . set_speaker ( self . app . sideband . config [ " voice_output " ] )
def input_device_action ( self , sender = None ) :
self . app . sideband . config [ " voice_input " ] = sender . device
self . app . sideband . save_configuration ( )
for w in self . voice_settings_screen . ids . input_devices . children : w . icon = " "
sender . icon = " check "
if self . app . sideband . telephone :
self . app . sideband . telephone . set_microphone ( self . app . sideband . config [ " voice_input " ] )
def ringer_device_action ( self , sender = None ) :
self . app . sideband . config [ " voice_ringer " ] = sender . device
self . app . sideband . save_configuration ( )
for w in self . voice_settings_screen . ids . ringer_devices . children : w . icon = " "
sender . icon = " check "
if self . app . sideband . telephone :
self . app . sideband . telephone . set_ringer ( self . app . sideband . config [ " voice_ringer " ] )
2025-03-09 14:29:02 +01:00
layout_voice_screen = """
MDScreen :
name : " voice_screen "
BoxLayout :
orientation : " vertical "
MDTopAppBar :
title : " Voice "
anchor_title : " left "
elevation : 0
left_action_items :
[ [ ' menu ' , lambda x : root . app . nav_drawer . set_state ( " open " ) ] ]
right_action_items :
[
2025-03-14 14:09:38 +01:00
[ ' wrench-cog ' , lambda x : root . delegate . settings_action ( self ) ] ,
2025-03-09 14:29:02 +01:00
[ ' 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 ) ]
2025-03-09 18:32:31 +01:00
MDBoxLayout :
orientation : " vertical "
# spacing: "24dp"
2025-03-09 14:29:02 +01:00
size_hint_y : None
2025-03-09 18:32:31 +01:00
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 )
2025-03-09 14:29:02 +01:00
MDBoxLayout :
orientation : " vertical "
spacing : " 24dp "
size_hint_y : None
height : self . minimum_height
padding : [ dp ( 0 ) , dp ( 35 ) , dp ( 0 ) , dp ( 35 ) ]
MDRectangleFlatIconButton :
2025-03-09 18:32:31 +01:00
id : dial_button
icon : " phone-outgoing "
text : " Call "
2025-03-09 14:29:02 +01:00
padding : [ dp ( 0 ) , dp ( 14 ) , dp ( 0 ) , dp ( 14 ) ]
icon_size : dp ( 24 )
font_size : dp ( 16 )
size_hint : [ 1.0 , None ]
2025-03-09 18:32:31 +01:00
on_release : root . delegate . dial_action ( self )
disabled : True
2025-03-09 14:29:02 +01:00
"""
2025-03-14 14:09:38 +01:00
layout_voice_settings_screen = """
MDScreen :
name : " voice_settings_screen "
BoxLayout :
orientation : " vertical "
MDTopAppBar :
id : top_bar
title : " Voice Configuration "
anchor_title : " left "
elevation : 0
left_action_items :
[ [ ' menu ' , lambda x : root . app . nav_drawer . set_state ( " open " ) ] ]
right_action_items :
[
[ ' close ' , lambda x : root . app . close_sub_voice_action ( self ) ] ,
]
MDScrollView :
id : voice_settings_scrollview
size_hint_x : 1
size_hint_y : None
size : [ root . width , root . height - root . ids . top_bar . height ]
do_scroll_x : False
do_scroll_y : True
MDBoxLayout :
orientation : " vertical "
size_hint_y : None
height : self . minimum_height
2025-03-14 15:05:51 +01:00
padding : [ dp ( 28 ) , dp ( 48 ) , dp ( 28 ) , dp ( 16 ) ]
MDLabel :
text : " Call Handling "
font_style : " H6 "
height : self . texture_size [ 1 ]
padding : [ dp ( 0 ) , dp ( 0 ) , dp ( 0 ) , dp ( 12 ) ]
2025-03-14 14:09:38 +01:00
MDLabel :
id : voice_settings_info
markup : True
2025-03-14 15:05:51 +01:00
text : " You can block calls from all other callers than contacts marked as trusted, by enabling the following option. "
2025-03-14 14:09:38 +01:00
size_hint_y : None
text_size : self . width , None
height : self . texture_size [ 1 ]
2025-03-14 15:05:51 +01:00
padding : [ dp ( 0 ) , dp ( 16 ) , dp ( 0 ) , dp ( 16 ) ]
MDBoxLayout :
orientation : " horizontal "
padding : [ 0 , 0 , dp ( 24 ) , 0 ]
size_hint_y : None
height : dp ( 48 )
MDLabel :
text : " Block non-trusted callers "
font_style : " H6 "
MDSwitch :
id : voice_trusted_only
pos_hint : { " center_y " : 0.3 }
active : False
2025-03-14 14:09:38 +01:00
MDLabel :
2025-03-14 15:05:51 +01:00
text : " Audio Devices "
2025-03-14 14:09:38 +01:00
font_style : " H6 "
2025-03-14 15:05:51 +01:00
padding : [ dp ( 0 ) , dp ( 96 ) , dp ( 0 ) , dp ( 12 ) ]
MDLabel :
id : voice_settings_info
markup : True
text : " You can configure which audio devices Sideband will use for voice calls, by selecting either the system default device, or specific audio devices available. "
size_hint_y : None
text_size : self . width , None
height : self . texture_size [ 1 ]
padding : [ dp ( 0 ) , dp ( 64 ) , dp ( 0 ) , dp ( 32 ) ]
MDLabel :
text : " [b]Output[/b] "
font_size : dp ( 18 )
markup : True
2025-03-14 14:09:38 +01:00
MDBoxLayout :
id : output_devices
orientation : " vertical "
spacing : " 12dp "
size_hint_y : None
height : self . minimum_height
2025-03-14 15:05:51 +01:00
padding : [ dp ( 0 ) , dp ( 24 ) , dp ( 0 ) , dp ( 48 ) ]
2025-03-14 14:09:38 +01:00
# MDRectangleFlatIconButton:
# id: output_default_button
# text: "System Default"
# 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.output_device_action(self)
# disabled: False
MDLabel :
2025-03-14 15:05:51 +01:00
text : " [b]Input[/b] "
font_size : dp ( 18 )
markup : True
2025-03-14 14:09:38 +01:00
MDBoxLayout :
id : input_devices
orientation : " vertical "
spacing : " 12dp "
size_hint_y : None
height : self . minimum_height
2025-03-14 15:05:51 +01:00
padding : [ dp ( 0 ) , dp ( 24 ) , dp ( 0 ) , dp ( 48 ) ]
2025-03-14 14:09:38 +01:00
MDLabel :
2025-03-14 15:05:51 +01:00
text : " [b]Ringer[/b] "
font_size : dp ( 18 )
markup : True
2025-03-14 14:09:38 +01:00
MDBoxLayout :
id : ringer_devices
orientation : " vertical "
spacing : " 12dp "
size_hint_y : None
height : self . minimum_height
2025-03-14 15:05:51 +01:00
padding : [ dp ( 0 ) , dp ( 24 ) , dp ( 0 ) , dp ( 48 ) ]
2025-03-14 14:09:38 +01:00
"""