mirror of
https://github.com/markqvist/Sideband.git
synced 2026-04-27 22:25:39 +00:00
5738 lines
280 KiB
Python
5738 lines
280 KiB
Python
__debug_build__ = False
|
|
__disable_shaders__ = False
|
|
__version__ = "1.9.0"
|
|
__variant__ = ""
|
|
|
|
import sys
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Sideband LXMF Client")
|
|
parser.add_argument("-v", "--verbose", action='store_true', default=False, help="increase logging verbosity")
|
|
parser.add_argument("-c", "--config", action='store', default=None, help="specify path of config directory")
|
|
parser.add_argument("-r", "--rnsconfig", action='store', default=None, help="specify path of RNS config directory")
|
|
parser.add_argument("-d", "--daemon", action='store_true', default=False, help="run as a daemon, without user interface")
|
|
parser.add_argument("-i", "--interactive", action='store_true', default=False, help="connect interactive console after daemon init")
|
|
parser.add_argument("--export-settings", action='store', default=None, help="export application settings to file")
|
|
parser.add_argument("--import-settings", action='store', default=None, help="import application settings from file")
|
|
parser.add_argument("--version", action="version", version="sideband {version}".format(version=__version__))
|
|
args = parser.parse_args()
|
|
sys.argv = [sys.argv[0]]
|
|
|
|
import RNS
|
|
import LXMF
|
|
import time
|
|
import os
|
|
import re
|
|
import pathlib
|
|
import base64
|
|
import threading
|
|
import RNS.vendor.umsgpack as msgpack
|
|
|
|
WINDOW_DEFAULT_WIDTH = 494
|
|
WINDOW_DEFAULT_HEIGHT = 800
|
|
WINDOW_HEIGHT_MARGIN = 0
|
|
|
|
WINDOW_MIN_WIDTH = 415
|
|
WINDOW_MIN_HEIGHT = 550
|
|
|
|
app_ui_scaling_path = None
|
|
app_ui_wcfg_path = None
|
|
app_ui_window_config = None
|
|
app_ui_dsp_width = None
|
|
app_ui_dsp_height = None
|
|
app_init_window_state = "normal"
|
|
|
|
def get_display_res():
|
|
global app_ui_dsp_width, app_ui_dsp_height
|
|
if not RNS.vendor.platformutils.is_linux(): return None, None
|
|
else:
|
|
try:
|
|
import subprocess
|
|
# Try to get connected and primary display
|
|
cmd_xrandr = subprocess.Popen(["xrandr"], stdout=subprocess.PIPE)
|
|
cmd_grep = subprocess.Popen(["grep", " connected primary"], stdin=cmd_xrandr.stdout, stdout=subprocess.PIPE)
|
|
cmd_xrandr.stdout.close(); res_bytes, _ = cmd_grep.communicate()
|
|
if not (len(res_bytes) > 25 and b"x" in res_bytes):
|
|
# Try to get connected display
|
|
cmd_xrandr = subprocess.Popen(["xrandr"], stdout=subprocess.PIPE)
|
|
cmd_grep = subprocess.Popen(["grep", " connected"], stdin=cmd_xrandr.stdout, stdout=subprocess.PIPE)
|
|
cmd_xrandr.stdout.close(); res_bytes, _ = cmd_grep.communicate()
|
|
|
|
resolution_es = res_bytes.split()
|
|
for e in resolution_es:
|
|
if b"x" in e:
|
|
if b"+" in e: e = e.split(b"+")[0]
|
|
cs = e.split(b"x")
|
|
if len(cs) == 2:
|
|
resolution = e
|
|
break
|
|
|
|
width, height = resolution.split(b"x")
|
|
app_ui_dsp_width = int(width)
|
|
app_ui_dsp_height = int(height)
|
|
return app_ui_dsp_width, app_ui_dsp_height
|
|
except Exception as e:
|
|
RNS.log(f"Could not get display resolution: {e}", RNS.LOG_WARNING)
|
|
RNS.trace_exception(e)
|
|
return None, None
|
|
|
|
def apply_ui_scale():
|
|
global app_ui_scaling_path
|
|
global app_ui_wcfg_path
|
|
global app_ui_window_config
|
|
|
|
if args.config != None: config_path = os.path.expanduser(args.config)
|
|
else: config_path = None
|
|
|
|
default_scale = os.environ["KIVY_METRICS_DENSITY"] if "KIVY_METRICS_DENSITY" in os.environ else "unknown"
|
|
ui_scale_path = None
|
|
ui_wcfg_path = None
|
|
res_ident = ""
|
|
dsp_width, dsp_height = get_display_res()
|
|
if dsp_width and dsp_height:
|
|
RNS.log(f"Got display res: {dsp_width}x{dsp_height}", RNS.LOG_DEBUG)
|
|
res_ident = f"_{dsp_width}_{dsp_height}"
|
|
|
|
try:
|
|
if RNS.vendor.platformutils.is_android():
|
|
import plyer
|
|
ui_scale_path = os.path.join(plyer.storagepath.get_application_dir(), "io.unsigned.sideband", "files", "app_storage", "ui_scale")
|
|
else:
|
|
if config_path == None:
|
|
import sbapp.plyer as plyer
|
|
ui_scale_path = os.path.join(plyer.storagepath.get_home_dir(), ".config", "sideband", "app_storage", "ui_scale")
|
|
if ui_scale_path.startswith("file://"): ui_scale_path = ui_scale_path.replace("file://", "")
|
|
else:
|
|
ui_scale_path = os.path.join(config_path, "app_storage", "ui_scale")
|
|
|
|
if ui_scale_path:
|
|
ui_scale_path = f"{ui_scale_path}{res_ident}"
|
|
ui_wcfg_path = f"{ui_scale_path}_windowcfg"
|
|
|
|
app_ui_scaling_path = ui_scale_path
|
|
app_ui_wcfg_path = ui_wcfg_path
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while locating UI scale file: {e}", RNS.LOG_ERROR)
|
|
|
|
if ui_scale_path != None:
|
|
RNS.log("Default scaling factor on this platform is "+str(default_scale), RNS.LOG_NOTICE)
|
|
try:
|
|
RNS.log("Looking for scaling info in "+str(ui_scale_path), RNS.LOG_NOTICE)
|
|
if os.path.isfile(ui_scale_path):
|
|
scale_factor = None
|
|
with open(ui_scale_path, "r") as sf: scale_factor = float(sf.readline())
|
|
|
|
if scale_factor != None:
|
|
if scale_factor >= 0.3 and scale_factor <= 5.0:
|
|
os.environ["KIVY_METRICS_DENSITY"] = str(scale_factor)
|
|
RNS.log("UI scaling factor set to "+str(os.environ["KIVY_METRICS_DENSITY"]), RNS.LOG_NOTICE)
|
|
elif scale_factor == 0.0:
|
|
RNS.log("Using default UI scaling factor", RNS.LOG_NOTICE)
|
|
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while reading UI scaling factor: {e}", RNS.LOG_ERROR)
|
|
|
|
if ui_scale_path != None:
|
|
try:
|
|
RNS.log("Looking for saved window configuration in "+str(ui_wcfg_path), RNS.LOG_NOTICE)
|
|
if os.path.isfile(ui_wcfg_path):
|
|
scale_factor = None
|
|
with open(ui_wcfg_path, "r") as sf: window_config = sf.readline().split()
|
|
|
|
if window_config != None:
|
|
if type(window_config) == list and len(window_config) >= 5:
|
|
app_ui_window_config = window_config
|
|
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while reading saved window configuration: {e}", RNS.LOG_ERROR)
|
|
|
|
###################################################
|
|
# Run-time patch to fix broken Kivy on Python 3.14
|
|
# until a Kivy release is made that actually works.
|
|
#
|
|
if sys.version_info[0] >= 3 and sys.version_info[1] >= 14:
|
|
if RNS.vendor.platformutils.is_linux():
|
|
from .compat import python314_kivy_patch
|
|
python314_kivy_patch()
|
|
|
|
###################################################
|
|
# Kivy/SDL2 run-time patch to fix horribly slow
|
|
# window resize updates on Linux. For more info:
|
|
# https://github.com/kivy/kivy/issues/9106
|
|
#
|
|
_sdl2_window_event_filter_original = None
|
|
_sdl2_window_event_filter_instance = None
|
|
def _sdl2_window_event_filter_proxy(action, *largs):
|
|
global _sdl2_window_event_filter_original
|
|
global _sdl2_window_event_filter_instance
|
|
if not action == 'windowresized': return _sdl2_window_event_filter_original(action, *largs)
|
|
else:
|
|
_sdl2_window_event_filter_instance._size = largs
|
|
_sdl2_window_event_filter_instance._win.resize_window(*_sdl2_window_event_filter_instance._size)
|
|
# The only change this patched method makes is to
|
|
# remove the offending "EventLoop.idle()" statement
|
|
# EventLoop.idle()
|
|
return 0
|
|
|
|
def patch_sdl_window_events(patch_target):
|
|
if RNS.vendor.platformutils.is_linux():
|
|
global _sdl2_window_event_filter_original
|
|
global _sdl2_window_event_filter_instance
|
|
_sdl2_window_event_filter_original = patch_target._event_filter
|
|
_sdl2_window_event_filter_instance = patch_target
|
|
patch_target._event_filter = _sdl2_window_event_filter_proxy
|
|
patch_target._win.set_event_filter(patch_target._event_filter)
|
|
#
|
|
# End of Kivy/SDL2 patch ##########################
|
|
|
|
window_x_offset = 0
|
|
window_y_offset = 0
|
|
|
|
if args.export_settings:
|
|
from .sideband.core import SidebandCore
|
|
sideband = SidebandCore(
|
|
None,
|
|
config_path=args.config,
|
|
is_client=False,
|
|
verbose=(args.verbose or __debug_build__),
|
|
is_daemon=True,
|
|
load_config_only=True,
|
|
)
|
|
|
|
sideband.version_str = "v"+__version__+" "+__variant__
|
|
|
|
import json
|
|
export = sideband.config.copy()
|
|
for k in export:
|
|
if isinstance(export[k], bytes):
|
|
export[k] = RNS.hexrep(export[k], delimit=False)
|
|
try:
|
|
export_path = os.path.expanduser(args.export_settings)
|
|
with open(export_path, "wb") as export_file:
|
|
export_file.write(json.dumps(export, indent=4).encode("utf-8"))
|
|
print(f"Application settings written to {export_path}")
|
|
exit(0)
|
|
|
|
except Exception as e:
|
|
print(f"Could not write application settings to {export_path}. The contained exception was:\n{e}")
|
|
exit(1)
|
|
|
|
elif args.import_settings:
|
|
from .sideband.core import SidebandCore
|
|
sideband = SidebandCore(
|
|
None,
|
|
config_path=args.config,
|
|
is_client=False,
|
|
verbose=(args.verbose or __debug_build__),
|
|
is_daemon=True,
|
|
load_config_only=True,
|
|
)
|
|
|
|
sideband.version_str = "v"+__version__+" "+__variant__
|
|
|
|
import json
|
|
addr_fields = ["lxmf_propagation_node", "last_lxmf_propagation_node", "nn_home_node", "telemetry_collector"]
|
|
try:
|
|
import_path = os.path.expanduser(args.import_settings)
|
|
imported = None
|
|
with open(import_path, "rb") as import_file:
|
|
json_data = import_file.read().decode("utf-8")
|
|
imported = json.loads(json_data)
|
|
for k in imported:
|
|
if k in addr_fields and imported[k] != None:
|
|
imported[k] = bytes.fromhex(imported[k])
|
|
if len(imported[k]) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
|
|
raise ValueError(f"Invalid hash length for {RNS.prettyhexrep(imported[k])}")
|
|
|
|
if imported:
|
|
sideband.config = imported
|
|
sideband.save_configuration()
|
|
while sideband.saving_configuration:
|
|
time.sleep(0.1)
|
|
print(f"Application settings imported from {import_path}")
|
|
exit(0)
|
|
|
|
except Exception as e:
|
|
print(f"Could not import application settings from {import_path}. The contained exception was:\n{e}")
|
|
exit(1)
|
|
|
|
if not args.daemon:
|
|
from LXST._version import __version__ as lxst_version
|
|
from LXST.Primitives.Recorders import FileRecorder
|
|
from LXST.Primitives.Players import FilePlayer
|
|
from LXST.Codecs import Opus
|
|
from LXST.Filters import BandPass, AGC
|
|
|
|
from kivy.logger import Logger, LOG_LEVELS
|
|
from PIL import Image as PilImage
|
|
import io
|
|
|
|
# Squelch excessive method signature logging
|
|
class redirect_log():
|
|
def isEnabledFor(self, arg):
|
|
return False
|
|
def debug(self, arg):
|
|
pass
|
|
def trace(self, arg):
|
|
pass
|
|
def warning(self, arg):
|
|
RNS.log("Kivy error: "+str(arg), RNS.LOG_WARNING)
|
|
def critical(self, arg):
|
|
RNS.log("Kivy error: "+str(arg), RNS.LOG_ERROR)
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
import jnius.reflect
|
|
def mod(method, name, signature):
|
|
pass
|
|
jnius.reflect.log_method = mod
|
|
jnius.reflect.log = redirect_log()
|
|
|
|
if __debug_build__ or args.verbose:
|
|
Logger.setLevel(LOG_LEVELS["debug"])
|
|
else:
|
|
Logger.setLevel(LOG_LEVELS["error"])
|
|
|
|
if RNS.vendor.platformutils.get_platform() != "android":
|
|
local = os.path.dirname(__file__)
|
|
sys.path.append(local)
|
|
|
|
if args.daemon:
|
|
from .sideband.core import SidebandCore
|
|
class DaemonElement():
|
|
pass
|
|
class DaemonApp():
|
|
pass
|
|
|
|
MDApp = DaemonApp; OneLineIconListItem = DaemonElement; Window = DaemonElement; Clipboard = DaemonElement;
|
|
EventLoop = DaemonElement; Clock = DaemonElement; Builder = DaemonElement; ScrollEffect = DaemonElement; SlideTransition = DaemonElement;
|
|
ScreenManager = DaemonElement; FadeTransition = DaemonElement; NoTransition = DaemonElement; OneLineIconListItem = DaemonElement;
|
|
StringProperty = DaemonElement; BaseButton = DaemonElement; MDIconButton = DaemonElement; MDFileManager = DaemonElement;
|
|
toast = DaemonElement; dp = DaemonElement; sp = DaemonElement; MDRectangleFlatButton = DaemonElement; MDDialog = DaemonElement;
|
|
colors = DaemonElement; Telemeter = DaemonElement; CustomMapMarker = DaemonElement; MBTilesMapSource = DaemonElement;
|
|
MapSource = DaemonElement; webbrowser = DaemonElement; Conversations = DaemonElement; MsgSync = DaemonElement; IconLeftWidget = DaemonElement;
|
|
NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement;
|
|
Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement;
|
|
ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement;
|
|
SoundLoader = DaemonElement; BoxLayout = DaemonElement; mdconv = DaemonElement;
|
|
|
|
else:
|
|
apply_ui_scale()
|
|
|
|
if not RNS.vendor.platformutils.is_android():
|
|
# Set default scaling factor and position
|
|
scaling_factor = 1.0
|
|
window_target_x = None
|
|
window_target_y = None
|
|
window_state = "normal"
|
|
|
|
# Attempt to read configured scaling factor
|
|
# from environment variable
|
|
if not RNS.vendor.platformutils.is_windows() and not RNS.vendor.platformutils.is_darwin():
|
|
try: scaling_factor = float(os.environ["KIVY_METRICS_DENSITY"])
|
|
except Exception as e: pass
|
|
|
|
# Bound scaling factor to reasonable values
|
|
if scaling_factor < 0.75: scaling_factor = 0.75
|
|
if scaling_factor > 2: scaling_factor = 2
|
|
|
|
# Get reasonable maximum window bounds
|
|
if app_ui_dsp_width and app_ui_dsp_height:
|
|
# Use display resolution if available
|
|
max_width = app_ui_dsp_width
|
|
max_height = app_ui_dsp_height-WINDOW_HEIGHT_MARGIN
|
|
else:
|
|
# Assume bounds from default size * scaling
|
|
max_width = WINDOW_DEFAULT_WIDTH*scaling_factor
|
|
max_height = WINDOW_DEFAULT_HEIGHT*scaling_factor
|
|
|
|
# Try to find device model to apply reasonable
|
|
# bounds on window sizes
|
|
model = None
|
|
try:
|
|
if os.path.isfile("/sys/firmware/devicetree/base/model"):
|
|
with open("/sys/firmware/devicetree/base/model", "r") as mf:
|
|
model = mf.read()
|
|
except: pass
|
|
|
|
# Apply window sizing based on model
|
|
if model:
|
|
# Decrease default height for Raspberry Pi
|
|
# if screen resolution is unavailable, to
|
|
# avoid overflow on small screens
|
|
if model.startswith("Raspberry Pi ") and not app_ui_dsp_height: max_height = 625
|
|
|
|
# Initialize size to defaults
|
|
window_width_target = int(WINDOW_DEFAULT_WIDTH)
|
|
window_height_target = int(WINDOW_DEFAULT_HEIGHT)
|
|
|
|
# But use saved window configuration if possible
|
|
if type(app_ui_window_config) == list and len(app_ui_window_config) >= 5:
|
|
window_width_target = int(app_ui_window_config[0])
|
|
window_height_target = int(app_ui_window_config[1])
|
|
window_target_x = int(app_ui_window_config[2])
|
|
window_target_y = int(app_ui_window_config[3])
|
|
window_state = app_ui_window_config[4]
|
|
|
|
if len(app_ui_window_config) > 5: window_x_offset = int(app_ui_window_config[5])
|
|
if len(app_ui_window_config) > 6: window_y_offset = int(app_ui_window_config[6])
|
|
|
|
if not window_state in ["maximized", "minimized", "normal"]: window_state = "normal"
|
|
if window_target_x < 0: window_target_x = 0
|
|
if window_target_y < 0: window_target_y = 0
|
|
|
|
# Calculate final window size
|
|
window_width = int(max(min(window_width_target, max_width), WINDOW_MIN_WIDTH))
|
|
window_height = int(max(min(window_height_target, max_height), WINDOW_MIN_HEIGHT))
|
|
|
|
if app_ui_dsp_width and app_ui_dsp_height and window_target_x and window_target_y:
|
|
if window_target_x > (app_ui_dsp_width - window_width): window_target_x = app_ui_dsp_width - window_width
|
|
if window_target_y > (app_ui_dsp_height - window_height): window_target_y = app_ui_dsp_height - window_height
|
|
|
|
from kivy.config import Config
|
|
Config.set("graphics", "width", str(window_width))
|
|
Config.set("graphics", "height", str(window_height))
|
|
|
|
if window_target_x and window_target_y:
|
|
Config.set('graphics', 'position', 'custom')
|
|
Config.set("graphics", "left", str(window_target_x+window_x_offset))
|
|
Config.set("graphics", "top", str(window_target_y+window_y_offset))
|
|
_window_init_x = window_target_x
|
|
_window_init_y = window_target_y
|
|
|
|
if window_state == "maximized":
|
|
Config.set("graphics", "window_state", "maximized")
|
|
app_init_window_state = window_state
|
|
|
|
from kivymd.app import MDApp
|
|
app_superclass = MDApp
|
|
from kivy.core.window import Window
|
|
from kivy.core.clipboard import Clipboard
|
|
from kivy.core.audio import SoundLoader
|
|
from kivy.base import EventLoop
|
|
from kivy.clock import Clock
|
|
from kivy.lang.builder import Builder
|
|
from kivy.effects.scroll import ScrollEffect
|
|
from kivy.uix.screenmanager import ScreenManager
|
|
from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition
|
|
from kivy.uix.boxlayout import BoxLayout
|
|
from kivymd.uix.list import OneLineIconListItem, IconLeftWidget
|
|
from kivy.properties import StringProperty
|
|
from kivymd.uix.button import BaseButton, MDIconButton
|
|
from kivymd.uix.filemanager import MDFileManager
|
|
from kivy.metrics import dp, sp
|
|
from kivymd.uix.button import MDRectangleFlatButton
|
|
from kivymd.uix.dialog import MDDialog
|
|
from kivymd.color_definitions import colors
|
|
from sideband.sense import Telemeter
|
|
from mapview import CustomMapMarker
|
|
from mapview.mbtsource import MBTilesMapSource
|
|
from mapview.source import MapSource
|
|
from kivy.utils import escape_markup
|
|
import webbrowser
|
|
import kivy.core.image
|
|
kivy.core.image.Logger = redirect_log()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
from sideband.core import SidebandCore
|
|
import plyer
|
|
|
|
from ui.layouts import *
|
|
from ui.conversations import Conversations, MsgSync, NewConv
|
|
from ui.telemetry import Telemetry
|
|
from ui.utilities import Utilities
|
|
from ui.voice import Voice
|
|
from ui.guide import Guide
|
|
from ui.keys import Keys
|
|
from ui.hardware import Hardware
|
|
from ui.objectdetails import ObjectDetails
|
|
from ui.announces import Announces
|
|
from ui.messages import Messages, ts_format, messages_screen_kv
|
|
from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
|
|
from ui.helpers import multilingual_markup, mdc, dark_theme_text_color
|
|
from kivymd.toast import toast
|
|
|
|
from jnius import cast
|
|
from jnius import autoclass
|
|
from android import mActivity
|
|
from android.permissions import request_permissions, check_permission
|
|
from android.storage import primary_external_storage_path, secondary_external_storage_path
|
|
|
|
import LXST.Codecs.libs.pyogg as pyogg
|
|
from LXST.Codecs.libs.pydub import AudioSegment
|
|
|
|
from kivymd.utils.set_bars_colors import set_bars_colors
|
|
android_api_version = autoclass('android.os.Build$VERSION').SDK_INT
|
|
|
|
from android.broadcast import BroadcastReceiver
|
|
BluetoothAdapter = autoclass('android.bluetooth.BluetoothAdapter')
|
|
|
|
else:
|
|
from .sideband.core import SidebandCore
|
|
import sbapp.plyer as plyer
|
|
|
|
from .ui.layouts import *
|
|
from .ui.conversations import Conversations, MsgSync, NewConv
|
|
from .ui.announces import Announces
|
|
from .ui.telemetry import Telemetry
|
|
from .ui.utilities import Utilities
|
|
from .ui.voice import Voice
|
|
from .ui.guide import Guide
|
|
from .ui.keys import Keys
|
|
from .ui.hardware import Hardware
|
|
from .ui.objectdetails import ObjectDetails
|
|
from .ui.messages import Messages, ts_format, messages_screen_kv
|
|
from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
|
|
from .ui.helpers import multilingual_markup, mdc, dark_theme_text_color
|
|
|
|
import LXST.Codecs.libs.pyogg as pyogg
|
|
from LXST.Codecs.libs.pydub import AudioSegment
|
|
|
|
from kivymd.toast import toast
|
|
|
|
from kivy.config import Config
|
|
Config.set('input', 'mouse', 'mouse,disable_multitouch')
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
from jnius import autoclass
|
|
from android.runnable import run_on_ui_thread
|
|
|
|
TRANSITION_DURATION = 0.25
|
|
if RNS.vendor.platformutils.is_android():
|
|
ll_ot = 0.55
|
|
ll_ft = 0.275
|
|
else:
|
|
ll_ot = 0.4
|
|
ll_ft = 0.275
|
|
|
|
class SidebandApp(MDApp):
|
|
STARTING = 0x00
|
|
ACTIVE = 0x01
|
|
PAUSED = 0x02
|
|
STOPPING = 0x03
|
|
|
|
PKGNAME = "io.unsigned.sideband"
|
|
|
|
SERVICE_TIMEOUT = 30
|
|
|
|
EINK_BG_STR = "1,0,0,1"
|
|
EINK_BG_ARR = [1,0,0,1]
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.title = "Sideband"
|
|
self.app_state = SidebandApp.STARTING
|
|
self.android_service = None
|
|
self.app_dir = plyer.storagepath.get_application_dir()
|
|
self.shaders_disabled = __disable_shaders__
|
|
self.keyboard_enabled = False
|
|
|
|
self.no_transition = NoTransition()
|
|
self.slide_transition = SlideTransition()
|
|
|
|
if args.config != None:
|
|
self.config_path = os.path.expanduser(args.config)
|
|
else:
|
|
self.config_path = None
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
self.sideband = SidebandCore(self, config_path=self.config_path, is_client=True, android_app_dir=self.app_dir, verbose=__debug_build__)
|
|
else:
|
|
self.sideband = SidebandCore(self, config_path=self.config_path, is_client=False, verbose=(args.verbose or __debug_build__),rns_config_path=args.rnsconfig)
|
|
|
|
self.sideband.version_str = "v"+__version__+" "+__variant__
|
|
|
|
self.set_ui_theme()
|
|
self.font_config()
|
|
self.update_input_language()
|
|
self.dark_theme_text_color = dark_theme_text_color
|
|
|
|
self.conversations_view = None
|
|
self.include_conversations = True
|
|
self.include_objects = False
|
|
self.messages_view = None
|
|
self.map = None
|
|
self.map_layer = None
|
|
self.map_screen = None
|
|
self.telemetry_screen = None
|
|
self.connectivity_screen = None
|
|
self.map_cache = self.sideband.map_cache
|
|
self.offline_source = None
|
|
self.map_settings_screen = None
|
|
self.object_details_screen = None
|
|
self.sync_dialog = None
|
|
self.settings_ready = False
|
|
self.telemetry_ready = False
|
|
self.utilities_ready = False
|
|
self.voice_ready = False
|
|
self.connectivity_ready = False
|
|
self.hardware_ready = False
|
|
self.repository_ready = False
|
|
self.hw_error_dialog = None
|
|
|
|
self.final_load_completed = False
|
|
self.service_last_available = 0
|
|
self.closing_app = False
|
|
|
|
self.file_manager = None
|
|
self.attach_path = None
|
|
self.attach_type = None
|
|
self.attach_dialog = None
|
|
self.shared_attach_dialog = None
|
|
self.rec_dialog = None
|
|
self.recording_started = None
|
|
self.last_msg_audio = None
|
|
self.msg_sound = None
|
|
self.audio_msg_mode = LXMF.AM_OPUS_OGG
|
|
self.compat_error_dialog = None
|
|
self.rec_dialog_is_open = True
|
|
self.key_ptt_down = False
|
|
|
|
Window.softinput_mode = "below_target"
|
|
self.window_state = app_init_window_state
|
|
self.icon = os.path.join(self.sideband.asset_dir, "icon.png")
|
|
self.notification_icon = os.path.join(self.sideband.asset_dir, "notification_icon.png")
|
|
|
|
self.resume_event_scheduler = None
|
|
self.connectivity_updater = None
|
|
self.last_map_update = 0
|
|
self.last_telemetry_received = 0
|
|
self.repository_url = None
|
|
self.rnode_flasher_url = None
|
|
|
|
self.bt_adapter = None
|
|
self.discovered_bt_devices = {}
|
|
self.bt_bonded_devices = []
|
|
|
|
#################################################
|
|
# Application Startup #
|
|
#################################################
|
|
|
|
def update_loading_text(self):
|
|
if self.sideband:
|
|
loadingstate = self.sideband.getstate("init.loadingstate")
|
|
if loadingstate:
|
|
self.root.ids.connecting_status.text = loadingstate
|
|
|
|
def update_init_status(self, dt):
|
|
self.update_loading_text()
|
|
if not RNS.vendor.platformutils.is_android() or self.sideband.service_available():
|
|
self.service_last_available = time.time()
|
|
self.start_final()
|
|
self.loading_updater.cancel()
|
|
|
|
def start_core(self, dt):
|
|
self.loading_updater = Clock.schedule_interval(self.update_init_status, 0.1)
|
|
|
|
self.check_permissions()
|
|
self.check_bluetooth_permissions()
|
|
self.start_service()
|
|
|
|
Clock.schedule_interval(self.jobs, 1.5)
|
|
|
|
def dismiss_splash(dt):
|
|
from android import loadingscreen
|
|
loadingscreen.hide_loading_screen()
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
Clock.schedule_once(dismiss_splash, 0)
|
|
|
|
self.set_bars_colors()
|
|
|
|
def sjob(dt):
|
|
self.sideband.setstate("app.loaded", True)
|
|
self.sideband.setstate("app.running", True)
|
|
self.sideband.setstate("app.foreground", True)
|
|
Clock.schedule_once(sjob, 6.5)
|
|
|
|
def start_service(self):
|
|
if RNS.vendor.platformutils.is_android():
|
|
RNS.log("Running on Android API level "+str(android_api_version))
|
|
|
|
RNS.log("Launching platform-specific service for RNS and LXMF")
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
self.android_service = autoclass('io.unsigned.sideband.ServiceSidebandservice')
|
|
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
argument = self.app_dir
|
|
self.android_service.start(mActivity, argument)
|
|
|
|
def stop_service(self):
|
|
RNS.log("Stopping service...")
|
|
self.sideband.setstate("wants.service_stop", True)
|
|
while self.sideband.service_available(): time.sleep(0.2)
|
|
RNS.log("Service stopped")
|
|
|
|
def restart_service_action(self, sender):
|
|
if hasattr(self, "service_restarting") and self.service_restarting == True:
|
|
toast(f"Service restart already in progress")
|
|
else:
|
|
toast(f"Restarting RNS service...")
|
|
if hasattr(self, "connectivity_screen") and self.connectivity_screen != None:
|
|
self.connectivity_screen.ids.button_service_restart.disabled = True
|
|
def job():
|
|
if self.restart_service():
|
|
def tj(delta_time):
|
|
toast(f"Service restarted successfully!")
|
|
if hasattr(self, "connectivity_screen") and self.connectivity_screen != None:
|
|
self.connectivity_screen.ids.button_service_restart.disabled = False
|
|
Clock.schedule_once(tj, 0.1)
|
|
else:
|
|
def tj(delta_time):
|
|
toast(f"Service restart failed")
|
|
if hasattr(self, "connectivity_screen") and self.connectivity_screen != None:
|
|
self.connectivity_screen.ids.button_service_restart.disabled = False
|
|
Clock.schedule_once(tj, 0.1)
|
|
|
|
threading.Thread(target=job, daemon=True).start()
|
|
|
|
def restart_service(self):
|
|
if hasattr(self, "service_restarting") and self.service_restarting == True:
|
|
return False
|
|
else:
|
|
self.service_restarting = True
|
|
self.stop_service()
|
|
RNS.log("Waiting for service shutdown", RNS.LOG_DEBUG)
|
|
while self.sideband.service_rpc_request({"getstate": "service.heartbeat"}):
|
|
time.sleep(1)
|
|
time.sleep(4)
|
|
|
|
self.final_load_completed = False
|
|
self.sideband.service_stopped = True
|
|
|
|
RNS.log("Starting service...", RNS.LOG_DEBUG)
|
|
self.start_service()
|
|
RNS.log("Waiting for service restart...", RNS.LOG_DEBUG)
|
|
restart_timeout = time.time() + 45
|
|
while not self.sideband.service_rpc_request({"getstate": "service.heartbeat"}):
|
|
self.sideband.rpc_connection = None
|
|
time.sleep(1)
|
|
if time.time() > restart_timeout:
|
|
service_restarting = False
|
|
return False
|
|
|
|
RNS.log("Service restarted", RNS.LOG_DEBUG)
|
|
self.sideband.service_stopped = False
|
|
self.final_load_completed = True
|
|
self.service_restarting = False
|
|
|
|
return True
|
|
|
|
def check_launch_intent(self):
|
|
try:
|
|
if RNS.vendor.platformutils.is_android():
|
|
# Check for pending start intent
|
|
if not hasattr(mActivity, "startIntent"): RNS.log("Could not access pending intent from Android activity", RNS.LOG_ERROR)
|
|
else:
|
|
try:
|
|
pending_intent = mActivity.startIntent
|
|
RNS.log(f"Passing intent {pending_intent} to intent handler...", RNS.LOG_DEBUG)
|
|
self.on_new_intent(pending_intent)
|
|
|
|
except Exception as e:
|
|
RNS.log("An error occurred while getting pending intent on activity launch: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"An error occurred while checking Android launch intent: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
def start_final(self):
|
|
# Start local core instance
|
|
self.sideband.start()
|
|
|
|
# Pre-load announce stream widgets
|
|
self.update_loading_text()
|
|
|
|
self.loader_init()
|
|
if not RNS.vendor.platformutils.is_android():
|
|
self.telemetry_init()
|
|
self.settings_init()
|
|
self.information_init()
|
|
|
|
self.object_details_screen = None
|
|
|
|
# Wait a little extra for user to react to permissions prompt
|
|
# if RNS.vendor.platformutils.get_platform() == "android":
|
|
# if self.sideband.first_run:
|
|
# time.sleep(6)
|
|
|
|
if self.sideband.first_run:
|
|
self.guide_action()
|
|
def fp(delta_time):
|
|
self.request_permissions()
|
|
Clock.schedule_once(fp, 5)
|
|
else:
|
|
self.open_conversations()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
if self.sideband.getstate("android.power_restricted", allow_cache=False):
|
|
RNS.log("Android power restrictions detected, background connectivity will not work. Asking for permissions.", RNS.LOG_DEBUG)
|
|
def pm_job(dt):
|
|
Settings = autoclass("android.provider.Settings")
|
|
Intent = autoclass("android.content.Intent")
|
|
Uri = autoclass("android.net.Uri")
|
|
|
|
requestIntent = Intent()
|
|
requestIntent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
|
requestIntent.setData(Uri.parse("package:io.unsigned.sideband"))
|
|
mActivity.startActivity(requestIntent)
|
|
Clock.schedule_once(pm_job, 1.5)
|
|
|
|
if not self.root.ids.screen_manager.has_screen("messages_screen"):
|
|
self.messages_screen = Builder.load_string(messages_screen_kv)
|
|
self.messages_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.messages_screen)
|
|
|
|
self.app_state = SidebandApp.ACTIVE
|
|
self.loading_updater.cancel()
|
|
self.final_load_completed = True
|
|
self.keyboard_enabled = True
|
|
|
|
def check_errors(dt):
|
|
if self.sideband.getpersistent("startup.errors.rnode") != None:
|
|
if self.hw_error_dialog == None or (self.hw_error_dialog != None and not self.hw_error_dialog.is_open):
|
|
description = self.sideband.getpersistent("startup.errors.rnode")["description"]
|
|
self.sideband.setpersistent("startup.errors.rnode", None)
|
|
yes_button = MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
)
|
|
self.hw_error_dialog = MDDialog(
|
|
title="Hardware Error",
|
|
text="When starting a connected RNode, Reticulum reported the following error:\n\n[i]"+str(description)+"[/i]",
|
|
buttons=[ yes_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
self.hw_error_dialog.is_open = False
|
|
self.hw_error_dialog.dismiss()
|
|
yes_button.bind(on_release=dl_yes)
|
|
self.hw_error_dialog.open()
|
|
self.hw_error_dialog.is_open = True
|
|
|
|
def check_intent(dt): self.check_launch_intent()
|
|
|
|
Clock.schedule_once(check_errors, 1.5)
|
|
Clock.schedule_once(check_intent, 0.1)
|
|
|
|
|
|
#################################################
|
|
# General helpers #
|
|
#################################################
|
|
|
|
def set_ui_theme(self):
|
|
self.theme_cls.material_style = "M3"
|
|
self.theme_cls.widget_style = "android"
|
|
self.theme_cls.accent_palette = "Orange"
|
|
|
|
if self.sideband.config["telemetry_allow_requests_from_anyone"]:
|
|
self.theme_cls.primary_palette = "DeepOrange"
|
|
else:
|
|
self.theme_cls.primary_palette = "BlueGray"
|
|
|
|
if self.sideband.config["dark_ui"]:
|
|
self.theme_cls.theme_style = "Dark"
|
|
else:
|
|
self.theme_cls.theme_style = "Light"
|
|
|
|
self.update_ui_colors()
|
|
|
|
def font_config(self):
|
|
from kivy.core.text import LabelBase, DEFAULT_FONT
|
|
fb_path = os.path.join(self.sideband.asset_dir, "fonts")
|
|
LabelBase.register(name="hebrew",
|
|
fn_regular=os.path.join(fb_path, "NotoSansHebrew-Regular.ttf"),
|
|
fn_bold=os.path.join(fb_path, "NotoSansHebrew-Bold.ttf"))
|
|
|
|
LabelBase.register(name="japanese",
|
|
fn_regular=os.path.join(fb_path, "NotoSansJP-Regular.ttf"))
|
|
|
|
LabelBase.register(name="chinese",
|
|
fn_regular=os.path.join(fb_path, "NotoSansSC-Regular.ttf"))
|
|
|
|
LabelBase.register(name="korean",
|
|
fn_regular=os.path.join(fb_path, "NotoSansKR-Regular.ttf"))
|
|
|
|
LabelBase.register(name="emoji",
|
|
fn_regular=os.path.join(fb_path, "NotoEmoji-Regular.ttf"))
|
|
|
|
LabelBase.register(name="defaultinput",
|
|
fn_regular=os.path.join(fb_path, "DefaultInput.ttf"))
|
|
|
|
LabelBase.register(name="combined",
|
|
fn_regular=os.path.join(fb_path, "NotoSans-Regular.ttf"),
|
|
fn_bold=os.path.join(fb_path, "NotoSans-Bold.ttf"),
|
|
fn_italic=os.path.join(fb_path, "NotoSans-Italic.ttf"),
|
|
fn_bolditalic=os.path.join(fb_path, "NotoSans-BoldItalic.ttf"))
|
|
|
|
LabelBase.register(name="mono",
|
|
fn_regular=os.path.join(fb_path, "RobotoMonoNerdFont-Regular.ttf"))
|
|
|
|
LabelBase.register(name="term",
|
|
fn_regular=os.path.join(fb_path, "BigBlueTerm437NerdFont-Regular.ttf"))
|
|
|
|
LabelBase.register(name="nf",
|
|
fn_regular=os.path.join(fb_path, "RobotoMonoNerdFont-Regular.ttf"))
|
|
|
|
def update_input_language(self):
|
|
language = self.sideband.config["input_language"]
|
|
if language == None:
|
|
self.input_font = "defaultinput"
|
|
else:
|
|
self.input_font = language
|
|
|
|
RNS.log("Setting input language to "+str(self.input_font), RNS.LOG_DEBUG)
|
|
|
|
# def modify_input_font(self, ids):
|
|
# BIND_CLASSES = ["kivymd.uix.textfield.textfield.MDTextField",]
|
|
# for e in ids:
|
|
# te = ids[e]
|
|
# ts = str(te).split(" ")[0].replace("<", "")
|
|
# if ts in BIND_CLASSES:
|
|
# RNS.log("MODIFYING "+str(e)+" to "+self.input_font)
|
|
# te.font_name = self.input_font
|
|
|
|
def update_ui_colors(self):
|
|
if self.sideband.config["dark_ui"]:
|
|
self.color_reject = colors["DeepOrange"]["900"]
|
|
self.color_accept = colors["LightGreen"]["700"]
|
|
if not self.sideband.config["eink_mode"]:
|
|
self.color_hover = colors["Dark"]["CardsDialogs"]
|
|
else:
|
|
self.color_hover = colors["Gray"]["800"]
|
|
else:
|
|
self.color_reject = colors["DeepOrange"]["800"]
|
|
self.color_accept = colors["LightGreen"]["700"]
|
|
if not self.sideband.config["eink_mode"]:
|
|
self.color_hover = colors["Light"]["CardsDialogs"]
|
|
else:
|
|
self.color_hover = colors["Light"]["AppBar"]
|
|
|
|
self.apply_eink_mods()
|
|
self.set_bars_colors()
|
|
|
|
def save_window_config(self):
|
|
try:
|
|
if not RNS.vendor.platformutils.is_android():
|
|
wcfg = f"{Window.width} {Window.height} {Window.left} {Window.top} {self.window_state} {window_x_offset} {window_y_offset}"
|
|
if app_ui_wcfg_path == None: RNS.log("No path to UI window config file could be found, cannot save window config", RNS.LOG_ERROR)
|
|
else:
|
|
try:
|
|
with open(app_ui_wcfg_path, "w") as sfile: sfile.write(str(wcfg))
|
|
RNS.log(f"Saved window config to {app_ui_wcfg_path}", RNS.LOG_DEBUG)
|
|
except Exception as e: RNS.log(f"Error while saving window config to {app_ui_wcfg_path}: {e}", RNS.LOG_ERROR)
|
|
except Exception as e: RNS.log("Error while saving window configuration: {e}", RNS.LOG_ERROR)
|
|
|
|
def update_ui_theme(self):
|
|
if self.sideband.config["dark_ui"]:
|
|
self.theme_cls.theme_style = "Dark"
|
|
else:
|
|
self.theme_cls.theme_style = "Light"
|
|
self.apply_eink_mods()
|
|
|
|
self.update_ui_colors()
|
|
|
|
def apply_eink_mods(self):
|
|
if self.sideband.config["eink_mode"]:
|
|
if self.root != None:
|
|
self.root.md_bg_color = self.theme_cls.bg_light
|
|
|
|
else:
|
|
if self.root != None:
|
|
self.root.md_bg_color = self.theme_cls.bg_darkest
|
|
|
|
def set_bars_colors(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
|
|
def set_navicons(set_dark_icons = False):
|
|
from android.runnable import run_on_ui_thread
|
|
from jnius import autoclass
|
|
WindowManager = autoclass("android.view.WindowManager$LayoutParams")
|
|
activity = autoclass("org.kivy.android.PythonActivity").mActivity
|
|
View = autoclass("android.view.View")
|
|
|
|
def uit_exec():
|
|
window = activity.getWindow()
|
|
window.clearFlags(WindowManager.FLAG_TRANSLUCENT_STATUS)
|
|
window.addFlags(WindowManager.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
|
|
|
if set_dark_icons:
|
|
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
|
|
else:
|
|
window.getDecorView().setSystemUiVisibility(0)
|
|
|
|
return run_on_ui_thread(uit_exec)()
|
|
|
|
if self.sideband.config["dark_ui"]:
|
|
if self.sideband.config["eink_mode"] == True:
|
|
set_bars_colors(
|
|
self.theme_cls.primary_color, # status bar color
|
|
self.theme_cls.bg_light, # nav bar color
|
|
"Light", # icons color of status bar
|
|
)
|
|
else:
|
|
set_bars_colors(
|
|
self.theme_cls.primary_color,
|
|
self.theme_cls.bg_darkest,
|
|
"Light")
|
|
else:
|
|
if self.sideband.config["eink_mode"] == True:
|
|
set_bars_colors(
|
|
self.theme_cls.primary_color,
|
|
self.theme_cls.bg_light,
|
|
"Light")
|
|
else:
|
|
set_bars_colors(
|
|
self.theme_cls.primary_color,
|
|
self.theme_cls.bg_darkest,
|
|
"Light")
|
|
|
|
try:
|
|
set_navicons(set_dark_icons=True)
|
|
except Exception as e:
|
|
RNS.trace_exception(e)
|
|
|
|
def close_any_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
def share_text(self, text):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
Intent = autoclass('android.content.Intent')
|
|
JString = autoclass('java.lang.String')
|
|
|
|
shareIntent = Intent()
|
|
shareIntent.setAction(Intent.ACTION_SEND)
|
|
shareIntent.setType("text/plain")
|
|
shareIntent.putExtra(Intent.EXTRA_TEXT, JString(text))
|
|
|
|
mActivity.startActivity(shareIntent)
|
|
|
|
def share_image(self, image, filename):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
save_path = self.sideband.exports_dir
|
|
file_path = os.path.join(save_path, filename)
|
|
|
|
try:
|
|
if not os.path.isdir(save_path):
|
|
RNS.log("Creating directory: "+str(save_path))
|
|
os.makedirs(save_path)
|
|
|
|
Intent = autoclass("android.content.Intent")
|
|
Uri = autoclass("android.net.Uri")
|
|
File = autoclass("java.io.File")
|
|
FileProvider = autoclass("androidx.core.content.FileProvider")
|
|
|
|
if isinstance(image, bytes):
|
|
with open(file_path, "wb") as export_file:
|
|
export_file.write(image)
|
|
else:
|
|
image.save(file_path)
|
|
|
|
i_file = File(file_path)
|
|
image_uri = FileProvider.getUriForFile(mActivity, "io.unsigned.sideband.provider", i_file)
|
|
|
|
shareIntent = Intent()
|
|
shareIntent.setAction(Intent.ACTION_SEND)
|
|
shareIntent.setType("image/png")
|
|
shareIntent.putExtra(Intent.EXTRA_STREAM, cast('android.os.Parcelable', image_uri))
|
|
mActivity.startActivity(shareIntent)
|
|
|
|
except Exception as e:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Export Error",
|
|
text="The resource could not be exported and shared:\n\n"+str(e),
|
|
buttons=[ ok_button ],
|
|
)
|
|
def dl_ok(s):
|
|
dialog.dismiss()
|
|
|
|
ok_button.bind(on_release=dl_ok)
|
|
dialog.open()
|
|
|
|
def on_pause(self):
|
|
if self.sideband:
|
|
RNS.log("App pausing...", RNS.LOG_DEBUG)
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
if self.resume_event_scheduler == None:
|
|
self.resume_event_scheduler = Clock.schedule_interval(self.perform_paused_check, 0.1)
|
|
|
|
self.sideband.setstate("app.running", True)
|
|
self.sideband.setstate("app.foreground", False)
|
|
self.app_state = SidebandApp.PAUSED
|
|
self.sideband.should_persist_data()
|
|
RNS.log("App paused", RNS.LOG_DEBUG)
|
|
return True
|
|
|
|
else:
|
|
return True
|
|
|
|
def on_resume(self):
|
|
if self.sideband:
|
|
RNS.log("App resuming...", RNS.LOG_DEBUG)
|
|
self.sideband.setstate("app.running", True)
|
|
self.sideband.setstate("app.foreground", True)
|
|
self.sideband.setstate("wants.clear_notifications", True)
|
|
self.app_state = SidebandApp.ACTIVE
|
|
|
|
def ui_update_job():
|
|
time.sleep(0.05)
|
|
def cb(dt): self.perform_wake_update()
|
|
Clock.schedule_once(cb, 0.1)
|
|
threading.Thread(target=ui_update_job, daemon=True).start()
|
|
|
|
RNS.log("App resumed", RNS.LOG_DEBUG)
|
|
|
|
def on_stop(self):
|
|
RNS.log("App stopping...", RNS.LOG_DEBUG)
|
|
self.sideband.setstate("app.running", False)
|
|
self.sideband.setstate("app.foreground", False)
|
|
self.app_state = SidebandApp.STOPPING
|
|
RNS.log("App stopped", RNS.LOG_DEBUG)
|
|
|
|
def on_maximize(self, sender): self.window_state = "maximized"
|
|
|
|
def on_minimize(self, sender): self.window_state = "minimized"
|
|
|
|
def on_restore(self, sender): self.window_state = "normal"
|
|
|
|
def is_in_foreground(self):
|
|
if self.app_state == SidebandApp.ACTIVE: return True
|
|
else: return False
|
|
|
|
def perform_paused_check(self, delta_time):
|
|
# This workaround mitigates yet another bug in Kivy
|
|
# on Android, where the JNI/Python bridge now for
|
|
# Lord knows whatever reason fails to dispatch the
|
|
# onResume event from the Android app lifecycle
|
|
# management API. So we have to resort to this hack
|
|
# of scheduling a manual check that reads a patched-
|
|
# in property on the JNI side of the app activity,
|
|
# and then "manually" dispatch on_resume.
|
|
if self.app_state == SidebandApp.PAUSED:
|
|
# Oh hai, we're running, but we should really
|
|
# be paused? What gives? Must mean we've been
|
|
# woken up again, but someone forgot to inform
|
|
# us about that. Let's have a look, shall we...
|
|
activity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
if activity.activityPaused == False:
|
|
# Who would have thought, the activity was
|
|
# resumed! Good thing we can play event-
|
|
# dispatch pretend ourselves.
|
|
Clock.unschedule(self.resume_event_scheduler)
|
|
self.resume_event_scheduler = None
|
|
self.on_resume()
|
|
|
|
def perform_wake_update(self):
|
|
# This workaround mitigates a bug in Kivy on Android
|
|
# which causes the UI to turn black on app resume,
|
|
# probably due to an invalid GL draw context. By
|
|
# simply opening and immediately closing the nav
|
|
# drawer, we force the UI to do a frame redraw, which
|
|
# results in the UI actually being visible again.
|
|
if RNS.vendor.platformutils.is_android():
|
|
RNS.log("Performing app wake UI update", RNS.LOG_DEBUG)
|
|
self.root.ids.nav_drawer.set_state("open")
|
|
def cb(dt):
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
Clock.schedule_once(cb, 0)
|
|
|
|
def has_location_permissions(self):
|
|
if RNS.vendor.platformutils.is_android():
|
|
if RNS.vendor.platformutils.is_android():
|
|
if check_permission("android.permission.ACCESS_COARSE_LOCATION") and check_permission("android.permission.ACCESS_FINE_LOCATION"): return True
|
|
else: return False
|
|
|
|
def request_location_permissions(self):
|
|
if not self.has_location_permissions():
|
|
if RNS.vendor.platformutils.is_android():
|
|
RNS.log("Requesting location permission", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"])
|
|
|
|
def check_bluetooth_permissions(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
Context = autoclass('android.content.Context')
|
|
|
|
if android_api_version > 30: bt_permission_name = "android.permission.BLUETOOTH_CONNECT"
|
|
else: bt_permission_name = "android.permission.BLUETOOTH"
|
|
|
|
if check_permission(bt_permission_name):
|
|
RNS.log("Have bluetooth connect permissions", RNS.LOG_DEBUG)
|
|
|
|
if android_api_version > 30:
|
|
if check_permission("android.permission.BLUETOOTH_SCAN"):
|
|
RNS.log("Have bluetooth scan permissions", RNS.LOG_DEBUG)
|
|
self.sideband.setpersistent("permissions.bluetooth", True)
|
|
|
|
else:
|
|
RNS.log("Do not have bluetooth scan permissions")
|
|
self.sideband.setpersistent("permissions.bluetooth", False)
|
|
|
|
else:
|
|
self.sideband.setpersistent("permissions.bluetooth", True)
|
|
else:
|
|
RNS.log("Do not have bluetooth connect permissions")
|
|
self.sideband.setpersistent("permissions.bluetooth", False)
|
|
else:
|
|
self.sideband.setpersistent("permissions.bluetooth", True)
|
|
|
|
def check_permissions(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
Context = autoclass('android.content.Context')
|
|
NotificationManager = autoclass('android.app.NotificationManager')
|
|
notification_service = cast(NotificationManager, mActivity.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
|
|
if notification_service.areNotificationsEnabled():
|
|
self.sideband.setpersistent("permissions.notifications", True)
|
|
else:
|
|
if check_permission("android.permission.POST_NOTIFICATIONS"):
|
|
RNS.log("Have notification permissions", RNS.LOG_DEBUG)
|
|
self.sideband.setpersistent("permissions.notifications", True)
|
|
else:
|
|
RNS.log("Do not have notification permissions")
|
|
self.sideband.setpersistent("permissions.notifications", False)
|
|
else:
|
|
self.sideband.setpersistent("permissions.notifications", True)
|
|
|
|
def request_permissions(self):
|
|
self.request_notifications_permission()
|
|
|
|
def request_notifications_permission(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
if not check_permission("android.permission.POST_NOTIFICATIONS"):
|
|
RNS.log("Requesting notification permission", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.POST_NOTIFICATIONS"])
|
|
|
|
self.check_permissions()
|
|
|
|
def request_microphone_permission(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
if not check_permission("android.permission.RECORD_AUDIO"):
|
|
RNS.log("Requesting microphone permission", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.RECORD_AUDIO"])
|
|
|
|
def check_storage_permission(self):
|
|
storage_permissions_ok = False
|
|
if android_api_version < 30:
|
|
if check_permission("android.permission.WRITE_EXTERNAL_STORAGE") and check_permission("android.permission.READ_EXTERNAL_STORAGE"):
|
|
storage_permissions_ok = True
|
|
else:
|
|
self.request_storage_permission()
|
|
|
|
else:
|
|
Environment = autoclass('android.os.Environment')
|
|
|
|
if Environment.isExternalStorageManager():
|
|
storage_permissions_ok = True
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Storage Permission",
|
|
text="Sideband needs external storage permission to read offline map files.\n\nOn this Android version, the Manage All Files permission is needed, since normal external storage permission is no longer supported.\n\nSideband will only ever read and write to files you select, and does not read any other data from your system.",
|
|
buttons=[ ok_button ],
|
|
)
|
|
def dl_ok(s):
|
|
dialog.dismiss()
|
|
self.request_storage_permission()
|
|
|
|
ok_button.bind(on_release=dl_ok)
|
|
dialog.open()
|
|
|
|
return storage_permissions_ok
|
|
|
|
def request_storage_permission(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
if android_api_version < 30:
|
|
if not check_permission("android.permission.WRITE_EXTERNAL_STORAGE"):
|
|
RNS.log("Requesting storage write permission", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.WRITE_EXTERNAL_STORAGE"])
|
|
|
|
if not check_permission("android.permission.READ_EXTERNAL_STORAGE"):
|
|
RNS.log("Requesting storage read permission", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.READ_EXTERNAL_STORAGE"])
|
|
else:
|
|
Intent = autoclass('android.content.Intent')
|
|
Settings = autoclass('android.provider.Settings')
|
|
pIntent = Intent()
|
|
pIntent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
|
mActivity.startActivity(pIntent)
|
|
|
|
def request_bluetooth_permissions(self):
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
if not check_permission("android.permission.BLUETOOTH_CONNECT") or not check_permission("android.permission.BLUETOOTH_SCAN"):
|
|
RNS.log("Requesting Bluetooth permissions", RNS.LOG_DEBUG)
|
|
request_permissions(["android.permission.BLUETOOTH_CONNECT", "android.permission.BLUETOOTH_SCAN"])
|
|
|
|
self.check_bluetooth_permissions()
|
|
|
|
def bluetooth_update_bonded_devices(self, sender=None):
|
|
if self.bt_adapter == None: self.bt_adapter = BluetoothAdapter.getDefaultAdapter()
|
|
self.bt_bonded_devices = []
|
|
for device in self.bt_adapter.getBondedDevices():
|
|
device_addr = device.getAddress()
|
|
self.bt_bonded_devices.append(device_addr)
|
|
|
|
RNS.log(f"Updated bonded devices: {self.bt_bonded_devices}", RNS.LOG_DEBUG)
|
|
|
|
def bluetooth_scan_action(self, sender=None):
|
|
self.start_bluetooth_scan()
|
|
|
|
def start_bluetooth_scan(self):
|
|
if not self.has_location_permissions():
|
|
if not hasattr(self, "permission_dialog") or self.permission_dialog == None:
|
|
permission_dialog_text = "[b]Missing Permissions[/b]\n\nOn this version of Android, location permission is required to scan for Bluetooth devices. Yes, this is silly, but there's no way around it.\n\nIf you don't want Sideband to have location access, you can disable this permission after scanning and pairing your RNode, and everything will still work, as it is only the scanning process that requires this."
|
|
yes_button = MDRectangleFlatButton(text="Grant Permission",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
no_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
|
|
self.permission_dialog = MDDialog(text=permission_dialog_text, buttons=[ no_button, yes_button ])
|
|
def dl_no(s): self.permission_dialog.dismiss()
|
|
def dl_yes(s):
|
|
self.permission_dialog.dismiss()
|
|
def cb(dt): self.request_location_permissions()
|
|
Clock.schedule_once(cb, 0.15)
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
no_button.bind(on_release=dl_no)
|
|
self.permission_dialog.open()
|
|
|
|
else:
|
|
self.check_bluetooth_permissions()
|
|
if not self.sideband.getpersistent("permissions.bluetooth"): self.request_bluetooth_permissions()
|
|
else:
|
|
if self.root.ids.screen_manager.has_screen("hardware_rnode_screen") and hasattr(self, "hardware_rnode_screen") and self.hardware_rnode_screen != None:
|
|
self.hardware_rnode_screen.ids.hardware_rnode_bt_scan_button.disabled = True
|
|
self.hardware_rnode_screen.ids.hardware_rnode_bt_scan_button.text = "Scanning..."
|
|
|
|
toast("Starting Bluetooth scan...")
|
|
RNS.log("Starting bluetooth scan", RNS.LOG_DEBUG)
|
|
self.discovered_bt_devices = {}
|
|
if self.hardware_view: threading.Thread(target=self.hardware_view.hardware_rnode_scan_job, daemon=True).start()
|
|
|
|
BluetoothDevice = autoclass('android.bluetooth.BluetoothDevice')
|
|
self.bt_found_action = BluetoothDevice.ACTION_FOUND
|
|
self.broadcast_receiver = BroadcastReceiver(self.on_broadcast, actions=[self.bt_found_action])
|
|
self.broadcast_receiver.start()
|
|
|
|
self.bt_adapter = BluetoothAdapter.getDefaultAdapter()
|
|
self.bluetooth_update_bonded_devices()
|
|
self.bt_adapter.startDiscovery()
|
|
|
|
def stop_bluetooth_scan(self):
|
|
RNS.log("Stopping bluetooth scan", RNS.LOG_DEBUG)
|
|
self.check_bluetooth_permissions()
|
|
if not self.sideband.getpersistent("permissions.bluetooth"):
|
|
self.request_bluetooth_permissions()
|
|
else:
|
|
self.bt_adapter = BluetoothAdapter.getDefaultAdapter()
|
|
self.bt_adapter.cancelDiscovery()
|
|
|
|
def on_broadcast(self, context, intent):
|
|
try:
|
|
BluetoothDevice = autoclass('android.bluetooth.BluetoothDevice')
|
|
action = intent.getAction()
|
|
extras = intent.getExtras()
|
|
|
|
if str(action) == "android.bluetooth.device.action.FOUND":
|
|
if extras:
|
|
try:
|
|
if android_api_version < 33: device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE")
|
|
else: device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice)
|
|
dev_name = device.getName()
|
|
dev_addr = device.getAddress()
|
|
if dev_name.startswith("RNode "):
|
|
dev_rssi = extras.getShort("android.bluetooth.device.extra.RSSI", -9999)
|
|
discovered_device = {"name": dev_name, "address": dev_addr, "rssi": dev_rssi, "discovered": time.time()}
|
|
self.discovered_bt_devices[dev_addr] = discovered_device
|
|
RNS.log(f"Discovered RNode: {discovered_device}", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while mapping discovered device: {e}", RNS.LOG_ERROR)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"An error occurred while receiving Android broadcast intent: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
def on_new_intent(self, intent):
|
|
try:
|
|
intent_action = intent.getAction()
|
|
action = None
|
|
data = None
|
|
|
|
RNS.log(f"Received intent: {intent_action}", RNS.LOG_DEBUG)
|
|
|
|
if intent_action == "android.intent.action.MAIN":
|
|
JString = autoclass('java.lang.String')
|
|
Intent = autoclass("android.content.Intent")
|
|
try:
|
|
extras = intent.getExtras()
|
|
if extras:
|
|
data = extras.getString("intent_action", "undefined")
|
|
if data.startswith("conversation."):
|
|
conv_hexhash = bytes.fromhex(data.replace("conversation.", ""))
|
|
def cb(dt): self.open_conversation(conv_hexhash)
|
|
Clock.schedule_once(cb, 0.2)
|
|
|
|
elif data.startswith("incoming_call"):
|
|
def cb(dt): self.voice_action()
|
|
Clock.schedule_once(cb, 0.2)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while getting intent action data: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
if intent_action == "android.intent.action.WEB_SEARCH":
|
|
SearchManager = autoclass('android.app.SearchManager')
|
|
data = intent.getStringExtra(SearchManager.QUERY)
|
|
|
|
if data.lower().startswith(LXMF.LXMessage.URI_SCHEMA): action = "lxm_uri"
|
|
|
|
if intent_action == "android.intent.action.VIEW":
|
|
data = intent.getData().toString()
|
|
if data.lower().startswith(LXMF.LXMessage.URI_SCHEMA): action = "lxm_uri"
|
|
|
|
if intent_action == "android.intent.action.SEND":
|
|
try:
|
|
Intent = autoclass("android.content.Intent")
|
|
extras = intent.getExtras()
|
|
target = extras.get(Intent.EXTRA_STREAM)
|
|
mime_types = extras.get(Intent.EXTRA_MIME_TYPES)
|
|
target_uri = target.toString()
|
|
target_path = target.getPath()
|
|
target_filename = target.getLastPathSegment()
|
|
|
|
RNS.log(f"Received share intent: {target_uri} / {target_path} / {target_filename}", RNS.LOG_DEBUG)
|
|
for cf in os.listdir(self.sideband.share_cache):
|
|
rt = os.path.join(self.sideband.share_cache, cf)
|
|
os.unlink(rt)
|
|
RNS.log("Removed previously cached data: "+str(rt), RNS.LOG_DEBUG)
|
|
|
|
ContentResolver = autoclass("android.content.ContentResolver")
|
|
cr = mActivity.getContentResolver()
|
|
cache_path = os.path.join(self.sideband.share_cache, target_filename)
|
|
input_stream = cr.openInputStream(target)
|
|
with open(cache_path, "wb") as cache_file:
|
|
cache_file.write(bytes(input_stream.readAllBytes()))
|
|
RNS.log("Cached shared data from Android intent", RNS.LOG_DEBUG)
|
|
|
|
action = "shared_data"
|
|
data = {"filename": target_filename, "data_path": cache_path}
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while getting intent action data: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
if action != None: self.handle_action(action, data)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while handling received intent: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
def handle_action(self, action, data):
|
|
if action == "lxm_uri":
|
|
self.ingest_lxm_uri(data)
|
|
|
|
if action == "shared_data":
|
|
RNS.log("Got shared data from Android intent", RNS.LOG_DEBUG)
|
|
def cb(dt):
|
|
try:
|
|
self.shared_attachment_action(data)
|
|
except Exception as e:
|
|
RNS.log("Error while handling external message attachment", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
Clock.schedule_once(cb, 0.1)
|
|
|
|
def ingest_lxm_uri(self, lxm_uri):
|
|
RNS.log("Ingesting LXMF paper message from URI: "+str(lxm_uri), RNS.LOG_DEBUG)
|
|
self.sideband.lxm_ingest_uri(lxm_uri)
|
|
|
|
def build(self):
|
|
FONT_PATH = os.path.join(self.sideband.asset_dir, "fonts")
|
|
if RNS.vendor.platformutils.is_darwin(): self.icon = os.path.join(self.sideband.asset_dir, "icon_macos_formed.png")
|
|
else: self.icon = os.path.join(self.sideband.asset_dir, "icon.png")
|
|
|
|
self.announces_view = None
|
|
self.guide_view = None
|
|
self.keys_view = None
|
|
self.hardware_view = None
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
ActivityInfo = autoclass('android.content.pm.ActivityInfo')
|
|
activity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
|
|
|
|
from android import activity as a_activity
|
|
a_activity.bind(on_new_intent=self.on_new_intent)
|
|
|
|
if self.sideband.config["eink_mode"] == True:
|
|
screen = Builder.load_string(root_layout.replace("app.theme_cls.bg_darkest", "app.theme_cls.bg_light"))
|
|
else:
|
|
screen = Builder.load_string(root_layout)
|
|
|
|
self.nav_drawer = screen.ids.nav_drawer
|
|
|
|
return screen
|
|
|
|
def _state_jobs(self):
|
|
props = []
|
|
|
|
def jobs(self, delta_time):
|
|
if self.final_load_completed:
|
|
if RNS.vendor.platformutils.is_android() and not self.sideband.service_available():
|
|
if time.time() - self.service_last_available > SidebandApp.SERVICE_TIMEOUT:
|
|
if self.app_state == SidebandApp.ACTIVE:
|
|
info_text = "The Reticulum and LXMF service seem to have disappeared, and Sideband is no longer connected. This should not happen, and probably indicates a bug in the background service. Please restart Sideband to regain connectivity."
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Error",
|
|
text=info_text,
|
|
buttons=[ ok_button ])
|
|
|
|
def dl_ok(s):
|
|
dialog.dismiss()
|
|
self.quit_action(s)
|
|
|
|
ok_button.bind(on_release=dl_ok)
|
|
self.final_load_completed = False
|
|
dialog.open()
|
|
|
|
else:
|
|
self.quit_action(s)
|
|
|
|
else:
|
|
self.service_last_available = time.time()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
rnode_errors = self.sideband.getpersistent("runtime.errors.rnode")
|
|
if rnode_errors != None:
|
|
if self.hw_error_dialog == None or (self.hw_error_dialog != None and not self.hw_error_dialog.is_open):
|
|
description = rnode_errors["description"]
|
|
self.sideband.setpersistent("runtime.errors.rnode", None)
|
|
yes_button = MDRectangleFlatButton(
|
|
text="Ignore",
|
|
font_size=dp(18),
|
|
)
|
|
restart_button = MDRectangleFlatButton(
|
|
text="Restart RNS",
|
|
font_size=dp(18),
|
|
)
|
|
self.hw_error_dialog = MDDialog(
|
|
title="Hardware Error",
|
|
text="While communicating with an RNode, Reticulum reported the following error:\n\n[i]"+str(description)+"[/i]",
|
|
buttons=[ yes_button, restart_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
self.hw_error_dialog.dismiss()
|
|
self.hw_error_dialog.is_open = False
|
|
def dl_restart(s):
|
|
self.hw_error_dialog.dismiss()
|
|
self.hw_error_dialog.is_open = False
|
|
self.restart_service_action(None)
|
|
yes_button.bind(on_release=dl_yes)
|
|
restart_button.bind(on_release=dl_restart)
|
|
self.hw_error_dialog.open()
|
|
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
|
|
|
|
if self.sideband.voice_running:
|
|
incoming_call = self.sideband.getstate("voice.incoming_call")
|
|
ended_call = self.sideband.getstate("voice.ongoing_ended")
|
|
if incoming_call:
|
|
self.sideband.setstate("voice.incoming_call", None)
|
|
dn = multilingual_markup(escape_markup(str(incoming_call)).encode("utf-8")).decode("utf-8")
|
|
toast(f"Call from {dn}", duration=4)
|
|
|
|
if ended_call:
|
|
self.sideband.setstate("voice.ongoing_ended", False)
|
|
toast("Call ended", duration=4)
|
|
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
self.messages_view.update()
|
|
|
|
if not self.messages_view.ids.messages_scrollview.dest_known:
|
|
self.message_area_detect()
|
|
|
|
elif self.root.ids.screen_manager.current == "conversations_screen":
|
|
if self.sideband.getstate("app.flags.unread_conversations", allow_cache=True):
|
|
if self.conversations_view != None:
|
|
self.conversations_view.update()
|
|
|
|
if self.sideband.getstate("app.flags.lxmf_sync_dialog_open", allow_cache=True) and self.sync_dialog != None:
|
|
state = self.sideband.message_router.propagation_transfer_state
|
|
|
|
dlg_sp = self.sideband.get_sync_progress()*100; dlg_ss = self.sideband.get_sync_status()
|
|
if state > LXMF.LXMRouter.PR_IDLE and state <= LXMF.LXMRouter.PR_COMPLETE:
|
|
self.sync_dialog.ids.sync_progress.value = dlg_sp
|
|
else:
|
|
self.sync_dialog.ids.sync_progress.value = 0.1
|
|
|
|
self.sync_dialog.ids.sync_status.text = dlg_ss
|
|
|
|
if state > LXMF.LXMRouter.PR_IDLE and state < LXMF.LXMRouter.PR_COMPLETE:
|
|
self.widget_hide(self.sync_dialog.stop_button, False)
|
|
else:
|
|
self.widget_hide(self.sync_dialog.stop_button, True)
|
|
|
|
elif self.root.ids.screen_manager.current == "announces_screen":
|
|
if self.sideband.getstate("app.flags.new_announces", allow_cache=True):
|
|
if self.announces_view != None:
|
|
self.announces_view.update()
|
|
|
|
elif self.root.ids.screen_manager.current == "map_screen":
|
|
if self.map_screen and hasattr(self.map_screen.ids.map_layout, "map") and self.map_screen.ids.map_layout.map != None:
|
|
self.sideband.config["map_lat"] = self.map_screen.ids.map_layout.map.lat
|
|
self.sideband.config["map_lon"] = self.map_screen.ids.map_layout.map.lon
|
|
self.sideband.config["map_zoom"] = self.map_screen.ids.map_layout.map.zoom
|
|
|
|
self.last_telemetry_received = self.sideband.getstate("app.flags.last_telemetry", allow_cache=True) or 0
|
|
if self.last_telemetry_received > self.last_map_update:
|
|
self.map_update_markers()
|
|
|
|
if self.sideband.getstate("app.flags.new_conversations", allow_cache=True):
|
|
if self.conversations_view != None:
|
|
self.conversations_view.update()
|
|
|
|
if self.sideband.getstate("app.flags.new_ticket", allow_cache=True):
|
|
def cb(d):
|
|
self.sideband.message_router.reload_available_tickets()
|
|
self.sideband.setstate("app.flags.new_ticket", False)
|
|
Clock.schedule_once(cb, 1.5)
|
|
|
|
if self.sideband.getstate("wants.viewupdate.conversations", allow_cache=True):
|
|
if self.conversations_view != None:
|
|
self.conversations_view.update()
|
|
|
|
invalid_values = ["None", "False", "True", True, False, None]
|
|
imr = self.sideband.getstate("lxm_uri_ingest.result", allow_cache=True)
|
|
if imr:
|
|
if imr in invalid_values:
|
|
self.sideband.setstate("lxm_uri_ingest.result", False)
|
|
else:
|
|
info_text = str(imr)
|
|
self.sideband.setstate("lxm_uri_ingest.result", False)
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Message Scan",
|
|
text=info_text,
|
|
buttons=[ ok_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_ok(s):
|
|
dialog.dismiss()
|
|
|
|
ok_button.bind(on_release=dl_ok)
|
|
dialog.open()
|
|
|
|
invalid_values = ["None", "False", "True", True, False, None]
|
|
hwe = self.sideband.getstate("hardware_operation.error", allow_cache=True)
|
|
if hwe:
|
|
if hwe in invalid_values:
|
|
self.sideband.setstate("hardware_operation.error", False)
|
|
else:
|
|
info_text = str(hwe)
|
|
self.sideband.setstate("hardware_operation.error", False)
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Error",
|
|
text=info_text,
|
|
buttons=[ ok_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_ok(s):
|
|
dialog.dismiss()
|
|
|
|
ok_button.bind(on_release=dl_ok)
|
|
dialog.open()
|
|
|
|
def close_requested(self, *args):
|
|
if not self.closing_app:
|
|
self.quit_action(None)
|
|
return True
|
|
|
|
def file_dropped(self, window, file_path, x, y, *args):
|
|
self.shared_attachment_action({"data_path": file_path.decode("utf-8")})
|
|
|
|
def on_start(self):
|
|
self.last_exit_event = time.time()
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.duration = TRANSITION_DURATION
|
|
self.root.ids.screen_manager.transition.bind(on_complete=self.screen_transition_complete)
|
|
|
|
EventLoop.window.bind(on_keyboard=self.keyboard_event)
|
|
EventLoop.window.bind(on_key_down=self.keydown_event)
|
|
EventLoop.window.bind(on_key_up=self.keyup_event)
|
|
Window.bind(on_request_close=self.close_requested)
|
|
Window.bind(on_drop_file=self.file_dropped)
|
|
|
|
Window.bind(on_maximize=self.on_maximize)
|
|
Window.bind(on_minimize=self.on_minimize)
|
|
Window.bind(on_restore=self.on_restore)
|
|
|
|
patch_sdl_window_events(Window)
|
|
|
|
if __variant__ != "": variant_str = " "+__variant__
|
|
else: variant_str = ""
|
|
|
|
self.root.ids.screen_manager.app = self
|
|
self.root.ids.app_version_info.text = "Sideband v"+__version__+variant_str
|
|
self.root.ids.nav_scrollview.effect_cls = ScrollEffect
|
|
Clock.schedule_once(self.start_core, 0.25)
|
|
|
|
def close_handler(self):
|
|
if self.root.ids.screen_manager.current == "conversations_screen":
|
|
if self.include_conversations and not self.include_objects: self.quit_action(self)
|
|
else: self.conversations_action(direction="right")
|
|
elif self.root.ids.screen_manager.current == "hardware_rnode_screen": self.close_sub_hardware_action()
|
|
elif self.root.ids.screen_manager.current == "hardware_modem_screen": self.close_sub_hardware_action()
|
|
elif self.root.ids.screen_manager.current == "hardware_serial_screen": self.close_sub_hardware_action()
|
|
elif self.root.ids.screen_manager.current == "map_settings_screen": self.close_sub_map_action()
|
|
elif self.root.ids.screen_manager.current == "object_details_screen": self.object_details_screen.close_action()
|
|
elif self.root.ids.screen_manager.current == "sensors_screen": self.close_sub_telemetry_action()
|
|
elif self.root.ids.screen_manager.current == "icons_screen": self.close_sub_telemetry_action()
|
|
elif self.root.ids.screen_manager.current == "utilities_screen": self.close_any_action()
|
|
elif self.root.ids.screen_manager.current == "rnstatus_screen": self.utilities_screen.close_rnstatus_action()
|
|
elif self.root.ids.screen_manager.current == "logviewer_screen": self.close_sub_utilities_action()
|
|
elif self.root.ids.screen_manager.current == "advanced_screen": self.close_sub_utilities_action()
|
|
elif self.root.ids.screen_manager.current == "voice_settings_screen": self.close_sub_voice_action()
|
|
else: self.close_any_action()
|
|
|
|
def keyup_event(self, instance, keyboard, keycode):
|
|
if self.keyboard_enabled:
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
if not self.rec_dialog_is_open:
|
|
if not self.messages_view.ids.message_text.focus:
|
|
if self.messages_view.ptt_enabled and keycode == 44:
|
|
if self.key_ptt_down:
|
|
self.key_ptt_down = False
|
|
self.message_ptt_up_action()
|
|
|
|
|
|
def keydown_event(self, instance, keyboard, keycode, text, modifiers):
|
|
if self.keyboard_enabled:
|
|
if self.root.ids.screen_manager.current == "map_screen":
|
|
if not (len(modifiers) > 0 and "ctrl" in modifiers):
|
|
if len(modifiers) > 0 and "shift" in modifiers:
|
|
nav_mod = 4
|
|
elif len(modifiers) > 0 and "alt" in modifiers:
|
|
nav_mod = 0.25
|
|
else:
|
|
nav_mod = 1.0
|
|
|
|
if keycode == 79 or text == "d" or text == "l": self.map_nav_right(modifier=nav_mod)
|
|
if keycode == 80 or text == "a" or text == "h": self.map_nav_left(modifier=nav_mod)
|
|
if keycode == 81 or text == "s" or text == "j": self.map_nav_down(modifier=nav_mod)
|
|
if keycode == 82 or text == "w" or text == "k": self.map_nav_up(modifier=nav_mod)
|
|
if text == "q" or text == "-": self.map_nav_zoom_out(modifier=nav_mod)
|
|
if text == "e" or text == "+": self.map_nav_zoom_in(modifier=nav_mod)
|
|
|
|
if True or self.root.ids.screen_manager.current == "conversations_screen":
|
|
if len(modifiers) > 0 and "ctrl" in modifiers:
|
|
if keycode < 40 and keycode > 29:
|
|
c_index = keycode-29
|
|
self.conversation_index_action(c_index)
|
|
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
if keycode == 43:
|
|
if not self.messages_view.ids.message_text.focus:
|
|
self.messages_view.ids.message_text.write_tab = False
|
|
self.messages_view.ids.message_text.focus = True
|
|
def tab_job(delta): self.messages_view.ids.message_text.write_tab = True
|
|
Clock.schedule_once(tab_job, 0.15)
|
|
|
|
elif len(modifiers) == 0 and self.rec_dialog != None and self.rec_dialog_is_open:
|
|
if text == " ": self.msg_rec_a_rec(None)
|
|
elif keycode == 40: self.msg_rec_a_save(None)
|
|
|
|
elif len(modifiers) == 0 and not self.rec_dialog_is_open and not self.messages_view.ids.message_text.focus and self.messages_view.ptt_enabled and keycode == 44:
|
|
if not self.key_ptt_down:
|
|
self.key_ptt_down = True
|
|
self.message_ptt_down_action()
|
|
|
|
elif len(modifiers) > 1 and "shift" in modifiers and "ctrl" in modifiers:
|
|
def clear_att():
|
|
if self.attach_path != None:
|
|
self.attach_path = None
|
|
self.attach_type = None
|
|
self.update_message_widgets()
|
|
if text == "a": clear_att(); self.message_attachment_action(None)
|
|
if text == "i": clear_att(); self.message_attach_action(attach_type="defimg")
|
|
if text == "f": clear_att(); self.message_attach_action(attach_type="file")
|
|
if text == "v":
|
|
clear_att()
|
|
self.audio_msg_mode = LXMF.AM_OPUS_OGG
|
|
self.message_attach_action(attach_type="audio")
|
|
if text == "c":
|
|
clear_att()
|
|
self.audio_msg_mode = LXMF.AM_CODEC2_2400
|
|
self.message_attach_action(attach_type="audio")
|
|
|
|
if len(modifiers) > 0:
|
|
if modifiers[0] == "ctrl":
|
|
if text == "q":
|
|
self.quit_action(self)
|
|
|
|
if text == "w": self.close_handler()
|
|
|
|
if text == "s" or text == "d":
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
self.message_send_action()
|
|
|
|
if text == "l":
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
self.message_propagation_action(self)
|
|
elif self.root.ids.screen_manager.current == "map_screen":
|
|
self.map_layers_action()
|
|
else:
|
|
self.announces_action(self)
|
|
|
|
if text == "m":
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
context_dest = self.messages_view.ids.messages_scrollview.active_conversation
|
|
self.map_show_peer_location(context_dest)
|
|
elif self.root.ids.screen_manager.current == "object_details_screen":
|
|
context_dest = self.object_details_screen.object_hash
|
|
self.map_show_peer_location(context_dest)
|
|
else:
|
|
self.map_action(self)
|
|
|
|
if text == "p":
|
|
if self.root.ids.screen_manager.current == "map_screen":
|
|
self.map_settings_action()
|
|
else:
|
|
self.settings_action(self)
|
|
|
|
if text == "t":
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
self.object_details_action(self.messages_view, from_conv=True)
|
|
elif self.root.ids.screen_manager.current == "object_details_screen":
|
|
self.object_details_screen.reload_telemetry()
|
|
else:
|
|
self.telemetry_action(self)
|
|
|
|
if text == "y":
|
|
self.map_display_own_telemetry()
|
|
|
|
if text == "u":
|
|
self.utilities_action()
|
|
|
|
if text == "o":
|
|
self.objects_action()
|
|
|
|
if text == "e":
|
|
self.voice_action()
|
|
|
|
if text == " ":
|
|
self.voice_answer_action()
|
|
|
|
if text == ".":
|
|
self.voice_reject_action()
|
|
|
|
if text == "r":
|
|
if self.root.ids.screen_manager.current == "conversations_screen":
|
|
if self.include_objects:
|
|
self.conversations_action(self, direction="right")
|
|
else:
|
|
self.lxmf_sync_action(self)
|
|
elif self.root.ids.screen_manager.current == "telemetry_screen":
|
|
self.conversations_action(self, direction="right")
|
|
elif self.root.ids.screen_manager.current == "rnstatus_screen":
|
|
self.utilities_screen.update_rnstatus()
|
|
elif self.root.ids.screen_manager.current == "object_details_screen":
|
|
if not self.object_details_screen.object_hash == self.sideband.lxmf_destination.hash:
|
|
self.converse_from_telemetry(self)
|
|
else:
|
|
self.conversations_action(self, direction="right")
|
|
else:
|
|
self.conversations_action(self, direction="right")
|
|
|
|
if len(modifiers) > 0 and modifiers[0] == 'ctrl' and (text == "g"):
|
|
self.guide_action(self)
|
|
|
|
if text == "n":
|
|
if self.root.ids.screen_manager.current == "conversations_screen":
|
|
if not hasattr(self, "dialog_open") or not self.dialog_open:
|
|
self.new_conversation_action(self)
|
|
|
|
def keyboard_event(self, window, key, *largs):
|
|
if self.keyboard_enabled:
|
|
# Handle escape/back
|
|
if key == 27:
|
|
if self.root.ids.screen_manager.current == "conversations_screen":
|
|
if not self.include_conversations and self.include_objects: self.conversations_action(direction="right")
|
|
else:
|
|
if time.time() - self.last_exit_event < 2: self.quit_action(self)
|
|
else: self.last_exit_event = time.time()
|
|
|
|
else: self.close_handler()
|
|
return True
|
|
|
|
def widget_hide(self, w, hide=True):
|
|
if hasattr(w, "saved_attrs"):
|
|
if not hide:
|
|
w.height, w.size_hint_y, w.opacity, w.disabled = w.saved_attrs
|
|
del w.saved_attrs
|
|
elif hide:
|
|
w.saved_attrs = w.height, w.size_hint_y, w.opacity, w.disabled
|
|
w.height, w.size_hint_y, w.opacity, w.disabled = 0, None, 0, True
|
|
|
|
def ui_clipboard_action(self, sender=None, event=None):
|
|
try:
|
|
if len(sender.text) == 0:
|
|
sender.text = Clipboard.paste()
|
|
else:
|
|
Clipboard.copy(sender.text)
|
|
action = "tap" if RNS.vendor.platformutils.is_android() else "click"
|
|
toast(f"Field copied, double-{action} any empty field to paste")
|
|
except Exception as e:
|
|
RNS.log("An error occurred while handling clipboard action: "+str(e), RNS.LOG_ERROR)
|
|
|
|
def loader_init(self, sender=None):
|
|
if not self.root.ids.screen_manager.has_screen("loader_screen"):
|
|
self.loader_screen = Builder.load_string(layout_loader_screen)
|
|
self.root.ids.screen_manager.add_widget(self.loader_screen)
|
|
|
|
def loader_action(self, target=None, direction="left"):
|
|
if not self.root.ids.screen_manager.has_screen("loader_screen"):
|
|
self.loader_init()
|
|
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.current = "loader_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
|
|
def quit_action(self, sender):
|
|
self.closing_app = True
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.should_persist_data()
|
|
|
|
if not self.root.ids.screen_manager.has_screen("exit_screen"):
|
|
self.exit_screen = Builder.load_string(layout_exit_screen)
|
|
self.root.ids.screen_manager.add_widget(self.exit_screen)
|
|
|
|
self.root.ids.screen_manager.transition = NoTransition()
|
|
self.root.ids.screen_manager.current = "exit_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
self.sideband.setstate("app.running", False)
|
|
self.sideband.setstate("app.foreground", False)
|
|
|
|
def final_exit(dt):
|
|
RNS.log("Stopping service...")
|
|
self.sideband.setstate("wants.service_stop", True)
|
|
while self.sideband.service_available():
|
|
time.sleep(0.2)
|
|
RNS.log("Service stopped")
|
|
self.sideband.service_stopped = True
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
RNS.log("Finishing activity")
|
|
activity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
activity.finishAndRemoveTask()
|
|
else:
|
|
RNS.exit()
|
|
MDApp.get_running_app().stop()
|
|
Window.close()
|
|
|
|
Clock.schedule_once(final_exit, 0.85)
|
|
|
|
def announce_now_action(self, sender=None):
|
|
self.sideband.lxmf_announce()
|
|
if self.sideband.telephone: self.sideband.telephone.announce()
|
|
|
|
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
|
|
dialog = MDDialog(
|
|
title="Announce Sent",
|
|
text="Your LXMF address has been announced on all available interfaces",
|
|
buttons=[ yes_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
dialog.open()
|
|
|
|
|
|
#################################################
|
|
# Screens #
|
|
#################################################
|
|
|
|
### Messages (conversation) screen
|
|
######################################
|
|
|
|
def md_to_bbcode(self, text):
|
|
if not hasattr(self, "mdconv"):
|
|
if RNS.vendor.platformutils.is_android(): from md import mdconv
|
|
else: from .md import mdconv
|
|
self.mdconv = mdconv
|
|
|
|
converted = self.mdconv(text)
|
|
while converted.endswith("\n"):
|
|
converted = converted[:-1]
|
|
|
|
return converted
|
|
|
|
def process_bb_markup(self, text):
|
|
st = time.time()
|
|
ms = int(sp(14))
|
|
h1s = int(sp(20))
|
|
h2s = int(sp(18))
|
|
h3s = int(sp(16))
|
|
|
|
if not hasattr(self, "pres"):
|
|
self.presz = re.compile(r"\[(?:size=\d*?)\]", re.IGNORECASE | re.MULTILINE )
|
|
self.pres = []
|
|
res = [ [r"\[(?:code|icode).*?\]", f"[font=mono][size={ms}]"],
|
|
[r"\[\/(?:code|icode).*?\]", "[/size][/font]"],
|
|
[r"\[(?:heading)\]", f"[b][size={h1s}]"],
|
|
[r"\[(?:heading=1)*?\]", f"[b][size={h1s}]"],
|
|
[r"\[(?:heading=2)*?\]", f"[b][size={h2s}]"],
|
|
[r"\[(?:heading=3)*?\]", f"[b][size={h3s}]"],
|
|
[r"\[(?:heading=).*?\]", f"[b][size={h3s}]"], # Match all remaining lower-level headings
|
|
[r"\[\/(?:heading).*?\]", "[/size][/b]"],
|
|
[r"\[(?:list).*?\]", ""],
|
|
[r"\[\/(?:list).*?\]", ""],
|
|
[r"\n\[(?:\*).*?\]", "\n - "],
|
|
[r"\[(?:url).*?\]", ""], # Strip URLs for now
|
|
[r"\[\/(?:url).*?\]", ""],
|
|
[r"\[(?:img).*?\].*\[\/(?:img).*?\]", ""] # Strip images for now
|
|
]
|
|
|
|
for r in res:
|
|
self.pres.append([re.compile(r[0], re.IGNORECASE | re.MULTILINE ), r[1]])
|
|
|
|
|
|
size_matches = self.presz.findall(text)
|
|
for sm in size_matches:
|
|
text = text.replace(sm, f"{sm[:-1]}sp]")
|
|
|
|
for pr in self.pres:
|
|
text = pr[0].sub(pr[1], text)
|
|
|
|
return text
|
|
|
|
def conversation_from_announce_action(self, context_dest):
|
|
if self.sideband.has_conversation(context_dest):
|
|
pass
|
|
else:
|
|
self.sideband.create_conversation(context_dest)
|
|
self.sideband.setstate("app.flags.new_conversations", True)
|
|
|
|
self.open_conversation(context_dest)
|
|
|
|
def conversation_index_action(self, index):
|
|
if self.conversations_view != None and self.conversations_view.list != None:
|
|
i = index-1
|
|
c = self.conversations_view.list.children[0].children
|
|
if len(c) > i:
|
|
item = c[(len(c)-1)-i]
|
|
self.conversation_action(item)
|
|
|
|
def init_confirm_call_dialog(self, call_dialog_text):
|
|
yes_button = MDRectangleFlatButton(text="Call",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
no_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
|
|
self.confirm_call_dialog = MDDialog(text=call_dialog_text, buttons=[ no_button, yes_button ])
|
|
def dl_no(s): self.confirm_call_dialog.dismiss()
|
|
def dl_yes(s):
|
|
self.confirm_call_dialog.dismiss()
|
|
def cb(dt): self.dial_action(self.confirm_call_dialog.dest_identity_hash)
|
|
Clock.schedule_once(cb, 0.15)
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
no_button.bind(on_release=dl_no)
|
|
|
|
def conversation_action(self, sender):
|
|
if sender.conv_type == self.sideband.CONV_P2P:
|
|
context_dest = sender.sb_uid
|
|
def cb(dt): self.open_conversation(context_dest)
|
|
def cbu(dt): self.conversations_view.update()
|
|
Clock.schedule_once(cb, 0.15)
|
|
Clock.schedule_once(cbu, 0.15+0.25)
|
|
|
|
elif sender.conv_type == self.sideband.CONV_VOICE:
|
|
identity_hash = sender.sb_uid
|
|
if not self.sideband.config["confirm_calls"]:
|
|
def cb(dt): self.dial_action(identity_hash)
|
|
Clock.schedule_once(cb, 0.15)
|
|
else:
|
|
call_dialog_text = f"[b]Initiate Voice Call?[/b]\n\nDestination Identity:\n{RNS.prettyhexrep(identity_hash)}"
|
|
if hasattr(self, "confirm_call_dialog"): self.confirm_call_dialog.text = call_dialog_text
|
|
else: self.init_confirm_call_dialog(call_dialog_text)
|
|
self.confirm_call_dialog.dest_identity_hash = identity_hash
|
|
self.confirm_call_dialog.open()
|
|
|
|
def open_conversation(self, context_dest, direction="left"):
|
|
self.rec_dialog_is_open = False
|
|
self.outbound_mode_paper = False
|
|
self.outbound_mode_command = False
|
|
self.outbound_mode_propagation = False
|
|
if self.include_objects and not self.include_conversations:
|
|
if self.sideband.config["propagation_by_default"]:
|
|
self.outbound_mode_propagation = True
|
|
else:
|
|
self.outbound_mode_command = True
|
|
else:
|
|
if self.sideband.config["propagation_by_default"]:
|
|
self.outbound_mode_propagation = True
|
|
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
self.messages_view = Messages(self, context_dest)
|
|
|
|
self.messages_view.ids.messages_scrollview.effect_cls = ScrollEffect
|
|
for child in self.messages_view.ids.messages_scrollview.children:
|
|
self.messages_view.ids.messages_scrollview.remove_widget(child)
|
|
|
|
list_widget = self.messages_view.get_widget()
|
|
|
|
self.messages_view.ids.messages_scrollview.add_widget(list_widget)
|
|
self.messages_view.ids.messages_scrollview.scroll_y = 0.0
|
|
|
|
conv_title = multilingual_markup(escape_markup(str(self.sideband.peer_display_name(context_dest))).encode("utf-8")).decode("utf-8")
|
|
self.messages_view.ids.messages_toolbar.title = conv_title
|
|
self.messages_view.ids.messages_scrollview.active_conversation = context_dest
|
|
self.sideband.setstate("app.active_conversation", context_dest)
|
|
|
|
self.messages_view.ids.nokeys_text.text = ""
|
|
self.message_area_detect()
|
|
self.update_message_widgets()
|
|
self.messages_view.ids.message_text.disabled = False
|
|
|
|
self.root.ids.screen_manager.current = "messages_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
self.sideband.read_conversation(context_dest)
|
|
self.sideband.setstate("app.flags.unread_conversations", True)
|
|
|
|
def scb(dt):
|
|
self.messages_view.ids.messages_scrollview.scroll_y = 0.0
|
|
Clock.schedule_once(scb, 0.33)
|
|
|
|
def close_messages_action(self, sender=None):
|
|
self.rec_dialog_is_open = False
|
|
self.open_conversations(direction="right")
|
|
|
|
def message_send_action(self, sender=None):
|
|
self.rec_dialog_is_open = False
|
|
if self.messages_view.ids.message_text.text == "":
|
|
if not (self.attach_type != None and self.attach_path != None):
|
|
return
|
|
|
|
if self.outbound_mode_command:
|
|
return
|
|
|
|
def cb(dt): self.message_send_dispatch(sender)
|
|
Clock.schedule_once(cb, 0.20)
|
|
|
|
def message_send_dispatch(self, sender=None):
|
|
self.messages_view.ids.message_send_button.disabled = True
|
|
def cb(dt): self.messages_view.ids.message_send_button.disabled = False
|
|
Clock.schedule_once(cb, 0.5)
|
|
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
if self.outbound_mode_propagation and self.sideband.message_router.get_outbound_propagation_node() == None:
|
|
self.messages_view.send_error_dialog = MDDialog(
|
|
title="Error",
|
|
text="Propagated delivery was requested, but no active LXMF propagation nodes were found. Cannot send message.\n\nWait for a Propagation Node to announce on the network, or manually specify one in the settings.",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.messages_view.close_send_error_dialog
|
|
)
|
|
],
|
|
# elevation=0,
|
|
)
|
|
self.messages_view.send_error_dialog.open()
|
|
|
|
else:
|
|
msg_content = self.messages_view.ids.message_text.text
|
|
if msg_content == "":
|
|
msg_content = " "
|
|
|
|
context_dest = self.messages_view.ids.messages_scrollview.active_conversation
|
|
|
|
attachment = None
|
|
image = None
|
|
audio = None
|
|
if not self.outbound_mode_command and not self.outbound_mode_paper:
|
|
if self.attach_type != None and self.attach_path != None:
|
|
try:
|
|
RNS.log("Processing "+str(self.attach_type)+" attachment \""+str(self.attach_path)+"\"", RNS.LOG_DEBUG)
|
|
fbn = os.path.basename(self.attach_path)
|
|
|
|
if self.attach_type == "file":
|
|
with open(self.attach_path, "rb") as af:
|
|
attachment = [fbn, af.read()]
|
|
|
|
if self.attach_type == "audio":
|
|
if self.audio_msg_mode == LXMF.AM_OPUS_OGG:
|
|
with open(self.attach_path, "rb") as af:
|
|
audio = [self.audio_msg_mode, af.read()]
|
|
elif self.audio_msg_mode >= LXMF.AM_CODEC2_700C and self.audio_msg_mode <= LXMF.AM_CODEC2_3200:
|
|
with open(self.attach_path, "rb") as af:
|
|
audio = [self.audio_msg_mode, af.read()]
|
|
|
|
elif self.attach_type == "lbimg":
|
|
max_size = 320, 320
|
|
with PilImage.open(self.attach_path) as im:
|
|
im.thumbnail(max_size)
|
|
buf = io.BytesIO()
|
|
im.save(buf, format="webp", quality=22)
|
|
image = ["webp", buf.getvalue()]
|
|
|
|
elif self.attach_type == "defimg":
|
|
max_size = 640, 640
|
|
with PilImage.open(self.attach_path) as im:
|
|
im.thumbnail(max_size)
|
|
buf = io.BytesIO()
|
|
im.save(buf, format="webp", quality=66)
|
|
image = ["webp", buf.getvalue()]
|
|
|
|
elif self.attach_type == "hqimg":
|
|
max_size = 1280, 1280
|
|
with PilImage.open(self.attach_path) as im:
|
|
im.thumbnail(max_size)
|
|
buf = io.BytesIO()
|
|
im.save(buf, format="webp", quality=75)
|
|
image = ["webp", buf.getvalue()]
|
|
|
|
except Exception as e:
|
|
self.messages_view.send_error_dialog = MDDialog(
|
|
title="Attachment Error",
|
|
text="An error occurred while processing the attachment:\n\n[i]"+str(e)+"[/i]",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.messages_view.close_send_error_dialog
|
|
)
|
|
],
|
|
)
|
|
self.messages_view.send_error_dialog.open()
|
|
self.attach_type = None
|
|
self.attach_path = None
|
|
self.update_message_widgets()
|
|
return
|
|
|
|
self.attach_type = None
|
|
self.attach_path = None
|
|
self.update_message_widgets()
|
|
|
|
if self.outbound_mode_command:
|
|
if self.sideband.send_command(msg_content, context_dest, False):
|
|
self.messages_view.ids.message_text.text = ""
|
|
self.messages_view.ids.messages_scrollview.scroll_y = 0
|
|
self.jobs(0)
|
|
else:
|
|
self.messages_view.send_error_dialog = MDDialog(
|
|
title="Error",
|
|
text="Could not send the command. Check that the syntax is correct, and that the command is supported.",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.messages_view.close_send_error_dialog
|
|
)
|
|
],
|
|
)
|
|
self.messages_view.send_error_dialog.open()
|
|
|
|
elif self.outbound_mode_paper:
|
|
if self.sideband.paper_message(msg_content, context_dest):
|
|
self.messages_view.ids.message_text.text = ""
|
|
self.messages_view.ids.messages_scrollview.scroll_y = 0
|
|
self.jobs(0)
|
|
|
|
elif self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation, attachment = attachment, image = image, audio = audio):
|
|
self.messages_view.ids.message_text.text = ""
|
|
self.messages_view.ids.messages_scrollview.scroll_y = 0
|
|
self.jobs(0)
|
|
|
|
else:
|
|
self.messages_view.send_error_dialog = MDDialog(
|
|
title="Error",
|
|
text="Could not send the message",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.messages_view.close_send_error_dialog
|
|
)
|
|
],
|
|
)
|
|
self.messages_view.send_error_dialog.open()
|
|
|
|
def peer_show_location_action(self, sender):
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
context_dest = self.messages_view.ids.messages_scrollview.active_conversation
|
|
self.map_show_peer_location(context_dest)
|
|
if self.root.ids.screen_manager.current == "object_details_screen":
|
|
context_dest = self.object_details_screen.object_hash
|
|
self.map_show_peer_location(context_dest)
|
|
|
|
def peer_show_telemetry_action(self, sender):
|
|
if self.root.ids.screen_manager.current == "messages_screen":
|
|
self.object_details_action(self.messages_view, from_conv=True)
|
|
|
|
def outbound_mode_reset(self, sender=None):
|
|
self.outbound_mode_paper = False
|
|
self.outbound_mode_propagation = False
|
|
self.outbound_mode_command = False
|
|
|
|
def message_propagation_action(self, sender):
|
|
if self.outbound_mode_command:
|
|
self.outbound_mode_paper = False
|
|
self.outbound_mode_propagation = False
|
|
self.outbound_mode_command = False
|
|
else:
|
|
if self.outbound_mode_paper:
|
|
self.outbound_mode_paper = False
|
|
self.outbound_mode_propagation = False
|
|
self.outbound_mode_command = True
|
|
else:
|
|
if self.outbound_mode_propagation:
|
|
self.outbound_mode_paper = True
|
|
self.outbound_mode_propagation = False
|
|
self.outbound_mode_command = False
|
|
else:
|
|
self.outbound_mode_propagation = True
|
|
self.outbound_mode_paper = False
|
|
self.outbound_mode_command = False
|
|
|
|
self.update_message_widgets()
|
|
|
|
def message_fm_got_path(self, path):
|
|
self.message_fm_exited()
|
|
fbn = os.path.basename(path)
|
|
try:
|
|
tf = open(path, "rb")
|
|
tf.close()
|
|
self.attach_path = path
|
|
if self.outbound_mode_command:
|
|
self.outbound_mode_reset()
|
|
|
|
toast("Attached \""+str(fbn)+"\"")
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while attaching \"{fbn}\": "+str(e), RNS.LOG_ERROR)
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
toast("Could not attach \""+str(fbn)+"\"")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Attachment Error",
|
|
text="The specified file could not be attached:\n\n[i]"+str(e)+"[/i]",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
self.update_message_widgets()
|
|
|
|
|
|
def message_fm_exited(self, *args):
|
|
self.manager_open = False
|
|
if self.file_manager != None:
|
|
self.file_manager.close()
|
|
|
|
def message_select_file_action(self, sender=None):
|
|
perm_ok = False
|
|
if RNS.vendor.platformutils.is_android():
|
|
perm_ok = self.check_storage_permission()
|
|
path = primary_external_storage_path()
|
|
|
|
else:
|
|
perm_ok = True
|
|
path = os.path.expanduser("~")
|
|
|
|
|
|
if perm_ok and path != None:
|
|
try:
|
|
if self.attach_type in ["lbimg", "defimg", "hqimg"]:
|
|
self.file_manager = MDFileManager(
|
|
exit_manager=self.message_fm_exited,
|
|
select_path=self.message_fm_got_path,
|
|
# Current KivyMD preview implementation is too slow to be reliable on Android
|
|
preview=False)
|
|
else:
|
|
self.file_manager = MDFileManager(
|
|
exit_manager=self.message_fm_exited,
|
|
select_path=self.message_fm_got_path,
|
|
preview=False)
|
|
|
|
# self.file_manager.ext = []
|
|
# self.file_manager.search = "all"
|
|
self.file_manager.show(path)
|
|
|
|
except Exception as e:
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
toast("Error reading directory, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Attachment Error",
|
|
text="Error reading directory, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
else:
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
toast("No file access, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Attachment Error",
|
|
text="No file access, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
def display_codec2_error(self):
|
|
if self.compat_error_dialog == None:
|
|
def cb(sender):
|
|
self.compat_error_dialog.dismiss()
|
|
self.compat_error_dialog = MDDialog(
|
|
title="Could not load Codec2",
|
|
text="The Codec2 library could not be loaded. This likely means that you do not have the [b]codec2[/b] package or shared library installed on your system.\n\nThis library is normally installed automatically when Sideband is installed, but on some systems, this is not possible.\n\nTry installing it with a command such as [b]pamac install codec2[/b] or [b]apt install codec2[/b], or by compiling it from source for this system.",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=cb
|
|
)
|
|
],
|
|
)
|
|
self.compat_error_dialog.open()
|
|
|
|
def play_audio_field(self, audio_field):
|
|
try:
|
|
if self.msg_sound != None and self.msg_sound.playing:
|
|
RNS.log("Stopping playback", RNS.LOG_DEBUG)
|
|
self.msg_sound.stop()
|
|
return
|
|
|
|
temp_path = None
|
|
if self.last_msg_audio != audio_field[1]:
|
|
RNS.log("Reloading audio source", RNS.LOG_DEBUG)
|
|
if len(audio_field[1]) > 10: self.last_msg_audio = audio_field[1]
|
|
else:
|
|
self.last_msg_audio = None
|
|
return
|
|
|
|
if audio_field[0] == LXMF.AM_OPUS_OGG:
|
|
temp_path = os.path.join(self.sideband.rec_cache, "msg.ogg")
|
|
with open(temp_path, "wb") as af: af.write(self.last_msg_audio)
|
|
|
|
elif audio_field[0] >= LXMF.AM_CODEC2_700C and audio_field[0] <= LXMF.AM_CODEC2_3200:
|
|
temp_path = os.path.join(self.sideband.rec_cache, "msg.ogg")
|
|
from sideband.audioproc import samples_to_ogg, decode_codec2, detect_codec2
|
|
|
|
target_rate = 48000
|
|
if detect_codec2():
|
|
if samples_to_ogg(decode_codec2(audio_field[1], audio_field[0]), temp_path, input_rate=8000, output_rate=target_rate): RNS.log("Wrote OGG file to: "+temp_path, RNS.LOG_DEBUG)
|
|
else: RNS.log("OGG write failed", RNS.LOG_DEBUG)
|
|
else:
|
|
self.last_msg_audio = None
|
|
self.display_codec2_error()
|
|
return
|
|
|
|
else: raise NotImplementedError(audio_field[0])
|
|
|
|
if self.msg_sound == None:
|
|
self.msg_sound = FilePlayer()
|
|
|
|
self.msg_sound.set_source(temp_path)
|
|
|
|
if self.msg_sound != None:
|
|
RNS.log("Starting playback", RNS.LOG_DEBUG)
|
|
self.msg_sound.play()
|
|
else:
|
|
RNS.log("Playback was requested, but no audio data was loaded for playback", RNS.LOG_ERROR)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while playing message audio:"+str(e))
|
|
RNS.trace_exception(e)
|
|
|
|
def message_ptt_down_action(self, sender=None):
|
|
if self.sideband.ui_recording: return
|
|
|
|
self.sideband.ui_started_recording()
|
|
self.recording_started = time.time()
|
|
if self.sideband.config["hq_ptt"]: self.audio_msg_mode = LXMF.AM_OPUS_OGG
|
|
else: self.audio_msg_mode = LXMF.AM_CODEC2_2400
|
|
|
|
if not hasattr(self, "ptt_recorder") or self.ptt_recorder == None:
|
|
self.ptt_recording_path = os.path.join(self.sideband.rec_cache, "ptt_recording.ogg")
|
|
self.ptt_recorder = FileRecorder(self.ptt_recording_path, profile=Opus.PROFILE_VOICE_HIGH, gain=2.0,
|
|
skip=0.075, ease_in=0.125, filters=[BandPass(300, 8500), AGC(target_level=-15.0)])
|
|
|
|
self.message_attach_action(attach_type="audio", nodialog=True)
|
|
el_button = self.messages_view.ids.message_ptt_button
|
|
el_icon = self.messages_view.ids.message_ptt_button.children[0].children[1]
|
|
el_button.theme_text_color="Custom"
|
|
el_button.text_color=mdc("Orange","400")
|
|
el_button.line_color=mdc("Orange","400")
|
|
el_icon.theme_text_color="Custom"
|
|
el_icon.text_color=mdc("Orange","400")
|
|
self.ptt_recorder.start()
|
|
|
|
|
|
def message_ptt_up_action(self, sender=None):
|
|
if not self.sideband.ui_recording: return
|
|
|
|
el_button = self.messages_view.ids.message_ptt_button
|
|
el_icon = self.messages_view.ids.message_ptt_button.children[0].children[1]
|
|
el_button.theme_text_color="Custom"
|
|
el_button.text_color=mdc("BlueGray","500")
|
|
el_button.line_color=mdc("BlueGray","500")
|
|
el_icon.theme_text_color="Custom"
|
|
el_icon.text_color=mdc("BlueGray","500")
|
|
|
|
def job():
|
|
try:
|
|
self.ptt_recorder.stop()
|
|
self.ptt_recorder = None
|
|
except Exception as e:
|
|
RNS.log("An error occurred while stopping recording: "+str(e), RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
self.sideband.ui_stopped_recording()
|
|
if self.recording_started != None:
|
|
duration = time.time() - self.recording_started
|
|
self.recording_started = None
|
|
if duration < 0.6: RNS.log(f"Discarding recording, only {RNS.prettyshorttime(duration)} of audio", RNS.LOG_WARNING)
|
|
else:
|
|
if os.path.isfile(self.ptt_recording_path):
|
|
if self.message_process_audio(self.ptt_recording_path):
|
|
if self.outbound_mode_command: self.outbound_mode_reset()
|
|
self.message_send_action()
|
|
|
|
threading.Thread(target=job, daemon=True).start()
|
|
|
|
def message_process_audio(self, input_path):
|
|
from sideband.audioproc import voice_processing
|
|
if self.audio_msg_mode == LXMF.AM_OPUS_OGG:
|
|
proc_path = voice_processing(input_path)
|
|
if proc_path:
|
|
self.attach_path = proc_path
|
|
os.unlink(input_path)
|
|
RNS.log("Using voice-processed OPUS data in OGG container", RNS.LOG_DEBUG)
|
|
else:
|
|
self.attach_path = input_path
|
|
RNS.log("Using unmodified OPUS data in OGG container", RNS.LOG_DEBUG)
|
|
else:
|
|
ap_start = time.time()
|
|
proc_path = voice_processing(input_path)
|
|
if proc_path:
|
|
opus_file = pyogg.OpusFile(proc_path)
|
|
RNS.log("Using voice-processed audio for codec2 encoding", RNS.LOG_DEBUG)
|
|
else:
|
|
opus_file = pyogg.OpusFile(input_path)
|
|
RNS.log("Using unprocessed audio data for codec2 encoding", RNS.LOG_DEBUG)
|
|
|
|
RNS.log(f"OPUS LOAD {opus_file.frequency}Hz {opus_file.bytes_per_sample*8}bit {opus_file.channels}ch")
|
|
|
|
audio = AudioSegment(
|
|
bytes(opus_file.as_array()),
|
|
frame_rate=opus_file.frequency,
|
|
sample_width=opus_file.bytes_per_sample,
|
|
channels=opus_file.channels,
|
|
)
|
|
audio = audio.split_to_mono()[0]
|
|
audio = audio.apply_gain(-audio.max_dBFS)
|
|
|
|
if self.audio_msg_mode >= LXMF.AM_CODEC2_700C and self.audio_msg_mode <= LXMF.AM_CODEC2_3200:
|
|
audio = audio.set_frame_rate(8000)
|
|
audio = audio.set_sample_width(2)
|
|
samples = audio.get_array_of_samples()
|
|
|
|
from sideband.audioproc import encode_codec2, detect_codec2
|
|
if detect_codec2():
|
|
encoded = encode_codec2(samples, self.audio_msg_mode)
|
|
|
|
ap_duration = time.time() - ap_start
|
|
RNS.log("Audio processing complete in "+RNS.prettytime(ap_duration), RNS.LOG_DEBUG)
|
|
|
|
export_path = os.path.join(self.sideband.rec_cache, "recording.enc")
|
|
with open(export_path, "wb") as export_file:
|
|
export_file.write(encoded)
|
|
self.attach_path = export_path
|
|
os.unlink(input_path)
|
|
else:
|
|
self.display_codec2_error()
|
|
return False
|
|
|
|
return True
|
|
|
|
def message_init_rec_dialog(self):
|
|
ss = int(dp(18))
|
|
if RNS.vendor.platformutils.is_android(): self.request_microphone_permission()
|
|
self.recording_player = FilePlayer()
|
|
|
|
def a_rec_action(sender):
|
|
if not self.rec_dialog.recording and not self.rec_dialog.recorder:
|
|
self.sideband.ui_started_recording()
|
|
self.rec_dialog.recorder = FileRecorder(self.rec_dialog.file_path, profile=Opus.PROFILE_VOICE_HIGH, gain=2.0,
|
|
skip=0.075, ease_in=0.125, filters=[BandPass(300, 8500), AGC(target_level=-15.0)])
|
|
RNS.log("Starting recording...", RNS.LOG_DEBUG) # TODO: Remove
|
|
self.rec_dialog.recording = True
|
|
el = self.rec_dialog.rec_item.children[0].children[0]
|
|
el.ttc = el.theme_text_color; el.tc = el.text_color
|
|
el.theme_text_color="Custom"
|
|
el.text_color=mdc("Red","400")
|
|
el.icon = "stop-circle"
|
|
self.rec_dialog.rec_item.text = "[size="+str(ss)+"]Stop Recording[/size]"
|
|
self.rec_dialog.recorder.start()
|
|
|
|
else:
|
|
RNS.log("Stopping recording...", RNS.LOG_DEBUG) # TODO: Remove
|
|
self.rec_dialog.recorder.stop()
|
|
self.rec_dialog.recorder = None
|
|
RNS.log("Recording stopped", RNS.LOG_DEBUG) # TODO: Remove
|
|
self.rec_dialog.rec_item.text = "[size="+str(ss)+"]Start Recording[/size]"
|
|
el = self.rec_dialog.rec_item.children[0].children[0]
|
|
el.icon = "record"
|
|
el.text_color = self.theme_cls._get_text_color()
|
|
self.rec_dialog.play_item.disabled = False
|
|
self.rec_dialog.save_item.disabled = False
|
|
self.rec_dialog.recording = False
|
|
self.sideband.ui_stopped_recording()
|
|
|
|
self.msg_rec_a_rec = a_rec_action
|
|
|
|
def a_play(sender):
|
|
if self.rec_dialog.recording: a_rec_action(sender)
|
|
|
|
if not self.rec_dialog.playing:
|
|
RNS.log("Playing recording...", RNS.LOG_DEBUG)
|
|
self.rec_dialog.playing = True
|
|
self.rec_dialog.play_item.children[0].children[0].icon = "stop"
|
|
self.rec_dialog.play_item.text = "[size="+str(ss)+"]Stop[/size]"
|
|
self.recording_player.set_source(self.rec_dialog.file_path)
|
|
self.recording_player.play()
|
|
else:
|
|
RNS.log("Stopping playback...", RNS.LOG_DEBUG)
|
|
self.rec_dialog.playing = False
|
|
self.rec_dialog.play_item.children[0].children[0].icon = "play"
|
|
self.rec_dialog.play_item.text = "[size="+str(ss)+"]Play[/size]"
|
|
self.recording_player.stop()
|
|
|
|
self.msg_rec_a_play = a_play
|
|
|
|
def a_finished(sender):
|
|
RNS.log("Playback finished", RNS.LOG_DEBUG)
|
|
self.rec_dialog.playing = False
|
|
self.rec_dialog.play_item.children[0].children[0].icon = "play"
|
|
self.rec_dialog.play_item.text = "[size="+str(ss)+"]Play[/size]"
|
|
|
|
self.recording_player.finished_callback = a_finished
|
|
|
|
def a_save(sender):
|
|
if self.rec_dialog.recording: a_rec_action(sender)
|
|
self.rec_dialog_is_open = False
|
|
self.rec_dialog.dismiss()
|
|
|
|
try:
|
|
self.message_process_audio(self.rec_dialog.file_path)
|
|
if self.outbound_mode_command: self.outbound_mode_reset()
|
|
self.update_message_widgets()
|
|
toast("Added recorded audio to message")
|
|
|
|
except Exception as e:
|
|
RNS.trace_exception(e)
|
|
|
|
self.msg_rec_a_save = a_save
|
|
|
|
cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18))
|
|
rec_item = DialogItem(IconLeftWidget(icon="record", on_release=a_rec_action), text="[size="+str(ss)+"]Start Recording[/size]", on_release=a_rec_action)
|
|
play_item = DialogItem(IconLeftWidget(icon="play", on_release=a_play), text="[size="+str(ss)+"]Play[/size]", on_release=a_play, disabled=True)
|
|
save_item = DialogItem(IconLeftWidget(icon="content-save-move-outline", on_release=a_save), text="[size="+str(ss)+"]Save to message[/size]", on_release=a_save, disabled=True)
|
|
self.rec_dialog = MDDialog(
|
|
title="Record Audio",
|
|
type="simple",
|
|
items=[
|
|
rec_item,
|
|
play_item,
|
|
save_item,
|
|
],
|
|
buttons=[ cancel_button ],
|
|
width_offset=dp(32),
|
|
)
|
|
cancel_button.bind(on_release=self.rec_dialog.dismiss)
|
|
self.rec_dialog.recorder = None
|
|
self.rec_dialog.recording = False
|
|
self.rec_dialog.playing = False
|
|
self.rec_dialog.rec_item = rec_item
|
|
self.rec_dialog.play_item = play_item
|
|
self.rec_dialog.save_item = save_item
|
|
self.rec_dialog.file_path = os.path.join(self.sideband.rec_cache, "recording.ogg")
|
|
|
|
def message_record_audio_action(self):
|
|
ss = int(dp(18))
|
|
if self.rec_dialog == None: self.message_init_rec_dialog()
|
|
|
|
else:
|
|
self.rec_dialog.play_item.disabled = True
|
|
self.rec_dialog.save_item.disabled = True
|
|
self.rec_dialog.recording = False
|
|
self.rec_dialog.rec_item.text = "[size="+str(ss)+"]Start Recording[/size]"
|
|
self.rec_dialog.rec_item.children[0].children[0].icon = "record"
|
|
|
|
self.rec_dialog.open()
|
|
self.rec_dialog_is_open = True
|
|
|
|
def message_attach_action(self, attach_type=None, nodialog=False):
|
|
file_attach_types = ["lbimg", "defimg", "hqimg", "file"]
|
|
rec_attach_types = ["audio"]
|
|
|
|
self.attach_path = None
|
|
self.rec_dialog_is_open = False
|
|
if attach_type in file_attach_types:
|
|
self.attach_type = attach_type
|
|
if not nodialog:
|
|
self.message_select_file_action()
|
|
elif attach_type in rec_attach_types:
|
|
self.attach_type = attach_type
|
|
if not nodialog:
|
|
self.message_record_audio_action()
|
|
|
|
def message_attachment_action(self, sender):
|
|
self.rec_dialog_is_open = False
|
|
if self.attach_path == None:
|
|
def a_img_lb(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.message_attach_action(attach_type="lbimg")
|
|
def a_img_def(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.message_attach_action(attach_type="defimg")
|
|
def a_img_hq(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.message_attach_action(attach_type="hqimg")
|
|
def a_file(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.message_attach_action(attach_type="file")
|
|
def a_audio_hq(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.audio_msg_mode = LXMF.AM_OPUS_OGG
|
|
self.message_attach_action(attach_type="audio")
|
|
def a_audio_lb(sender):
|
|
self.attach_dialog.dismiss()
|
|
self.audio_msg_mode = LXMF.AM_CODEC2_2400
|
|
self.message_attach_action(attach_type="audio")
|
|
|
|
if self.attach_dialog == None:
|
|
ss = int(dp(18))
|
|
cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18))
|
|
ad_items = [
|
|
DialogItem(IconLeftWidget(icon="message-image-outline", on_release=a_img_lb), text="[size="+str(ss)+"]Low-bandwidth Image[/size]", on_release=a_img_lb),
|
|
DialogItem(IconLeftWidget(icon="file-image", on_release=a_img_def), text="[size="+str(ss)+"]Medium Image[/size]", on_release=a_img_def),
|
|
DialogItem(IconLeftWidget(icon="image-outline", on_release=a_img_hq), text="[size="+str(ss)+"]High-res Image[/size]", on_release=a_img_hq),
|
|
DialogItem(IconLeftWidget(icon="account-voice", on_release=a_audio_lb), text="[size="+str(ss)+"]Low-bandwidth Voice[/size]", on_release=a_audio_lb),
|
|
DialogItem(IconLeftWidget(icon="microphone-message", on_release=a_audio_hq), text="[size="+str(ss)+"]High-quality Voice[/size]", on_release=a_audio_hq),
|
|
DialogItem(IconLeftWidget(icon="file-outline", on_release=a_file), text="[size="+str(ss)+"]File Attachment[/size]", on_release=a_file)]
|
|
|
|
if RNS.vendor.platformutils.is_android() and android_api_version < 29:
|
|
ad_items.pop(3)
|
|
ad_items.pop(3)
|
|
|
|
self.attach_dialog = MDDialog(
|
|
title="Add Attachment",
|
|
type="simple",
|
|
text="Select the type of attachment you want to send with this message\n",
|
|
items=ad_items,
|
|
buttons=[ cancel_button ],
|
|
width_offset=dp(32),
|
|
)
|
|
|
|
cancel_button.bind(on_release=self.attach_dialog.dismiss)
|
|
|
|
self.attach_dialog.open()
|
|
|
|
else:
|
|
self.attach_path = None
|
|
self.attach_type = None
|
|
self.update_message_widgets()
|
|
|
|
toast("Attachment removed")
|
|
|
|
def shared_attachment_action(self, attachment_data):
|
|
if not self.root.ids.screen_manager.current == "messages_screen":
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("Please select a conversation first")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="No active conversation",
|
|
text="To drop files as attachments, please open a conversation first",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
else:
|
|
self.rec_dialog_is_open = False
|
|
|
|
def a_img_lb(sender):
|
|
self.attach_type="lbimg"
|
|
self.shared_attach_dialog.dismiss()
|
|
self.shared_attach_dialog.att_exc()
|
|
def a_img_def(sender):
|
|
self.attach_type="defimg"
|
|
self.shared_attach_dialog.dismiss()
|
|
self.shared_attach_dialog.att_exc()
|
|
def a_img_hq(sender):
|
|
self.attach_type="hqimg"
|
|
self.shared_attach_dialog.dismiss()
|
|
self.shared_attach_dialog.att_exc()
|
|
def a_file(sender):
|
|
self.attach_type="file"
|
|
self.shared_attach_dialog.dismiss()
|
|
self.shared_attach_dialog.att_exc()
|
|
|
|
if self.shared_attach_dialog == None:
|
|
ss = int(dp(18))
|
|
cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18))
|
|
ad_items = [
|
|
DialogItem(IconLeftWidget(icon="message-image-outline", on_release=a_img_lb), text="[size="+str(ss)+"]Low-bandwidth Image[/size]", on_release=a_img_lb),
|
|
DialogItem(IconLeftWidget(icon="file-image", on_release=a_img_def), text="[size="+str(ss)+"]Medium Image[/size]", on_release=a_img_def),
|
|
DialogItem(IconLeftWidget(icon="image-outline", on_release=a_img_hq), text="[size="+str(ss)+"]High-res Image[/size]", on_release=a_img_hq),
|
|
DialogItem(IconLeftWidget(icon="file-outline", on_release=a_file), text="[size="+str(ss)+"]File Attachment[/size]", on_release=a_file)]
|
|
|
|
self.shared_attach_dialog = MDDialog(
|
|
title="Add Attachment",
|
|
type="simple",
|
|
text="Select how you want to attach this data to the next message sent\n",
|
|
items=ad_items,
|
|
buttons=[ cancel_button ],
|
|
width_offset=dp(32),
|
|
)
|
|
|
|
cancel_button.bind(on_release=self.shared_attach_dialog.dismiss)
|
|
|
|
def att_exc():
|
|
self.message_fm_got_path(attachment_data["data_path"])
|
|
|
|
self.shared_attach_dialog.att_exc = att_exc
|
|
self.shared_attach_dialog.open()
|
|
|
|
def update_message_widgets(self):
|
|
toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children
|
|
mode_item = toolbar_items[1]
|
|
attachment_item = toolbar_items[4]
|
|
|
|
if self.attach_path != None:
|
|
attachment_item.icon = "attachment-check"
|
|
else:
|
|
attachment_item.icon = "attachment-plus"
|
|
|
|
if self.outbound_mode_paper:
|
|
mode_item.icon = "qrcode"
|
|
self.messages_view.ids.message_text.hint_text = "Paper message"
|
|
else:
|
|
if self.outbound_mode_command:
|
|
mode_item.icon = "console"
|
|
self.messages_view.ids.message_text.hint_text = "Send command or request"
|
|
else:
|
|
if not self.outbound_mode_propagation:
|
|
mode_item.icon = "lan-connect"
|
|
self.messages_view.ids.message_text.hint_text = "Message for direct delivery"
|
|
else:
|
|
mode_item.icon = "upload-network"
|
|
self.messages_view.ids.message_text.hint_text = "Message for propagation"
|
|
# self.root.ids.message_text.hint_text = "Write message for delivery via propagation nodes"
|
|
|
|
def key_query_action(self, sender):
|
|
context_dest = self.messages_view.ids.messages_scrollview.active_conversation
|
|
if self.sideband.request_key(context_dest):
|
|
keys_str = "Public key information for "+RNS.prettyhexrep(context_dest)+" was requested from the network. Waiting for request to be answered."
|
|
self.messages_view.ids.nokeys_text.text = keys_str
|
|
else:
|
|
keys_str = "Could not send request. Check your connectivity and addresses."
|
|
self.messages_view.ids.nokeys_text.text = keys_str
|
|
|
|
def message_area_detect(self):
|
|
context_dest = self.messages_view.ids.messages_scrollview.active_conversation
|
|
if self.sideband.is_known(context_dest):
|
|
self.messages_view.ids.messages_scrollview.dest_known = True
|
|
self.widget_hide(self.messages_view.ids.message_input_part, False)
|
|
self.widget_hide(self.messages_view.ids.no_keys_part, True)
|
|
else:
|
|
self.messages_view.ids.messages_scrollview.dest_known = False
|
|
if self.messages_view.ids.nokeys_text.text == "":
|
|
keys_str = "The crytographic keys for the destination address are unknown at this time. You can wait for an announce to arrive, or query the network for the necessary keys."
|
|
self.messages_view.ids.nokeys_text.text = keys_str
|
|
self.widget_hide(self.messages_view.ids.message_input_part, True)
|
|
self.widget_hide(self.messages_view.ids.message_ptt, True)
|
|
self.widget_hide(self.messages_view.ids.no_keys_part, False)
|
|
|
|
|
|
### Conversations screen
|
|
######################################
|
|
def conversations_action(self, sender=None, direction="left", no_transition=False):
|
|
self.rec_dialog_is_open = False
|
|
if self.include_objects:
|
|
self.include_conversations = True
|
|
self.include_objects = False
|
|
self.conversations_view.update()
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.open_conversations(direction=direction)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def objects_action(self, sender=None, direction="left", no_transition=False):
|
|
if self.include_conversations:
|
|
self.include_conversations = False
|
|
self.include_objects = True
|
|
self.conversations_view.update()
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.open_conversations(direction=direction)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def open_conversations(self, direction="left"):
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
|
|
if not self.conversations_view:
|
|
self.conversations_view = Conversations(self)
|
|
|
|
self.root.ids.screen_manager.current = "conversations_screen"
|
|
if self.messages_view:
|
|
self.messages_view.ids.messages_scrollview.active_conversation = None
|
|
|
|
def cb(dt):
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
self.sideband.setstate("wants.clear_notifications", True)
|
|
Clock.schedule_once(cb, 0.10)
|
|
|
|
def get_connectivity_text(self):
|
|
try:
|
|
connectivity_status = ""
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
connectivity_status = str(self.sideband.getstate("service.connectivity_status"))
|
|
|
|
else:
|
|
if self.sideband.reticulum.is_connected_to_shared_instance:
|
|
connectivity_status = "Sideband is connected via a shared Reticulum instance running on this system. Use the [b]rnstatus[/b] utility to obtain full connectivity info."
|
|
else:
|
|
connectivity_status = "Sideband is currently running a standalone or master Reticulum instance on this system. Use the [b]rnstatus[/b] utility to obtain full connectivity info."
|
|
|
|
return connectivity_status
|
|
except Exception as e:
|
|
RNS.log("An error occurred while retrieving connectivity status: "+str(e), RNS.LOG_ERROR)
|
|
return "Could not retrieve connectivity status"
|
|
|
|
def connectivity_status(self, sender):
|
|
if RNS.vendor.platformutils.is_android():
|
|
hs = dp(22)
|
|
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
full_button = MDRectangleFlatButton(text="Full RNS Status",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
dialog = MDDialog(
|
|
title="Connectivity Status",
|
|
text=str(self.get_connectivity_text()),
|
|
buttons=[full_button, yes_button])
|
|
def cs_updater(dt): dialog.text = str(self.get_connectivity_text())
|
|
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
if self.connectivity_updater != None: self.connectivity_updater.cancel()
|
|
|
|
def cb_rns(sender):
|
|
dialog.dismiss()
|
|
if self.connectivity_updater != None: self.connectivity_updater.cancel()
|
|
self.rnstatus_action(from_conversations=True)
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
full_button.bind(on_release=cb_rns)
|
|
dialog.open()
|
|
|
|
if self.connectivity_updater != None:
|
|
self.connectivity_updater.cancel()
|
|
|
|
self.connectivity_updater = Clock.schedule_interval(cs_updater, 2.0)
|
|
|
|
else:
|
|
self.rnstatus_action(from_conversations=True)
|
|
|
|
def rnstatus_action(self, sender=None, from_conversations=False):
|
|
if not self.utilities_ready: self.utilities_init()
|
|
self.utilities_screen.rnstatus_action(from_conversations=from_conversations)
|
|
|
|
def ingest_lxm_action(self, sender):
|
|
def cb(dt):
|
|
self.open_ingest_lxm_dialog(sender)
|
|
Clock.schedule_once(cb, 0.15)
|
|
|
|
def open_ingest_lxm_dialog(self, sender=None):
|
|
try:
|
|
cancel_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
|
|
ingest_button = MDRectangleFlatButton(text="Read LXM",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
|
|
dialog = MDDialog(
|
|
title="Ingest Paper Message",
|
|
text="You can read LXMF paper messages into this program by scanning a QR-code containing the message with your device camera or QR-scanner app, and then opening the resulting link in Sideband.\n\nAlternatively, you can copy an [b]lxm://[/b] link from any source to your clipboard, and ingest it using the [i]Read LXM[/i] button below.",
|
|
buttons=[ ingest_button, cancel_button ],
|
|
)
|
|
def dl_yes(s):
|
|
try:
|
|
lxm_uri = Clipboard.paste()
|
|
if not lxm_uri.lower().startswith(LXMF.LXMessage.URI_SCHEMA+"://"):
|
|
lxm_uri = LXMF.LXMessage.URI_SCHEMA+"://"+lxm_uri
|
|
|
|
self.ingest_lxm_uri(lxm_uri)
|
|
dialog.dismiss()
|
|
|
|
except Exception as e:
|
|
response = "Error ingesting message from URI: "+str(e)
|
|
RNS.log(response, RNS.LOG_ERROR)
|
|
self.sideband.setstate("lxm_uri_ingest.result", response)
|
|
dialog.dismiss()
|
|
|
|
def dl_no(s):
|
|
dialog.dismiss()
|
|
|
|
def dl_ds(s):
|
|
self.dialog_open = False
|
|
|
|
ingest_button.bind(on_release=dl_yes)
|
|
cancel_button.bind(on_release=dl_no)
|
|
|
|
dialog.bind(on_dismiss=dl_ds)
|
|
dialog.open()
|
|
self.dialog_open = True
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while creating ingest LXM dialog: "+str(e), RNS.LOG_ERROR)
|
|
|
|
def lxmf_sync_action(self, sender):
|
|
def cb(dt):
|
|
self.lxmf_sync_request(sender)
|
|
Clock.schedule_once(cb, 0.15)
|
|
|
|
def lxmf_sync_request(self, sender):
|
|
if self.sideband.message_router.get_outbound_propagation_node() == None:
|
|
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
|
|
dialog = MDDialog(
|
|
title="Can't Sync",
|
|
text="No active LXMF propagation nodes were found. Cannot fetch messages. Wait for a Propagation Node to announce on the network, or manually specify one in the settings.",
|
|
buttons=[ yes_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
dialog.open()
|
|
else:
|
|
if self.sideband.config["lxmf_sync_limit"]:
|
|
sl = self.sideband.config["lxmf_sync_max"]
|
|
else:
|
|
sl = None
|
|
|
|
sync_title = "LXMF Sync"
|
|
if not hasattr(self, "message_sync_dialog") or self.message_sync_dialog == None:
|
|
close_button = MDRectangleFlatButton(text="Close",font_size=dp(18))
|
|
stop_button = MDRectangleFlatButton(text="Stop",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject)
|
|
|
|
dialog_content = MsgSync()
|
|
dialog = MDDialog(
|
|
title=sync_title,
|
|
type="custom",
|
|
content_cls=dialog_content,
|
|
buttons=[ stop_button, close_button ],
|
|
# elevation=0,
|
|
)
|
|
dialog.d_content = dialog_content
|
|
def dl_close(s):
|
|
self.sideband.setstate("app.flags.lxmf_sync_dialog_open", False)
|
|
dialog.dismiss()
|
|
self.message_sync_dialog.d_content.ids.sync_progress.value = 0.1
|
|
self.message_sync_dialog.d_content.ids.sync_status.text = ""
|
|
if self.sideband.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE:
|
|
self.sideband.message_router.acknowledge_sync_completion(reset_state=True)
|
|
|
|
def dl_stop(s):
|
|
self.sideband.cancel_lxmf_sync()
|
|
def cb(dt):
|
|
self.widget_hide(self.sync_dialog.stop_button, True)
|
|
Clock.schedule_once(cb, 0.25)
|
|
|
|
close_button.bind(on_release=dl_close)
|
|
stop_button.bind(on_release=dl_stop)
|
|
|
|
self.message_sync_dialog = dialog
|
|
self.sync_dialog = dialog_content
|
|
self.sync_dialog.stop_button = stop_button
|
|
|
|
s_state = self.sideband.message_router.propagation_transfer_state
|
|
if s_state > LXMF.LXMRouter.PR_PATH_REQUESTED and s_state <= LXMF.LXMRouter.PR_COMPLETE:
|
|
dsp = self.sideband.get_sync_progress()*100
|
|
else:
|
|
dsp = 0
|
|
|
|
self.sideband.setstate("app.flags.lxmf_sync_dialog_open", True)
|
|
self.message_sync_dialog.title = sync_title
|
|
self.message_sync_dialog.d_content.ids.node_info.text = f"Via {RNS.prettyhexrep(self.sideband.message_router.get_outbound_propagation_node())}\n"
|
|
self.message_sync_dialog.d_content.ids.sync_status.text = self.sideband.get_sync_status()
|
|
self.message_sync_dialog.d_content.ids.sync_progress.value = dsp
|
|
self.message_sync_dialog.d_content.ids.sync_progress.start()
|
|
self.sync_dialog.ids.sync_progress.stop()
|
|
self.message_sync_dialog.open()
|
|
|
|
def sij(dt):
|
|
self.sideband.setpersistent("lxmf.lastsync", time.time())
|
|
self.sideband.setpersistent("lxmf.syncretrying", False)
|
|
self.sideband.request_lxmf_sync(limit=sl)
|
|
Clock.schedule_once(sij, 0.1)
|
|
|
|
def new_conversation_action(self, sender=None):
|
|
def cb(dt):
|
|
self.new_conversation_request(sender)
|
|
Clock.schedule_once(cb, 0.15)
|
|
|
|
def new_conversation_request(self, sender=None):
|
|
try:
|
|
cancel_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
|
|
create_button = MDRectangleFlatButton(text="Create",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
|
|
dialog_content = NewConv()
|
|
dialog = MDDialog(
|
|
title="New Conversation",
|
|
type="custom",
|
|
content_cls=dialog_content,
|
|
buttons=[ create_button, cancel_button ],
|
|
# elevation=0,
|
|
)
|
|
dialog.d_content = dialog_content
|
|
def dl_yes(s):
|
|
new_result = False
|
|
try:
|
|
n_address = dialog.d_content.ids["n_address_field"].text
|
|
n_name = dialog.d_content.ids["n_name_field"].text
|
|
n_trusted = dialog.d_content.ids["n_trusted"].active
|
|
n_voice_only = dialog.d_content.ids["n_voice_only"].active
|
|
new_result = self.sideband.new_conversation(n_address, n_name, n_trusted, n_voice_only)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR)
|
|
|
|
if new_result:
|
|
dialog.d_content.ids["n_address_field"].helper_text = ""
|
|
dialog.d_content.ids["n_address_field"].helper_text_mode = "on_focus"
|
|
dialog.d_content.ids["n_address_field"].error = False
|
|
dialog.dismiss()
|
|
if self.conversations_view != None:
|
|
self.conversations_view.update()
|
|
else:
|
|
dialog.d_content.ids["n_address_field"].helper_text = "Invalid address, check your input"
|
|
dialog.d_content.ids["n_address_field"].helper_text_mode = "persistent"
|
|
dialog.d_content.ids["n_address_field"].error = True
|
|
# dialog.d_content.ids["n_error_field"].text = "Could not create conversation. Check your input."
|
|
|
|
def dl_no(s):
|
|
dialog.dismiss()
|
|
|
|
def dl_ds(s):
|
|
self.dialog_open = False
|
|
|
|
create_button.bind(on_release=dl_yes)
|
|
cancel_button.bind(on_release=dl_no)
|
|
|
|
dialog.bind(on_dismiss=dl_ds)
|
|
dialog.open()
|
|
self.dialog_open = True
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while creating new conversation dialog: "+str(e), RNS.LOG_ERROR)
|
|
|
|
### Information/version screen
|
|
######################################
|
|
def information_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("information_screen"):
|
|
self.information_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.information_init()
|
|
def o(dt):
|
|
self.information_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def information_init(self):
|
|
if not self.root.ids.screen_manager.has_screen("information_screen"):
|
|
self.information_screen = Builder.load_string(layout_information_screen)
|
|
self.information_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.information_screen)
|
|
|
|
def link_exec(sender=None, event=None):
|
|
def lj():
|
|
webbrowser.open("https://unsigned.io/donate")
|
|
threading.Thread(target=lj, daemon=True).start()
|
|
|
|
self.information_screen.ids.information_scrollview.effect_cls = ScrollEffect
|
|
self.information_screen.ids.information_logo.icon = os.path.join(self.sideband.asset_dir, "rns_256.png")
|
|
|
|
str_comps = " - [b]Reticulum[/b] (Reticulum License)\n - [b]LXMF[/b] (Reticulum License)\n - [b]LXST[/b] (Reticulum License)"
|
|
str_comps += "\n - [b]Kivy[/b] (MIT License)\n - [b]KivyMD[/b] (MIT License)"
|
|
str_comps += "\n - [b]Codec2[/b] (LGPL License)\n - [b]PyCodec2[/b] (BSD-3 License)\n - [b]Able[/b] (MIT License)"
|
|
str_comps += "\n - [b]GeoidHeight[/b] (LGPL License)\n - [b]Paho MQTT[/b] (EPL2 License)\n - [b]Python[/b] (PSF License)"
|
|
str_comps += "\n\nGo to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project.\n\nThe Sideband app is Copyright © 2026 Mark Qvist / unsigned.io\n\nPermission is granted to freely share and distribute binary copies of "+self.root.ids.app_version_info.text+", so long as no payment or compensation is charged for said distribution or sharing.\n\nIf you were charged or paid anything for this copy of Sideband, please report it to [b]license@unsigned.io[/b].\n\nTHIS IS EXPERIMENTAL SOFTWARE - SIDEBAND COMES WITH ABSOLUTELY NO WARRANTY - USE AT YOUR OWN RISK AND RESPONSIBILITY"
|
|
info = "This is "+self.root.ids.app_version_info.text+"\nRunning on RNS v"+RNS.__version__+", LXMF v"+LXMF.__version__+" and LXST v"+lxst_version+".\n\nHumbly build using the following open components:\n\n"+str_comps
|
|
self.information_screen.ids.information_info.text = info
|
|
self.information_screen.ids.information_info.bind(on_ref_press=link_exec)
|
|
|
|
def information_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = "left"
|
|
self.root.ids.screen_manager.current = "information_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def close_information_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
|
|
### Settings screen
|
|
######################################
|
|
def settings_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
if self.sideband.active_propagation_node != None:
|
|
self.settings_screen.ids.settings_propagation_node_address.text = RNS.hexrep(self.sideband.active_propagation_node, delimit=False)
|
|
self.root.ids.screen_manager.current = "settings_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def configure_ui_scaling_action(self, sender=None):
|
|
global app_ui_scaling_path
|
|
try:
|
|
cancel_button = MDRectangleFlatButton(text="Cancel",font_size=dp(18))
|
|
set_button = MDRectangleFlatButton(text="Set",font_size=dp(18), theme_text_color="Custom", line_color=self.color_accept, text_color=self.color_accept)
|
|
|
|
dialog_content = UIScaling()
|
|
dialog = MDDialog(
|
|
title="UI Scaling",
|
|
type="custom",
|
|
content_cls=dialog_content,
|
|
buttons=[ set_button, cancel_button ],
|
|
# elevation=0,
|
|
)
|
|
dialog.d_content = dialog_content
|
|
dialog.d_content.ids["scaling_factor"].text = os.environ["KIVY_METRICS_DENSITY"] if "KIVY_METRICS_DENSITY" in os.environ else "0.0"
|
|
def dl_yes(s):
|
|
new_sf = 1.0
|
|
scaling_ok = False
|
|
try:
|
|
si = dialog.d_content.ids["scaling_factor"].text
|
|
sf = float(si)
|
|
if (sf >= 0.3 and sf <= 5.0) or sf == 0.0:
|
|
new_sf = sf
|
|
scaling_ok = True
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while getting scaling factor from user: "+str(e), RNS.LOG_ERROR)
|
|
|
|
if scaling_ok:
|
|
dialog.d_content.ids["scaling_factor"].helper_text = ""
|
|
dialog.d_content.ids["scaling_factor"].helper_text_mode = "on_focus"
|
|
dialog.d_content.ids["scaling_factor"].error = False
|
|
dialog.dismiss()
|
|
if app_ui_scaling_path == None:
|
|
RNS.log("No path to UI scaling factor file could be found, cannot save scaling factor", RNS.LOG_ERROR)
|
|
else:
|
|
try:
|
|
with open(app_ui_scaling_path, "w") as sfile:
|
|
sfile.write(str(new_sf))
|
|
RNS.log(f"Saved configured scaling factor {new_sf} to {app_ui_scaling_path}", RNS.LOG_DEBUG)
|
|
except Exception as e:
|
|
RNS.log(f"Error while saving scaling factor {new_sf} to {app_ui_scaling_path}: {e}", RNS.LOG_ERROR)
|
|
|
|
else:
|
|
dialog.d_content.ids["scaling_factor"].helper_text = "Invalid scale factor, check your input"
|
|
dialog.d_content.ids["scaling_factor"].helper_text_mode = "persistent"
|
|
dialog.d_content.ids["scaling_factor"].error = True
|
|
|
|
def dl_no(s):
|
|
dialog.dismiss()
|
|
|
|
def dl_ds(s):
|
|
self.dialog_open = False
|
|
|
|
set_button.bind(on_release=dl_yes)
|
|
cancel_button.bind(on_release=dl_no)
|
|
|
|
dialog.bind(on_dismiss=dl_ds)
|
|
dialog.open()
|
|
self.dialog_open = True
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while creating UI scaling dialog: "+str(e), RNS.LOG_ERROR)
|
|
|
|
|
|
def settings_action(self, sender=None, direction="left"):
|
|
if self.settings_ready:
|
|
self.settings_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.settings_init()
|
|
def o(dt):
|
|
self.settings_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def interval_to_slider_val(self, interval):
|
|
try:
|
|
mseg = 72; hseg = 84; sv = 0
|
|
mm = mseg*5*60; hm = hseg*60*30+mm
|
|
|
|
if interval <= mm:
|
|
sv = interval/60/5
|
|
elif interval > mm and interval <= hm:
|
|
half_hours = interval/(60*30)-(mm/(60*30))
|
|
sv = mseg+half_hours
|
|
else:
|
|
days = (interval/86400)-((hseg*60*30)/84600)-(mm/86400)
|
|
sv = 1+mseg+hseg+days
|
|
except Exception as e:
|
|
return 43200
|
|
|
|
return sv
|
|
|
|
def bind_clipboard_actions(self, ids, force=False):
|
|
if force or RNS.vendor.platformutils.is_android():
|
|
BIND_CLASSES = ["kivymd.uix.textfield.textfield.MDTextField",]
|
|
for e in ids:
|
|
te = ids[e]
|
|
ts = str(te).split(" ")[0].replace("<", "")
|
|
if ts in BIND_CLASSES and not hasattr(e, "no_clipboard"):
|
|
te.bind(on_double_tap=self.ui_clipboard_action)
|
|
|
|
def settings_init(self, sender=None):
|
|
if not self.settings_ready:
|
|
if not self.root.ids.screen_manager.has_screen("settings_screen"):
|
|
self.settings_screen = Builder.load_string(layout_settings_screen)
|
|
self.settings_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.settings_screen)
|
|
self.bind_clipboard_actions(self.settings_screen.ids)
|
|
|
|
self.settings_screen.ids.settings_scrollview.effect_cls = ScrollEffect
|
|
|
|
self.settings_screen.ids.settings_info_lang.text = "\nIf you write messages in another script than Latin, Greek or Cyrillic, you can configure the text input language for messages and other fields below.\n"
|
|
|
|
info1_text = "\nYou can set your [b]Display Name[/b] to a custom value, or leave it as the default unspecified value. "
|
|
info1_text += "This name will be included in any announces you send, and will be visible to others on the network. "
|
|
info1_text += "\n\nYou can manually specify which [b]Propagation Node[/b] to use, but if none is specified, Sideband will "
|
|
info1_text += "automatically select one nearby."
|
|
if RNS.vendor.platformutils.is_android():
|
|
info1_text += "\n\nDouble-tap any field to copy its value, and double-tap an empty field to paste into it."
|
|
|
|
self.settings_screen.ids.settings_info1.text = info1_text
|
|
|
|
|
|
def save_disp_name(sender=None, event=None):
|
|
if not sender.focus:
|
|
in_name = self.settings_screen.ids.settings_display_name.text
|
|
if in_name == "":
|
|
new_name = "Anonymous Peer"
|
|
else:
|
|
new_name = in_name
|
|
|
|
self.sideband.config["display_name"] = new_name
|
|
self.sideband.save_configuration()
|
|
|
|
def save_prop_addr(sender=None, event=None):
|
|
if not sender.focus:
|
|
in_addr = self.settings_screen.ids.settings_propagation_node_address.text
|
|
|
|
new_addr = None
|
|
if in_addr == "":
|
|
new_addr = None
|
|
self.settings_screen.ids.settings_propagation_node_address.error = False
|
|
else:
|
|
if len(in_addr) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2:
|
|
new_addr = None
|
|
else:
|
|
try:
|
|
new_addr = bytes.fromhex(in_addr)
|
|
except Exception as e:
|
|
new_addr = None
|
|
|
|
if new_addr == None:
|
|
self.settings_screen.ids.settings_propagation_node_address.error = True
|
|
else:
|
|
self.settings_screen.ids.settings_propagation_node_address.error = False
|
|
|
|
|
|
self.sideband.config["lxmf_propagation_node"] = new_addr
|
|
self.sideband.set_active_propagation_node(self.sideband.config["lxmf_propagation_node"])
|
|
|
|
def save_input_lang(sender=None, event=None):
|
|
if sender.active:
|
|
if sender != self.settings_screen.ids.settings_lang_default:
|
|
self.settings_screen.ids.settings_lang_default.active = False
|
|
|
|
if sender != self.settings_screen.ids.settings_lang_chinese:
|
|
self.settings_screen.ids.settings_lang_chinese.active = False
|
|
|
|
if sender != self.settings_screen.ids.settings_lang_japanese:
|
|
self.settings_screen.ids.settings_lang_japanese.active = False
|
|
|
|
if sender != self.settings_screen.ids.settings_lang_korean:
|
|
self.settings_screen.ids.settings_lang_korean.active = False
|
|
|
|
if sender != self.settings_screen.ids.settings_lang_devangari:
|
|
self.settings_screen.ids.settings_lang_devangari.active = False
|
|
|
|
if sender != self.settings_screen.ids.settings_lang_hebrew:
|
|
self.settings_screen.ids.settings_lang_hebrew.active = False
|
|
|
|
if self.settings_screen.ids.settings_lang_default.active:
|
|
self.sideband.config["input_language"] = None
|
|
self.settings_screen.ids.settings_display_name.font_name = "defaultinput"
|
|
elif self.settings_screen.ids.settings_lang_chinese.active:
|
|
self.sideband.config["input_language"] = "chinese"
|
|
self.settings_screen.ids.settings_display_name.font_name = "chinese"
|
|
elif self.settings_screen.ids.settings_lang_japanese.active:
|
|
self.sideband.config["input_language"] = "japanese"
|
|
self.settings_screen.ids.settings_display_name.font_name = "japanese"
|
|
elif self.settings_screen.ids.settings_lang_korean.active:
|
|
self.sideband.config["input_language"] = "korean"
|
|
self.settings_screen.ids.settings_display_name.font_name = "korean"
|
|
elif self.settings_screen.ids.settings_lang_devangari.active:
|
|
self.sideband.config["input_language"] = "combined"
|
|
self.settings_screen.ids.settings_display_name.font_name = "combined"
|
|
elif self.settings_screen.ids.settings_lang_hebrew.active:
|
|
self.sideband.config["input_language"] = "hebrew"
|
|
self.settings_screen.ids.settings_display_name.font_name = "hebrew"
|
|
else:
|
|
self.sideband.config["input_language"] = None
|
|
self.settings_screen.ids.settings_display_name.font_name = "defaultinput"
|
|
|
|
|
|
self.sideband.save_configuration()
|
|
self.update_input_language()
|
|
|
|
def save_dark_ui(sender=None, event=None):
|
|
self.sideband.config["dark_ui"] = self.settings_screen.ids.settings_dark_ui.active
|
|
self.sideband.save_configuration()
|
|
self.update_ui_theme()
|
|
|
|
def save_eink_mode(sender=None, event=None):
|
|
self.sideband.config["eink_mode"] = self.settings_screen.ids.settings_eink_mode.active
|
|
self.sideband.save_configuration()
|
|
self.update_ui_theme()
|
|
|
|
def save_classic_message_colors(sender=None, event=None):
|
|
self.sideband.config["classic_message_colors"] = self.settings_screen.ids.settings_classic_message_colors.active
|
|
self.sideband.save_configuration()
|
|
self.update_ui_theme()
|
|
|
|
def save_display_style_in_contact_list(sender=None, event=None):
|
|
self.sideband.config["display_style_in_contact_list"] = self.settings_screen.ids.display_style_in_contact_list.active
|
|
self.sideband.save_configuration()
|
|
self.sideband.setstate("wants.viewupdate.conversations", True)
|
|
|
|
def save_display_style_from_trusted_only(sender=None, event=None):
|
|
self.sideband.config["display_style_from_all"] = not self.settings_screen.ids.display_style_from_trusted_only.active
|
|
self.sideband.save_configuration()
|
|
self.sideband.setstate("wants.viewupdate.conversations", True)
|
|
|
|
def save_trusted_markup_only(sender=None, event=None):
|
|
self.sideband.config["trusted_markup_only"] = self.settings_screen.ids.settings_trusted_markup_only.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_compose_in_markdown(sender=None, event=None):
|
|
self.sideband.config["compose_in_markdown"] = self.settings_screen.ids.settings_compose_in_markdown.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_advanced_stats(sender=None, event=None):
|
|
self.sideband.config["advanced_stats"] = self.settings_screen.ids.settings_advanced_statistics.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_notifications_on(sender=None, event=None):
|
|
self.sideband.config["notifications_on"] = self.settings_screen.ids.settings_notifications_on.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_start_announce(sender=None, event=None):
|
|
self.sideband.config["start_announce"] = self.settings_screen.ids.settings_start_announce.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_delivery_by_default(sender=None, event=None):
|
|
self.sideband.config["propagation_by_default"] = self.settings_screen.ids.settings_lxmf_delivery_by_default.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_try_propagation_on_fail(sender=None, event=None):
|
|
self.sideband.config["lxmf_try_propagation_on_fail"] = self.settings_screen.ids.settings_lxmf_try_propagation_on_fail.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_ignore_unknown(sender=None, event=None):
|
|
self.sideband.config["lxmf_ignore_unknown"] = self.settings_screen.ids.settings_lxmf_ignore_unknown.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_ignore_invalid_stamps(sender=None, event=None):
|
|
self.sideband.config["lxmf_ignore_invalid_stamps"] = self.settings_screen.ids.settings_ignore_invalid_stamps.active
|
|
self.sideband.save_configuration()
|
|
self.sideband.update_ignore_invalid_stamps()
|
|
|
|
def save_lxmf_sync_limit(sender=None, event=None):
|
|
self.sideband.config["lxmf_sync_limit"] = self.settings_screen.ids.settings_lxmf_sync_limit.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxm_limit_1mb(sender=None, event=None):
|
|
self.sideband.config["lxm_limit_1mb"] = self.settings_screen.ids.settings_lxm_limit_1mb.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_debug(sender=None, event=None):
|
|
self.sideband.config["debug"] = self.settings_screen.ids.settings_debug.active
|
|
self.sideband.save_configuration()
|
|
self.sideband._reticulum_log_debug(self.sideband.config["debug"])
|
|
|
|
def save_block_predictive_text(sender=None, event=None):
|
|
self.sideband.config["block_predictive_text"] = self.settings_screen.ids.settings_block_predictive_text.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_hq_ptt(sender=None, event=None):
|
|
self.sideband.config["hq_ptt"] = self.settings_screen.ids.settings_hq_ptt.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_voice_enabled(sender=None, event=None):
|
|
self.sideband.config["voice_enabled"] = self.settings_screen.ids.settings_voice_enabled.active
|
|
self.sideband.save_configuration()
|
|
|
|
if self.sideband.config["voice_enabled"] == True:
|
|
self.request_microphone_permission()
|
|
self.sideband.start_voice()
|
|
else: self.sideband.stop_voice()
|
|
|
|
def save_start_at_boot(sender=None, event=None):
|
|
self.sideband.config["start_at_boot"] = self.settings_screen.ids.settings_start_at_boot.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_print_command(sender=None, event=None):
|
|
if not sender.focus:
|
|
in_cmd = self.settings_screen.ids.settings_print_command.text
|
|
if in_cmd == "":
|
|
new_cmd = "lp"
|
|
else:
|
|
new_cmd = in_cmd
|
|
|
|
self.sideband.config["print_command"] = new_cmd.strip()
|
|
self.settings_screen.ids.settings_print_command.text = self.sideband.config["print_command"]
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_stamp_cost(sender=None, event=None, save=True):
|
|
if self.settings_screen.ids.settings_lxmf_require_stamps.active:
|
|
self.widget_hide(self.settings_screen.ids.lxmf_costslider_container, False)
|
|
else:
|
|
self.widget_hide(self.settings_screen.ids.lxmf_costslider_container, True)
|
|
|
|
if save:
|
|
self.sideband.config["lxmf_require_stamps"] = self.settings_screen.ids.settings_lxmf_require_stamps.active
|
|
self.sideband.save_configuration()
|
|
|
|
def save_lxmf_periodic_sync(sender=None, event=None, save=True):
|
|
if self.settings_screen.ids.settings_lxmf_periodic_sync.active:
|
|
self.widget_hide(self.settings_screen.ids.lxmf_syncslider_container, False)
|
|
else:
|
|
self.widget_hide(self.settings_screen.ids.lxmf_syncslider_container, True)
|
|
|
|
if save:
|
|
self.sideband.config["lxmf_periodic_sync"] = self.settings_screen.ids.settings_lxmf_periodic_sync.active
|
|
self.sideband.save_configuration()
|
|
|
|
def sync_interval_change(sender=None, event=None, save=True):
|
|
slider_val = int(self.settings_screen.ids.settings_lxmf_sync_interval.value)
|
|
mseg = 72; hseg = 84
|
|
if slider_val <= mseg:
|
|
interval = slider_val*5*60
|
|
elif slider_val > mseg and slider_val <= mseg+hseg:
|
|
h = (slider_val-mseg)/2; mm = mseg*5*60
|
|
interval = h*60*60+mm
|
|
else:
|
|
d = slider_val-hseg-mseg
|
|
hm = (hseg/2)*60*60; mm = mseg*5*60
|
|
interval = d*86400+hm+mm
|
|
|
|
interval_text = RNS.prettytime(interval)
|
|
pre = self.settings_screen.ids.settings_lxmf_sync_periodic.text
|
|
self.settings_screen.ids.settings_lxmf_sync_periodic.text = "Auto sync every "+interval_text
|
|
if save:
|
|
if (event == None or not hasattr(event, "button") or not event.button) or not "scroll" in event.button:
|
|
self.sideband.config["lxmf_sync_interval"] = interval
|
|
self.sideband.save_configuration()
|
|
|
|
def stamp_cost_change(sender=None, event=None, save=True):
|
|
slider_val = int(self.settings_screen.ids.settings_lxmf_require_stamps_cost.value)
|
|
cost_text = str(slider_val)
|
|
|
|
self.settings_screen.ids.settings_lxmf_require_stamps_label.text = f"Require stamp cost {cost_text} for incoming messages"
|
|
if save:
|
|
if slider_val > 32:
|
|
slider_val = 32
|
|
if slider_val < 1:
|
|
slider_val = 1
|
|
self.sideband.config["lxmf_inbound_stamp_cost"] = slider_val
|
|
if (event == None or not hasattr(event, "button") or not event.button) or not "scroll" in event.button:
|
|
self.sideband.save_configuration()
|
|
|
|
self.settings_screen.ids.settings_lxmf_address.text = RNS.hexrep(self.sideband.lxmf_destination.hash, delimit=False)
|
|
self.settings_screen.ids.settings_identity_hash.text = RNS.hexrep(self.sideband.lxmf_destination.identity.hash, delimit=False)
|
|
|
|
self.settings_screen.ids.settings_display_name.text = self.sideband.config["display_name"]
|
|
self.settings_screen.ids.settings_display_name.bind(focus=save_disp_name)
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
pass
|
|
# self.widget_hide(self.settings_screen.ids.settings_print_command, True)
|
|
else:
|
|
self.settings_screen.ids.settings_print_command.text = self.sideband.config["print_command"]
|
|
self.settings_screen.ids.settings_print_command.bind(focus=save_print_command)
|
|
self.settings_screen.ids.settings_start_at_boot.disabled = True
|
|
|
|
if self.sideband.config["lxmf_propagation_node"] == None: prop_node_addr = ""
|
|
else: prop_node_addr = RNS.hexrep(self.sideband.config["lxmf_propagation_node"], delimit=False)
|
|
|
|
self.settings_screen.ids.settings_propagation_node_address.text = prop_node_addr
|
|
self.settings_screen.ids.settings_propagation_node_address.bind(focus=save_prop_addr)
|
|
|
|
if not RNS.vendor.platformutils.is_android() or android_api_version >= 26:
|
|
self.settings_screen.ids.settings_notifications_on.active = self.sideband.config["notifications_on"]
|
|
self.settings_screen.ids.settings_notifications_on.bind(active=save_notifications_on)
|
|
else:
|
|
self.settings_screen.ids.settings_notifications_on.active = False
|
|
self.settings_screen.ids.settings_notifications_on.disabled = True
|
|
|
|
self.settings_screen.ids.settings_dark_ui.active = self.sideband.config["dark_ui"]
|
|
self.settings_screen.ids.settings_dark_ui.bind(active=save_dark_ui)
|
|
|
|
self.settings_screen.ids.settings_eink_mode.active = self.sideband.config["eink_mode"]
|
|
self.settings_screen.ids.settings_eink_mode.bind(active=save_eink_mode)
|
|
|
|
self.settings_screen.ids.settings_classic_message_colors.active = self.sideband.config["classic_message_colors"]
|
|
self.settings_screen.ids.settings_classic_message_colors.bind(active=save_classic_message_colors)
|
|
|
|
self.settings_screen.ids.display_style_in_contact_list.active = self.sideband.config["display_style_in_contact_list"]
|
|
self.settings_screen.ids.display_style_in_contact_list.bind(active=save_display_style_in_contact_list)
|
|
|
|
self.settings_screen.ids.display_style_from_trusted_only.active = not self.sideband.config["display_style_from_all"]
|
|
self.settings_screen.ids.display_style_from_trusted_only.bind(active=save_display_style_from_trusted_only)
|
|
|
|
self.settings_screen.ids.settings_advanced_statistics.active = self.sideband.config["advanced_stats"]
|
|
self.settings_screen.ids.settings_advanced_statistics.bind(active=save_advanced_stats)
|
|
|
|
self.settings_screen.ids.settings_start_at_boot.active = self.sideband.config["start_at_boot"]
|
|
self.settings_screen.ids.settings_start_at_boot.bind(active=save_start_at_boot)
|
|
|
|
self.settings_screen.ids.settings_start_announce.active = self.sideband.config["start_announce"]
|
|
self.settings_screen.ids.settings_start_announce.bind(active=save_start_announce)
|
|
|
|
self.settings_screen.ids.settings_lxmf_delivery_by_default.active = self.sideband.config["propagation_by_default"]
|
|
self.settings_screen.ids.settings_lxmf_delivery_by_default.bind(active=save_lxmf_delivery_by_default)
|
|
|
|
self.settings_screen.ids.settings_lxmf_try_propagation_on_fail.active = self.sideband.config["lxmf_try_propagation_on_fail"]
|
|
self.settings_screen.ids.settings_lxmf_try_propagation_on_fail.bind(active=save_lxmf_try_propagation_on_fail)
|
|
|
|
self.settings_screen.ids.settings_lxmf_ignore_unknown.active = self.sideband.config["lxmf_ignore_unknown"]
|
|
self.settings_screen.ids.settings_lxmf_ignore_unknown.bind(active=save_lxmf_ignore_unknown)
|
|
|
|
self.settings_screen.ids.settings_trusted_markup_only.active = self.sideband.config["trusted_markup_only"]
|
|
self.settings_screen.ids.settings_trusted_markup_only.bind(active=save_trusted_markup_only)
|
|
|
|
self.settings_screen.ids.settings_compose_in_markdown.active = self.sideband.config["compose_in_markdown"]
|
|
self.settings_screen.ids.settings_compose_in_markdown.bind(active=save_compose_in_markdown)
|
|
|
|
self.settings_screen.ids.settings_ignore_invalid_stamps.active = self.sideband.config["lxmf_ignore_invalid_stamps"]
|
|
self.settings_screen.ids.settings_ignore_invalid_stamps.bind(active=save_lxmf_ignore_invalid_stamps)
|
|
|
|
self.settings_screen.ids.settings_lxmf_periodic_sync.active = self.sideband.config["lxmf_periodic_sync"]
|
|
self.settings_screen.ids.settings_lxmf_periodic_sync.bind(active=save_lxmf_periodic_sync)
|
|
save_lxmf_periodic_sync(save=False)
|
|
|
|
def sync_interval_change_cb(sender=None, event=None):
|
|
sync_interval_change(sender=sender, event=event, save=False)
|
|
self.settings_screen.ids.settings_lxmf_sync_interval.bind(value=sync_interval_change_cb)
|
|
self.settings_screen.ids.settings_lxmf_sync_interval.bind(on_touch_up=sync_interval_change)
|
|
self.settings_screen.ids.settings_lxmf_sync_interval.value = self.interval_to_slider_val(self.sideband.config["lxmf_sync_interval"])
|
|
sync_interval_change(save=False)
|
|
|
|
self.settings_screen.ids.settings_lxmf_require_stamps.active = self.sideband.config["lxmf_require_stamps"]
|
|
self.settings_screen.ids.settings_lxmf_require_stamps.bind(active=save_lxmf_stamp_cost)
|
|
save_lxmf_stamp_cost(save=False)
|
|
|
|
def stamp_cost_change_cb(sender=None, event=None):
|
|
stamp_cost_change(sender=sender, event=event, save=False)
|
|
self.settings_screen.ids.settings_lxmf_require_stamps_cost.bind(value=stamp_cost_change_cb)
|
|
self.settings_screen.ids.settings_lxmf_require_stamps_cost.bind(on_touch_up=stamp_cost_change)
|
|
cost_val = self.sideband.config["lxmf_inbound_stamp_cost"]
|
|
if cost_val == None or cost_val < 1:
|
|
cost_val = 1
|
|
if cost_val > 32:
|
|
cost_val = 32
|
|
self.settings_screen.ids.settings_lxmf_require_stamps_cost.value = cost_val
|
|
stamp_cost_change(save=False)
|
|
|
|
if self.sideband.config["lxmf_sync_limit"] == None or self.sideband.config["lxmf_sync_limit"] == False:
|
|
sync_limit = False
|
|
else:
|
|
sync_limit = True
|
|
|
|
self.settings_screen.ids.settings_lxmf_sync_limit.active = sync_limit
|
|
self.settings_screen.ids.settings_lxmf_sync_limit.bind(active=save_lxmf_sync_limit)
|
|
|
|
self.settings_screen.ids.settings_lxm_limit_1mb.active = self.sideband.config["lxm_limit_1mb"]
|
|
self.settings_screen.ids.settings_lxm_limit_1mb.bind(active=save_lxm_limit_1mb)
|
|
|
|
self.settings_screen.ids.settings_hq_ptt.active = self.sideband.config["hq_ptt"]
|
|
self.settings_screen.ids.settings_hq_ptt.bind(active=save_hq_ptt)
|
|
|
|
self.settings_screen.ids.settings_voice_enabled.active = self.sideband.config["voice_enabled"]
|
|
self.settings_screen.ids.settings_voice_enabled.bind(active=save_voice_enabled)
|
|
|
|
self.settings_screen.ids.settings_debug.active = self.sideband.config["debug"]
|
|
self.settings_screen.ids.settings_debug.bind(active=save_debug)
|
|
|
|
self.settings_screen.ids.settings_block_predictive_text.active = self.sideband.config["block_predictive_text"]
|
|
self.settings_screen.ids.settings_block_predictive_text.bind(active=save_block_predictive_text)
|
|
|
|
self.settings_screen.ids.settings_lang_default.active = False
|
|
self.settings_screen.ids.settings_lang_chinese.active = False
|
|
self.settings_screen.ids.settings_lang_japanese.active = False
|
|
self.settings_screen.ids.settings_lang_korean.active = False
|
|
self.settings_screen.ids.settings_lang_devangari.active = False
|
|
self.settings_screen.ids.settings_lang_default.bind(active=save_input_lang)
|
|
self.settings_screen.ids.settings_lang_chinese.bind(active=save_input_lang)
|
|
self.settings_screen.ids.settings_lang_japanese.bind(active=save_input_lang)
|
|
self.settings_screen.ids.settings_lang_korean.bind(active=save_input_lang)
|
|
self.settings_screen.ids.settings_lang_devangari.bind(active=save_input_lang)
|
|
self.settings_screen.ids.settings_lang_hebrew.bind(active=save_input_lang)
|
|
input_lang = self.sideband.config["input_language"]
|
|
if input_lang == None:
|
|
self.settings_screen.ids.settings_lang_default.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = ""
|
|
elif input_lang == "chinese":
|
|
self.settings_screen.ids.settings_lang_chinese.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = "chinese"
|
|
elif input_lang == "japanese":
|
|
self.settings_screen.ids.settings_lang_japanese.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = "japanese"
|
|
elif input_lang == "korean":
|
|
self.settings_screen.ids.settings_lang_korean.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = "korean"
|
|
elif input_lang == "combined":
|
|
self.settings_screen.ids.settings_lang_devangari.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = "combined"
|
|
elif input_lang == "hebrew":
|
|
self.settings_screen.ids.settings_lang_hebrew.active = True
|
|
self.settings_screen.ids.settings_display_name.font_name = "hebrew"
|
|
else:
|
|
self.settings_screen.ids.settings_display_name.font_name = ""
|
|
|
|
self.settings_ready = True
|
|
|
|
def close_settings_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
|
|
### Connectivity screen
|
|
######################################
|
|
def connectivity_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = "left"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.connectivity_init()
|
|
self.root.ids.screen_manager.current = "connectivity_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def connectivity_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("connectivity_screen"):
|
|
self.connectivity_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.connectivity_init()
|
|
def o(dt):
|
|
self.connectivity_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def connectivity_init(self, sender=None):
|
|
if not self.connectivity_ready:
|
|
if not self.root.ids.screen_manager.has_screen("connectivity_screen"):
|
|
self.connectivity_screen = Builder.load_string(layout_connectivity_screen)
|
|
self.connectivity_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.connectivity_screen)
|
|
self.bind_clipboard_actions(self.connectivity_screen.ids)
|
|
|
|
self.connectivity_screen.ids.connectivity_scrollview.effect_cls = ScrollEffect
|
|
def con_hide_settings():
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_local)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_local_groupid)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_local_ifac_netname)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_local_ifac_passphrase)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_tcp)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_host)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_port)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_ifac_netname)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_ifac_passphrase)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_i2p)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_b32)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_ifac_netname)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_ifac_passphrase)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_local_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_rnode_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_rnode_ifac_netname)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_rnode_ifac_passphrase)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_rnode)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_modem_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_modem)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_modem_fields)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_transport_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_enable_transport)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_serial_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_serial)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_use_weave)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_serial_fields)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_share_instance)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_shared_access)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_shared_access_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_shared_access_fields)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_transport_label)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_enable_transport)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_transport_info)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_transport_fields)
|
|
self.widget_hide(self.connectivity_screen.ids.connectivity_service_restart_fields)
|
|
|
|
def con_collapse_local(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_local_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_tcp(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_tcp_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_i2p(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_i2p_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_bluetooth(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_bluetooth_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_rnode(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_rnode_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_modem(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_modem_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_serial(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_serial_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_weave(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_serial_fields, collapse)
|
|
pass
|
|
|
|
def con_collapse_transport(collapse=True):
|
|
# self.widget_hide(self.connectivity_screen.ids.connectivity_transport_fields, collapse)
|
|
pass
|
|
|
|
def save_connectivity(sender=None, event=None):
|
|
self.sideband.config["connect_transport"] = self.connectivity_screen.ids.connectivity_enable_transport.active
|
|
self.sideband.config["connect_share_instance"] = self.connectivity_screen.ids.connectivity_share_instance.active
|
|
self.sideband.config["connect_local"] = self.connectivity_screen.ids.connectivity_use_local.active
|
|
self.sideband.config["connect_local_groupid"] = self.connectivity_screen.ids.connectivity_local_groupid.text
|
|
self.sideband.config["connect_local_ifac_netname"] = self.connectivity_screen.ids.connectivity_local_ifac_netname.text
|
|
self.sideband.config["connect_local_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_local_ifac_passphrase.text
|
|
self.sideband.config["connect_tcp"] = self.connectivity_screen.ids.connectivity_use_tcp.active
|
|
self.sideband.config["connect_tcp_host"] = self.connectivity_screen.ids.connectivity_tcp_host.text
|
|
self.sideband.config["connect_tcp_port"] = self.connectivity_screen.ids.connectivity_tcp_port.text
|
|
self.sideband.config["connect_tcp_ifac_netname"] = self.connectivity_screen.ids.connectivity_tcp_ifac_netname.text
|
|
self.sideband.config["connect_tcp_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_tcp_ifac_passphrase.text
|
|
self.sideband.config["connect_i2p"] = self.connectivity_screen.ids.connectivity_use_i2p.active
|
|
self.sideband.config["connect_i2p_b32"] = self.connectivity_screen.ids.connectivity_i2p_b32.text
|
|
self.sideband.config["connect_i2p_ifac_netname"] = self.connectivity_screen.ids.connectivity_i2p_ifac_netname.text
|
|
self.sideband.config["connect_i2p_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_i2p_ifac_passphrase.text
|
|
self.sideband.config["connect_rnode"] = self.connectivity_screen.ids.connectivity_use_rnode.active
|
|
self.sideband.config["connect_rnode_ifac_netname"] = self.connectivity_screen.ids.connectivity_rnode_ifac_netname.text
|
|
self.sideband.config["connect_rnode_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_rnode_ifac_passphrase.text
|
|
|
|
self.sideband.config["connect_serial"] = self.connectivity_screen.ids.connectivity_use_serial.active
|
|
self.sideband.config["connect_serial_ifac_netname"] = self.connectivity_screen.ids.connectivity_serial_ifac_netname.text
|
|
self.sideband.config["connect_serial_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_serial_ifac_passphrase.text
|
|
|
|
self.sideband.config["connect_weave"] = self.connectivity_screen.ids.connectivity_use_weave.active
|
|
self.sideband.config["connect_weave_ifac_netname"] = self.connectivity_screen.ids.connectivity_weave_ifac_netname.text
|
|
self.sideband.config["connect_weave_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_weave_ifac_passphrase.text
|
|
|
|
self.sideband.config["connect_modem"] = self.connectivity_screen.ids.connectivity_use_modem.active
|
|
self.sideband.config["connect_modem_ifac_netname"] = self.connectivity_screen.ids.connectivity_modem_ifac_netname.text
|
|
self.sideband.config["connect_modem_ifac_passphrase"] = self.connectivity_screen.ids.connectivity_modem_ifac_passphrase.text
|
|
|
|
self.sideband.config["connect_ifmode_local"] = self.connectivity_screen.ids.connectivity_local_ifmode.text.lower()
|
|
self.sideband.config["connect_ifmode_tcp"] = self.connectivity_screen.ids.connectivity_tcp_ifmode.text.lower()
|
|
self.sideband.config["connect_ifmode_i2p"] = self.connectivity_screen.ids.connectivity_i2p_ifmode.text.lower()
|
|
self.sideband.config["connect_ifmode_rnode"] = self.connectivity_screen.ids.connectivity_rnode_ifmode.text.lower()
|
|
self.sideband.config["connect_ifmode_modem"] = self.connectivity_screen.ids.connectivity_modem_ifmode.text.lower()
|
|
self.sideband.config["connect_ifmode_serial"] = self.connectivity_screen.ids.connectivity_serial_ifmode.text.lower()
|
|
|
|
con_collapse_local(collapse=not self.connectivity_screen.ids.connectivity_use_local.active)
|
|
con_collapse_tcp(collapse=not self.connectivity_screen.ids.connectivity_use_tcp.active)
|
|
con_collapse_i2p(collapse=not self.connectivity_screen.ids.connectivity_use_i2p.active)
|
|
con_collapse_rnode(collapse=not self.connectivity_screen.ids.connectivity_use_rnode.active)
|
|
con_collapse_modem(collapse=not self.connectivity_screen.ids.connectivity_use_modem.active)
|
|
con_collapse_serial(collapse=not self.connectivity_screen.ids.connectivity_use_serial.active)
|
|
con_collapse_weave(collapse=not self.connectivity_screen.ids.connectivity_use_weave.active)
|
|
con_collapse_transport(collapse=not self.sideband.config["connect_transport"])
|
|
|
|
self.sideband.save_configuration()
|
|
|
|
if sender == self.connectivity_screen.ids.connectivity_enable_transport:
|
|
if sender.active:
|
|
def cb(dt):
|
|
yes_button = MDRectangleFlatButton(text="Understood",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject)
|
|
dialog = MDDialog(
|
|
title="Warning!",
|
|
text="You have enabled [i]Reticulum Transport[/i] for this device.\n\nFor normal operation, and for most users, this is [b]not[/b] necessary, and might even degrade your network performance.\n\nWhen Transport is enabled, your device will route traffic between all connected interfaces and for all reachable devices on the network.\n\nThis should only be done if you intend to keep your device in a fixed position and for it to remain available continously.\n\nIf this is not the case, or you don't understand any of this, turn off Transport.",
|
|
buttons=[ yes_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
yes_button.bind(on_release=dl_yes)
|
|
dialog.open()
|
|
Clock.schedule_once(cb, 0.65)
|
|
|
|
def serial_connectivity_save(sender=None, event=None):
|
|
if sender.active:
|
|
self.connectivity_screen.ids.connectivity_use_rnode.unbind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_modem.unbind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_serial.unbind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_weave.unbind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_rnode.active = False
|
|
self.connectivity_screen.ids.connectivity_use_modem.active = False
|
|
self.connectivity_screen.ids.connectivity_use_serial.active = False
|
|
self.connectivity_screen.ids.connectivity_use_weave.active = False
|
|
sender.active = True
|
|
self.connectivity_screen.ids.connectivity_use_rnode.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_modem.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_serial.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_use_weave.bind(active=serial_connectivity_save)
|
|
save_connectivity(sender, event)
|
|
|
|
def focus_save(sender=None, event=None):
|
|
if not sender.focus:
|
|
save_connectivity(sender, event)
|
|
|
|
def ifmode_validate(sender=None, event=None):
|
|
if not sender.focus:
|
|
all_valid = True
|
|
iftypes = ["local", "tcp", "i2p", "rnode", "modem", "serial"]
|
|
for iftype in iftypes:
|
|
element = self.connectivity_screen.ids["connectivity_"+iftype+"_ifmode"]
|
|
modes = ["full", "gateway", "access point", "roaming", "boundary"]
|
|
value = element.text.lower()
|
|
if value in ["", "f"] or value.startswith("fu"):
|
|
value = "full"
|
|
elif value in ["g", "gw"] or value.startswith("ga"):
|
|
value = "gateway"
|
|
elif value in ["a", "ap", "a p", "accesspoint", "access point", "ac", "acc", "acce", "acces"] or value.startswith("access"):
|
|
value = "access point"
|
|
elif value in ["r"] or value.startswith("ro"):
|
|
value = "roaming"
|
|
elif value in ["b", "edge"] or value.startswith("bo"):
|
|
value = "boundary"
|
|
else:
|
|
value = "full"
|
|
|
|
if value in modes:
|
|
element.text = value.capitalize()
|
|
element.error = False
|
|
else:
|
|
element.error = True
|
|
all_valid = False
|
|
|
|
if all_valid:
|
|
save_connectivity(sender, event)
|
|
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
if not self.sideband.getpersistent("service.is_controlling_connectivity"):
|
|
info = "Sideband is connected via a shared Reticulum instance running on this system.\n\n"
|
|
info += "To configure connectivity, edit the relevant configuration file for the instance."
|
|
self.connectivity_screen.ids.connectivity_info.text = info
|
|
con_hide_settings()
|
|
|
|
else:
|
|
info = "By default, Sideband will try to discover and connect to any available Reticulum networks via active WiFi and/or Ethernet interfaces. If any Reticulum Transport Instances are found, Sideband will use these to connect to wider Reticulum networks. You can disable this behaviour if you don't want it.\n\n"
|
|
info += "You can also connect to a network via a remote or local Reticulum instance using TCP or I2P. [b]Please Note![/b] Connecting via I2P requires that you already have I2P running on your device, and that the SAM API is enabled.\n\n"
|
|
info += "For changes to connectivity to take effect, you must either restart the RNS service, or completely shut down and restart Sideband.\n"
|
|
self.connectivity_screen.ids.connectivity_info.text = info
|
|
|
|
self.connectivity_screen.ids.connectivity_use_local.active = self.sideband.config["connect_local"]
|
|
con_collapse_local(collapse=not self.connectivity_screen.ids.connectivity_use_local.active)
|
|
self.connectivity_screen.ids.connectivity_local_groupid.text = self.sideband.config["connect_local_groupid"]
|
|
self.connectivity_screen.ids.connectivity_local_ifac_netname.text = self.sideband.config["connect_local_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_local_ifac_passphrase.text = self.sideband.config["connect_local_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_tcp.active = self.sideband.config["connect_tcp"]
|
|
con_collapse_tcp(collapse=not self.connectivity_screen.ids.connectivity_use_tcp.active)
|
|
self.connectivity_screen.ids.connectivity_tcp_host.text = self.sideband.config["connect_tcp_host"]
|
|
self.connectivity_screen.ids.connectivity_tcp_port.text = self.sideband.config["connect_tcp_port"]
|
|
self.connectivity_screen.ids.connectivity_tcp_ifac_netname.text = self.sideband.config["connect_tcp_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_tcp_ifac_passphrase.text = self.sideband.config["connect_tcp_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_i2p.active = self.sideband.config["connect_i2p"]
|
|
con_collapse_i2p(collapse=not self.connectivity_screen.ids.connectivity_use_i2p.active)
|
|
self.connectivity_screen.ids.connectivity_i2p_b32.text = self.sideband.config["connect_i2p_b32"]
|
|
self.connectivity_screen.ids.connectivity_i2p_ifac_netname.text = self.sideband.config["connect_i2p_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_i2p_ifac_passphrase.text = self.sideband.config["connect_i2p_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_rnode.active = self.sideband.config["connect_rnode"]
|
|
con_collapse_rnode(collapse=not self.connectivity_screen.ids.connectivity_use_rnode.active)
|
|
self.connectivity_screen.ids.connectivity_rnode_ifac_netname.text = self.sideband.config["connect_rnode_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_rnode_ifac_passphrase.text = self.sideband.config["connect_rnode_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_modem.active = self.sideband.config["connect_modem"]
|
|
con_collapse_modem(collapse=not self.connectivity_screen.ids.connectivity_use_modem.active)
|
|
self.connectivity_screen.ids.connectivity_modem_ifac_netname.text = self.sideband.config["connect_modem_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_modem_ifac_passphrase.text = self.sideband.config["connect_modem_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_serial.active = self.sideband.config["connect_serial"]
|
|
con_collapse_serial(collapse=not self.connectivity_screen.ids.connectivity_use_serial.active)
|
|
self.connectivity_screen.ids.connectivity_serial_ifac_netname.text = self.sideband.config["connect_serial_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_serial_ifac_passphrase.text = self.sideband.config["connect_serial_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_use_weave.active = self.sideband.config["connect_weave"]
|
|
con_collapse_weave(collapse=not self.connectivity_screen.ids.connectivity_use_weave.active)
|
|
self.connectivity_screen.ids.connectivity_weave_ifac_netname.text = self.sideband.config["connect_weave_ifac_netname"]
|
|
self.connectivity_screen.ids.connectivity_weave_ifac_passphrase.text = self.sideband.config["connect_weave_ifac_passphrase"]
|
|
|
|
self.connectivity_screen.ids.connectivity_enable_transport.active = self.sideband.config["connect_transport"]
|
|
con_collapse_transport(collapse=not self.sideband.config["connect_transport"])
|
|
self.connectivity_screen.ids.connectivity_enable_transport.bind(active=save_connectivity)
|
|
|
|
self.connectivity_screen.ids.connectivity_share_instance.active = self.sideband.config["connect_share_instance"]
|
|
self.connectivity_screen.ids.connectivity_share_instance.bind(active=save_connectivity)
|
|
|
|
self.connectivity_screen.ids.connectivity_local_ifmode.text = self.sideband.config["connect_ifmode_local"].capitalize()
|
|
self.connectivity_screen.ids.connectivity_tcp_ifmode.text = self.sideband.config["connect_ifmode_tcp"].capitalize()
|
|
self.connectivity_screen.ids.connectivity_i2p_ifmode.text = self.sideband.config["connect_ifmode_i2p"].capitalize()
|
|
self.connectivity_screen.ids.connectivity_rnode_ifmode.text = self.sideband.config["connect_ifmode_rnode"].capitalize()
|
|
self.connectivity_screen.ids.connectivity_modem_ifmode.text = self.sideband.config["connect_ifmode_modem"].capitalize()
|
|
self.connectivity_screen.ids.connectivity_serial_ifmode.text = self.sideband.config["connect_ifmode_serial"].capitalize()
|
|
|
|
self.connectivity_screen.ids.connectivity_use_local.bind(active=save_connectivity)
|
|
self.connectivity_screen.ids.connectivity_local_groupid.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_local_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_local_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_tcp.bind(active=save_connectivity)
|
|
self.connectivity_screen.ids.connectivity_tcp_host.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_tcp_port.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_tcp_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_tcp_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_i2p.bind(active=save_connectivity)
|
|
self.connectivity_screen.ids.connectivity_i2p_b32.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_i2p_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_i2p_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_rnode.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_rnode_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_rnode_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_modem.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_modem_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_modem_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_serial.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_serial_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_serial_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_use_weave.bind(active=serial_connectivity_save)
|
|
self.connectivity_screen.ids.connectivity_weave_ifac_netname.bind(focus=focus_save)
|
|
self.connectivity_screen.ids.connectivity_weave_ifac_passphrase.bind(focus=focus_save)
|
|
|
|
self.connectivity_screen.ids.connectivity_local_ifmode.bind(focus=ifmode_validate)
|
|
self.connectivity_screen.ids.connectivity_tcp_ifmode.bind(focus=ifmode_validate)
|
|
self.connectivity_screen.ids.connectivity_i2p_ifmode.bind(focus=ifmode_validate)
|
|
self.connectivity_screen.ids.connectivity_rnode_ifmode.bind(focus=ifmode_validate)
|
|
self.connectivity_screen.ids.connectivity_modem_ifmode.bind(focus=ifmode_validate)
|
|
self.connectivity_screen.ids.connectivity_serial_ifmode.bind(focus=ifmode_validate)
|
|
|
|
else:
|
|
info = ""
|
|
|
|
if self.sideband.reticulum.is_connected_to_shared_instance:
|
|
info = "Sideband is connected via a shared Reticulum instance running on this system.\n\n"
|
|
info += "To get connectivity status, use the [b]rnstatus[/b] utility.\n\n"
|
|
info += "To configure connectivity, edit the configuration file located at:\n\n"
|
|
if not RNS.vendor.platformutils.is_windows(): info += str(RNS.Reticulum.configpath)
|
|
else: info += str(RNS.Reticulum.configpath.replace("/", "\\"))
|
|
else:
|
|
info = "Sideband is currently running a standalone or master Reticulum instance on this system.\n\n"
|
|
info += "To get connectivity status, use the [b]rnstatus[/b] utility.\n\n"
|
|
info += "To configure connectivity, edit the configuration file located at:\n\n"
|
|
if not RNS.vendor.platformutils.is_windows(): info += str(RNS.Reticulum.configpath)
|
|
else: info += str(RNS.Reticulum.configpath.replace("/", "\\"))
|
|
|
|
self.connectivity_screen.ids.connectivity_info.text = info
|
|
|
|
con_hide_settings()
|
|
|
|
self.connectivity_ready = True
|
|
|
|
def close_connectivity_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
def rpc_copy_action(self, sender=None):
|
|
c_yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject)
|
|
c_no_button = MDRectangleFlatButton(text="No, go back",font_size=dp(18))
|
|
c_dialog = MDDialog(text="[b]Caution![/b]\n\nA configuration line containing your Reticulum RPC key will be copied to the system clipboard.\n\nWhile the key can only be used by other programs running locally on this system, it is still recommended to take care in not exposing it to unwanted programs.\n\nAre you sure that you wish to proceed?", buttons=[ c_no_button, c_yes_button ])
|
|
def c_dl_no(s):
|
|
c_dialog.dismiss()
|
|
def c_dl_yes(s):
|
|
c_dialog.dismiss()
|
|
yes_button = MDRectangleFlatButton(text="OK")
|
|
dialog = MDDialog(text="The RPC configuration was copied to the system clipboard. Paste in into the [b][reticulum][/b] section of the relevant Reticulum configuration file to allow access to this instance.", buttons=[ yes_button ])
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
yes_button.bind(on_release=dl_yes)
|
|
|
|
rpc_string = "shared_instance_type = tcp\n"
|
|
rpc_string += "rpc_key = "+RNS.hexrep(self.sideband.reticulum.rpc_key, delimit=False)
|
|
Clipboard.copy(rpc_string)
|
|
dialog.open()
|
|
|
|
c_yes_button.bind(on_release=c_dl_yes)
|
|
c_no_button.bind(on_release=c_dl_no)
|
|
|
|
c_dialog.open()
|
|
|
|
### Repository screen
|
|
######################################
|
|
def repository_action(self, sender=None, direction="left"):
|
|
if self.repository_ready:
|
|
self.repository_update_info()
|
|
self.repository_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.repository_init()
|
|
def o(dt):
|
|
self.repository_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def repository_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.repository_screen.ids.repository_update.text = ""
|
|
self.root.ids.screen_manager.current = "repository_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def repository_link_action(self, sender=None, event=None):
|
|
if self.repository_url != None:
|
|
def lj():
|
|
webbrowser.open(self.repository_url)
|
|
threading.Thread(target=lj, daemon=True).start()
|
|
|
|
def repository_update_info(self, sender=None):
|
|
info = "Sideband includes a small repository of useful software and guides related to the Sideband and Reticulum ecosystem. You can start this repository to allow other people on your local network to download software and information directly from this device, without needing an Internet connection.\n\n"
|
|
info += "If you want to share the Sideband application itself via the repository server, you must first download it into the local repository, using the \"Update Content\" button below.\n\n"
|
|
info += "To make the repository available on your local network, simply start it below, and it will become browsable on a local IP address for anyone connected to the same WiFi or wired network.\n\n"
|
|
if self.sideband.webshare_server != None:
|
|
def getIP():
|
|
adrs = []
|
|
if RNS.vendor.platformutils.is_android():
|
|
try:
|
|
from jnius import autoclass
|
|
import ipaddress
|
|
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
|
SystemProperties = autoclass('android.os.SystemProperties')
|
|
Context = autoclass('android.content.Context')
|
|
connectivity_manager = mActivity.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
ns = connectivity_manager.getAllNetworks()
|
|
if not ns == None and len(ns) > 0:
|
|
for n in ns:
|
|
lps = connectivity_manager.getLinkProperties(n)
|
|
las = lps.getLinkAddresses()
|
|
for la in las:
|
|
ina = la.getAddress()
|
|
ha = ina.getHostAddress()
|
|
if not ina.isLinkLocalAddress():
|
|
adrs.append(ha)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while getting repository IP address: "+str(e), RNS.LOG_ERROR)
|
|
return None
|
|
|
|
else:
|
|
import socket
|
|
adrs.append(socket.gethostbyname(socket.gethostname()))
|
|
|
|
return adrs
|
|
|
|
ips = getIP()
|
|
if ips == None or len(ips) == 0:
|
|
info += "The repository server is running, but the local device IP address could not be determined.\n\nYou can access the repository by pointing a browser to: https://DEVICE_IP:4444/"
|
|
self.repository_url = None
|
|
else:
|
|
ipstr = ""
|
|
self.repository_url = None
|
|
for ip in ips:
|
|
ipurl = "https://" + str(ip) + ":4444/"
|
|
ipstr += "[u][ref=link]"+ipurl+"[/ref][u]\n"
|
|
if self.repository_url == None:
|
|
self.repository_url = ipurl
|
|
self.rnode_flasher_url = ipurl+"mirrors/rnode-flasher/RNode_Flasher.html"
|
|
|
|
ms = "" if len(ips) == 1 else "es"
|
|
info += "The repository server is running at the following address" + ms +":\n\n"+ipstr
|
|
self.repository_screen.ids.repository_info.bind(on_ref_press=self.repository_link_action)
|
|
|
|
def cb(dt):
|
|
self.repository_screen.ids.repository_enable_button.disabled = True
|
|
self.repository_screen.ids.repository_disable_button.disabled = False
|
|
if hasattr(self, "wants_flasher_launch") and self.wants_flasher_launch == True:
|
|
self.wants_flasher_launch = False
|
|
if self.rnode_flasher_url != None:
|
|
def lj():
|
|
webbrowser.open(self.rnode_flasher_url)
|
|
threading.Thread(target=lj, daemon=True).start()
|
|
|
|
Clock.schedule_once(cb, 0.1)
|
|
|
|
else:
|
|
self.repository_screen.ids.repository_enable_button.disabled = False
|
|
self.repository_screen.ids.repository_disable_button.disabled = True
|
|
|
|
info += ""
|
|
self.repository_screen.ids.repository_info.text = info
|
|
|
|
def repository_start_action(self, sender=None):
|
|
self.sideband.start_webshare()
|
|
Clock.schedule_once(self.repository_update_info, 1.0)
|
|
|
|
def repository_stop_action(self, sender=None):
|
|
self.repository_url = None
|
|
self.sideband.stop_webshare()
|
|
Clock.schedule_once(self.repository_update_info, 0.75)
|
|
|
|
def repository_download_action(self, sender=None):
|
|
def update_job(sender=None):
|
|
try:
|
|
import requests
|
|
### RNode Firmwares ###########
|
|
if True:
|
|
downloads = []
|
|
try:
|
|
release_url = "https://api.github.com/repos/markqvist/rnode_firmware/releases"
|
|
with requests.get(release_url) as response:
|
|
releases = response.json()
|
|
release = releases[0]
|
|
assets = release["assets"]
|
|
for asset in assets:
|
|
if asset["name"].lower().startswith("rnode_firmware"):
|
|
fw_url = asset["browser_download_url"]
|
|
pkgname = asset["name"]
|
|
fw_version = release["tag_name"]
|
|
RNS.log(f"Found version {fw_version} artefact {pkgname} at {fw_url}", RNS.LOG_DEBUG)
|
|
downloads.append([fw_url, pkgname, fw_version])
|
|
|
|
except Exception as e:
|
|
self.repository_screen.ids.repository_update.text = f"Downloading RNode firmware release info failed with the error:\n"+str(e)
|
|
return
|
|
|
|
try:
|
|
for download in downloads:
|
|
fw_url = download[0]
|
|
pkgname = download[1]
|
|
self.repository_screen.ids.repository_update.text = "Downloading: "+str(pkgname)
|
|
with requests.get(fw_url, stream=True) as response:
|
|
with open("./dl_tmp", "wb") as tmp_file:
|
|
cs = 32*1024
|
|
tds = 0
|
|
for chunk in response.iter_content(chunk_size=cs):
|
|
tmp_file.write(chunk)
|
|
tds += cs
|
|
self.repository_screen.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname)
|
|
|
|
os.rename("./dl_tmp", f"{self.sideband.webshare_dir}/pkg/{pkgname}")
|
|
self.repository_screen.ids.repository_update.text = f"Added {pkgname} to the repository!"
|
|
|
|
except Exception as e:
|
|
self.repository_screen.ids.repository_update.text = f"Downloading RNode firmware failed with the error:\n"+str(e)
|
|
return
|
|
|
|
### Sideband APK File #########
|
|
if True:
|
|
# Get release info
|
|
apk_version = None
|
|
apk_url = None
|
|
pkgname = None
|
|
try:
|
|
release_url = "https://api.github.com/repos/markqvist/sideband/releases"
|
|
with requests.get(release_url) as response:
|
|
releases = response.json()
|
|
release = releases[0]
|
|
assets = release["assets"]
|
|
for asset in assets:
|
|
if asset["name"].lower().endswith(".apk"):
|
|
apk_url = asset["browser_download_url"]
|
|
pkgname = asset["name"]
|
|
apk_version = release["tag_name"]
|
|
RNS.log(f"Found version {apk_version} artefact {pkgname} at {apk_url}", RNS.LOG_DEBUG)
|
|
except Exception as e:
|
|
self.repository_screen.ids.repository_update.text = f"Downloading Sideband APK release info failed with the error:\n"+str(e)
|
|
return
|
|
|
|
self.repository_screen.ids.repository_update.text = "Downloading: "+str(pkgname)
|
|
with requests.get(apk_url, stream=True) as response:
|
|
with open("./dl_tmp", "wb") as tmp_file:
|
|
cs = 32*1024
|
|
tds = 0
|
|
for chunk in response.iter_content(chunk_size=cs):
|
|
tmp_file.write(chunk)
|
|
tds += cs
|
|
self.repository_screen.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname)
|
|
|
|
os.rename("./dl_tmp", f"{self.sideband.webshare_dir}/pkg/{pkgname}")
|
|
self.repository_screen.ids.repository_update.text = f"Added {pkgname} to the repository!"
|
|
|
|
self.repository_screen.ids.repository_update.text = f"Repository contents updated successfully!"
|
|
|
|
except Exception as e:
|
|
self.repository_screen.ids.repository_update.text = f"Downloading contents failed with the error:\n"+str(e)
|
|
|
|
self.repository_screen.ids.repository_update.text = "Starting package download..."
|
|
def start_update_job(sender=None):
|
|
threading.Thread(target=update_job, daemon=True).start()
|
|
Clock.schedule_once(start_update_job, 0.5)
|
|
|
|
def repository_init(self, sender=None):
|
|
if not self.repository_ready:
|
|
if not self.root.ids.screen_manager.has_screen("repository_screen"):
|
|
self.repository_screen = Builder.load_string(layout_repository_screen)
|
|
self.repository_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.repository_screen)
|
|
|
|
self.repository_screen.ids.repository_scrollview.effect_cls = ScrollEffect
|
|
self.repository_update_info()
|
|
self.repository_ready = True
|
|
|
|
def close_repository_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
### Hardware screen
|
|
######################################
|
|
def hardware_action(self, sender=None, direction="left"):
|
|
if self.hardware_ready: self.hardware_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.init_hardware_view()
|
|
def o(dt): self.hardware_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def hardware_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition: self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
self.root.ids.screen_manager.current = "hardware_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition: self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def init_hardware_view(self, sender=None):
|
|
if not self.hardware_view: self.hardware_view = Hardware(self)
|
|
|
|
def close_sub_hardware_action(self, sender=None): self.hardware_action(direction="right")
|
|
def close_hardware_action(self, sender=None): self.open_conversations(direction="right")
|
|
|
|
### Announce Stream screen
|
|
######################################
|
|
def init_announces_view(self, sender=None):
|
|
if not self.announces_view:
|
|
self.announces_view = Announces(self)
|
|
self.sideband.setstate("app.flags.new_announces", True)
|
|
for child in self.announces_view.ids.announces_scrollview.children: self.announces_view.ids.announces_scrollview.remove_widget(child)
|
|
self.announces_view.ids.announces_scrollview.effect_cls = ScrollEffect
|
|
self.announces_view.ids.announces_scrollview.add_widget(self.announces_view.get_widget())
|
|
self.announces_view.update()
|
|
|
|
def announces_action(self, sender=None, direction="left"):
|
|
if self.announces_view: self.announces_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.init_announces_view()
|
|
def o(dt): self.announces_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def announces_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition: self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
if self.sideband.getstate("app.flags.new_announces"): self.announces_view.update()
|
|
self.root.ids.screen_manager.current = "announces_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
if no_transition: self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def close_announces_action(self, sender=None): self.open_conversations(direction="right")
|
|
|
|
def announce_filter_action(self, sender=None): pass
|
|
|
|
def screen_transition_complete(self, sender):
|
|
if self.root.ids.screen_manager.current == "announces_screen": pass
|
|
if self.root.ids.screen_manager.current == "conversations_screen": pass
|
|
|
|
### Keys screen
|
|
######################################
|
|
|
|
def keys_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("keys_screen"): self.keys_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.init_keys_view()
|
|
def o(dt): self.keys_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def init_keys_view(self, sender=None):
|
|
if not self.keys_view: self.keys_view = Keys(self)
|
|
|
|
def keys_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition: self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = "left"
|
|
self.root.ids.screen_manager.current = "keys_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
if no_transition: self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def close_keys_action(self, sender=None): self.open_conversations(direction="right")
|
|
|
|
|
|
### Plugins & Services screen
|
|
######################################
|
|
|
|
def plugins_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("plugins_screen"):
|
|
self.plugins_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.plugins_init()
|
|
def o(dt):
|
|
self.plugins_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def plugins_init(self):
|
|
if not self.root.ids.screen_manager.has_screen("plugins_screen"):
|
|
self.plugins_screen = Builder.load_string(layout_plugins_screen)
|
|
self.plugins_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.plugins_screen)
|
|
self.bind_clipboard_actions(self.plugins_screen.ids)
|
|
|
|
self.plugins_screen.ids.plugins_scrollview.effect_cls = ScrollEffect
|
|
info1 = "You can extend Sideband functionality with command and service plugins. This lets you to add your own custom functionality, or add community-developed features.\n"
|
|
info2 = "[b]Take extreme caution![/b]\nIf you add a plugin that you did not write yourself, make [b]absolutely[/b] sure you know what it is doing! Loaded plugins have full access to your Sideband application, and should only be added if you are completely certain they are trustworthy.\n\n"
|
|
info2 += "[i]Command Plugins[/i] allow you to define custom commands that can be carried out in response to LXMF command messages, and they can respond with any kind of information or data to the requestor (or to any LXMF address).\n\n"
|
|
info2 += "By using [i]Service Plugins[/i], you can start additional services or programs within the Sideband application context, that other plugins (or Sideband itself) can interact with.\n\n"
|
|
info2 += "With [i]Telemetry Plugins[/i], you can add custom telemetry from external devices and services to the Sideband telemetry system.\n\n"
|
|
info2 += "Restart Sideband for changes to these settings to take effect."
|
|
self.plugins_screen.ids.plugins_info1.text = info1
|
|
self.plugins_screen.ids.plugins_info2.text = info2
|
|
|
|
self.plugins_screen.ids.settings_command_plugins_enabled.active = self.sideband.config["command_plugins_enabled"]
|
|
self.plugins_screen.ids.settings_service_plugins_enabled.active = self.sideband.config["service_plugins_enabled"]
|
|
|
|
def plugins_settings_save(sender=None, event=None):
|
|
self.sideband.config["command_plugins_enabled"] = self.plugins_screen.ids.settings_command_plugins_enabled.active
|
|
self.sideband.config["service_plugins_enabled"] = self.plugins_screen.ids.settings_service_plugins_enabled.active
|
|
self.sideband.save_configuration()
|
|
|
|
self.plugins_screen.ids.settings_command_plugins_enabled.bind(active=plugins_settings_save)
|
|
self.plugins_screen.ids.settings_service_plugins_enabled.bind(active=plugins_settings_save)
|
|
|
|
def plugins_open(self, sender=None, direction="left", no_transition=False):
|
|
plugins_info_text = self.sideband.get_plugins_info()
|
|
self.plugins_screen.ids.plugins_loaded.text = plugins_info_text
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = "left"
|
|
self.root.ids.screen_manager.current = "plugins_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def close_plugins_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
def plugins_fm_got_path(self, path):
|
|
self.plugins_fm_exited()
|
|
try:
|
|
if os.path.isdir(path):
|
|
self.sideband.config["command_plugins_path"] = path
|
|
self.sideband.save_configuration()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("Using \""+str(path)+"\" as plugin directory")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Directory Set",
|
|
text="Using \""+str(path)+"\" as plugin directory",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while setting plugins directory to \"{path}\": "+str(e), RNS.LOG_ERROR)
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
toast("Could not set plugins directory to \""+str(path)+"\"")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
e_dialog = MDDialog(
|
|
title="Error",
|
|
text="Could not set plugins directory to \""+str(path)+"\"",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=e_dialog.dismiss)
|
|
e_dialog.open()
|
|
|
|
def plugins_fm_exited(self, *args):
|
|
self.manager_open = False
|
|
self.file_manager.close()
|
|
|
|
def plugins_select_directory_action(self, sender=None):
|
|
perm_ok = False
|
|
if self.sideband.config["command_plugins_path"] == None:
|
|
if RNS.vendor.platformutils.is_android():
|
|
perm_ok = self.check_storage_permission()
|
|
path = primary_external_storage_path()
|
|
|
|
else:
|
|
perm_ok = True
|
|
path = os.path.expanduser("~")
|
|
|
|
else:
|
|
perm_ok = True
|
|
path = self.sideband.config["command_plugins_path"]
|
|
|
|
if not os.path.isdir(path):
|
|
if not RNS.vendor.platformutils.is_android(): path = os.path.expanduser("~")
|
|
else: path = primary_external_storage_path()
|
|
|
|
if perm_ok and path != None:
|
|
try:
|
|
self.file_manager = MDFileManager(
|
|
exit_manager=self.plugins_fm_exited,
|
|
select_path=self.plugins_fm_got_path,
|
|
)
|
|
|
|
self.file_manager.show(path)
|
|
|
|
except Exception as e:
|
|
self.sideband.config["command_plugins_path"] = None
|
|
self.sideband.save_configuration()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("Error reading directory, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Error",
|
|
text="Could not read directory, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
else:
|
|
self.sideband.config["command_plugins_path"] = None
|
|
self.sideband.save_configuration()
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("No file access, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Error",
|
|
text="No file access, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
|
|
### Utilities Screen
|
|
######################################
|
|
|
|
def utilities_init(self):
|
|
if not self.utilities_ready:
|
|
self.utilities_screen = Utilities(self)
|
|
self.utilities_ready = True
|
|
|
|
def utilities_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.current = "utilities_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def utilities_action(self, sender=None, direction="left"):
|
|
if self.utilities_ready:
|
|
self.utilities_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.utilities_init()
|
|
def o(dt):
|
|
self.utilities_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def close_sub_utilities_action(self, sender=None):
|
|
self.utilities_action(direction="right")
|
|
|
|
|
|
### voice Screen
|
|
######################################
|
|
|
|
def voice_init(self):
|
|
if not self.voice_ready:
|
|
self.voice_screen = Voice(self)
|
|
self.voice_ready = True
|
|
|
|
def voice_open(self, sender=None, direction="left", no_transition=False, dial_on_complete=None):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.current = "voice_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
self.voice_screen.update_call_status()
|
|
if dial_on_complete:
|
|
self.voice_screen.dial_target = dial_on_complete
|
|
self.voice_screen.screen.ids.identity_hash.text = RNS.hexrep(dial_on_complete, delimit=False)
|
|
Clock.schedule_once(self.voice_screen.dial_action, 0.25)
|
|
|
|
if self.sideband.config["voice_enabled"] == True: self.request_microphone_permission()
|
|
|
|
def voice_action(self, sender=None, direction="left", dial_on_complete=None):
|
|
if self.voice_ready:
|
|
self.voice_open(direction=direction, dial_on_complete=dial_on_complete)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.voice_init()
|
|
def o(dt):
|
|
self.voice_open(no_transition=True, dial_on_complete=dial_on_complete)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def close_sub_voice_action(self, sender=None):
|
|
self.voice_action(direction="right")
|
|
|
|
def dial_action(self, identity_hash):
|
|
self.voice_action(dial_on_complete=identity_hash)
|
|
|
|
def voice_answer_action(self, sender=None):
|
|
if self.sideband.voice_running:
|
|
if self.sideband.telephone.is_ringing:
|
|
self.sideband.telephone.answer()
|
|
toast("Call answered")
|
|
|
|
def voice_reject_action(self, sender=None):
|
|
if self.sideband.voice_running:
|
|
if self.sideband.telephone.is_ringing or self.sideband.telephone.is_in_call:
|
|
self.sideband.telephone.hangup()
|
|
|
|
### Telemetry Screen
|
|
######################################
|
|
|
|
def telemetry_init(self):
|
|
if not self.telemetry_ready:
|
|
self.telemetry_screen = Telemetry(self)
|
|
self.telemetry_ready = True
|
|
|
|
def telemetry_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.current = "telemetry_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def telemetry_action(self, sender=None, direction="left"):
|
|
if self.telemetry_ready:
|
|
self.telemetry_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.telemetry_init()
|
|
def o(dt):
|
|
self.telemetry_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def close_sub_telemetry_action(self, sender=None):
|
|
self.telemetry_action(direction="right")
|
|
|
|
def converse_from_telemetry(self, sender=None):
|
|
if self.object_details_screen != None:
|
|
context_dest = self.object_details_screen.object_hash
|
|
if not self.object_details_screen.object_hash == self.sideband.lxmf_destination.hash:
|
|
if self.sideband.has_conversation(context_dest):
|
|
pass
|
|
else:
|
|
self.sideband.create_conversation(context_dest)
|
|
self.sideband.setstate("app.flags.new_conversations", True)
|
|
|
|
self.open_conversation(context_dest)
|
|
|
|
def telemetry_send_update(self, sender=None):
|
|
if not hasattr(self, "telemetry_info_dialog") or self.telemetry_info_dialog == None:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
self.telemetry_info_dialog = MDDialog(
|
|
title="Info",
|
|
text="",
|
|
buttons=[ ok_button ],
|
|
)
|
|
|
|
def dl_ok(s):
|
|
self.telemetry_info_dialog.dismiss()
|
|
ok_button.bind(on_release=dl_ok)
|
|
|
|
collector_address = self.sideband.config["telemetry_collector"]
|
|
|
|
if self.sideband.config["telemetry_send_all_to_collector"]:
|
|
last_timebase = (self.sideband.getpersistent(f"telemetry.{RNS.hexrep(collector_address, delimit=False)}.last_send_success_timebase") or 0)
|
|
result = self.sideband.create_telemetry_collector_response(to_addr=collector_address, timebase=last_timebase, is_authorized_telemetry_request=True)
|
|
else:
|
|
result = self.sideband.send_latest_telemetry(to_addr=collector_address)
|
|
|
|
if result == "no_address":
|
|
title_str = "Invalid Address"
|
|
info_str = "You must specify a valid LXMF address for the collector you want to sent data to."
|
|
elif result == "destination_unknown":
|
|
title_str = "Unknown Destination"
|
|
info_str = "No keys known for the destination. Connected reticules have been queried for the keys. Try again when an announce for the destination has arrived."
|
|
elif result == "in_progress":
|
|
title_str = "Transfer In Progress"
|
|
info_str = "There is already an outbound telemetry transfer in progress to the collector."
|
|
elif result == "already_sent":
|
|
title_str = "Already Delivered"
|
|
info_str = "The current telemetry data was already sent and delivered to the collector or propagation network."
|
|
elif result == "sent":
|
|
title_str = "Update Sent"
|
|
info_str = "A telemetry update was sent to the collector."
|
|
elif result == "not_sent":
|
|
title_str = "Not Sent"
|
|
info_str = "The telemetry update could not be sent."
|
|
elif result == "nothing_to_send":
|
|
title_str = "Nothing to Send"
|
|
info_str = "There was no new data to send."
|
|
else:
|
|
title_str = "Unknown Status"
|
|
info_str = "The status of the telemetry update is unknown: "+str(result)
|
|
|
|
self.telemetry_info_dialog.title = title_str
|
|
self.telemetry_info_dialog.text = info_str
|
|
self.telemetry_info_dialog.open()
|
|
|
|
def telemetry_request_action(self, sender=None):
|
|
if not hasattr(self, "telemetry_info_dialog") or self.telemetry_info_dialog == None:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
self.telemetry_info_dialog = MDDialog(
|
|
title="Info",
|
|
text="",
|
|
buttons=[ ok_button ],
|
|
)
|
|
|
|
def dl_ok(s):
|
|
self.telemetry_info_dialog.dismiss()
|
|
ok_button.bind(on_release=dl_ok)
|
|
|
|
result = self.sideband.request_latest_telemetry(from_addr=self.sideband.config["telemetry_collector"], is_collector_request=True)
|
|
|
|
if result == "no_address":
|
|
title_str = "Invalid Address"
|
|
info_str = "You must specify a valid LXMF address for the collector you want to request data from."
|
|
elif result == "destination_unknown":
|
|
title_str = "Unknown Destination"
|
|
info_str = "No keys known for the destination. Connected reticules have been queried for the keys. Try again when an announce for the destination has arrived."
|
|
elif result == "in_progress":
|
|
title_str = "Transfer In Progress"
|
|
info_str = "There is already a telemetry request transfer in progress to the collector."
|
|
elif result == "sent":
|
|
title_str = "Request Sent"
|
|
info_str = "A telemetry request was sent to the collector. The collector should send any available telemetry shortly."
|
|
elif result == "not_sent":
|
|
title_str = "Not Sent"
|
|
info_str = "A telemetry request could not be sent."
|
|
else:
|
|
title_str = "Unknown Status"
|
|
info_str = "The status of the telemetry request is unknown: "+str(result)
|
|
|
|
self.telemetry_info_dialog.title = title_str
|
|
self.telemetry_info_dialog.text = info_str
|
|
self.telemetry_info_dialog.open()
|
|
|
|
### Map Screen
|
|
######################################
|
|
|
|
def map_fm_got_path(self, path):
|
|
self.map_fm_exited()
|
|
try:
|
|
source = MBTilesMapSource(path)
|
|
self.map_update_source()
|
|
self.sideband.config["map_storage_file"] = path
|
|
self.sideband.config["map_storage_path"] = str(pathlib.Path(path).parent.resolve())
|
|
self.sideband.save_configuration()
|
|
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("Using \""+os.path.basename(path)+"\" as offline map")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Map Set",
|
|
text="Using \""+os.path.basename(path)+"\" as offline map",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while loading map \"{path}\": "+str(e), RNS.LOG_ERROR)
|
|
if RNS.vendor.platformutils.get_platform() == "android":
|
|
toast("Could not load map \""+os.path.basename(path)+"\"")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
map_dialog = MDDialog(
|
|
title="Map Error",
|
|
text="The specified map file could not be loaded. Make sure the selected file is an MBTiles map in raster format. Vector maps are currently not supported.",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=map_dialog.dismiss)
|
|
map_dialog.open()
|
|
self.sideband.config["map_storage_file"] = None
|
|
self.sideband.config["map_use_offline"] = False
|
|
self.sideband.config["map_use_online"] = True
|
|
self.map_settings_load_states()
|
|
self.map_update_source()
|
|
|
|
def map_fm_exited(self, *args):
|
|
self.manager_open = False
|
|
self.file_manager.close()
|
|
self.map_update_source()
|
|
|
|
def map_select_file_action(self, sender=None):
|
|
perm_ok = False
|
|
if self.sideband.config["map_storage_path"] == None:
|
|
if RNS.vendor.platformutils.is_android():
|
|
perm_ok = self.check_storage_permission()
|
|
|
|
if self.sideband.config["map_storage_external"]:
|
|
path = secondary_external_storage_path()
|
|
if path == None: path = primary_external_storage_path()
|
|
else:
|
|
path = primary_external_storage_path()
|
|
|
|
else:
|
|
perm_ok = True
|
|
if self.sideband.config["map_storage_external"]:
|
|
path = "/"
|
|
else:
|
|
path = os.path.expanduser("~")
|
|
else:
|
|
perm_ok = True
|
|
path = self.sideband.config["map_storage_path"]
|
|
|
|
if perm_ok and path != None:
|
|
try:
|
|
self.file_manager = MDFileManager(
|
|
exit_manager=self.map_fm_exited,
|
|
select_path=self.map_fm_got_path,
|
|
)
|
|
self.file_manager.ext = [".mbtiles"]
|
|
self.file_manager.show(path)
|
|
|
|
except Exception as e:
|
|
self.sideband.config["map_storage_path"] = None
|
|
self.sideband.save_configuration()
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("Error reading directory, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Error",
|
|
text="Could not read directory, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
else:
|
|
self.sideband.config["map_storage_path"] = None
|
|
self.sideband.save_configuration()
|
|
if RNS.vendor.platformutils.is_android():
|
|
toast("No file access, check permissions!")
|
|
else:
|
|
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
|
|
ate_dialog = MDDialog(
|
|
title="Error",
|
|
text="No file access, check permissions!",
|
|
buttons=[ ok_button ],
|
|
)
|
|
ok_button.bind(on_release=ate_dialog.dismiss)
|
|
ate_dialog.open()
|
|
|
|
def map_get_offline_source(self):
|
|
if self.offline_source != None:
|
|
return self.offline_source
|
|
else:
|
|
try:
|
|
current_map_path = self.sideband.config["map_storage_file"]
|
|
if current_map_path == None:
|
|
raise ValueError("Map path cannot be None")
|
|
source = MBTilesMapSource(current_map_path, cache_dir=self.map_cache)
|
|
self.offline_source = source
|
|
return self.offline_source
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while loading map from \"{current_map_path}\": "+str(e))
|
|
self.sideband.config["map_storage_file"] = None
|
|
self.sideband.config["map_use_offline"] = False
|
|
self.sideband.config["map_use_online"] = True
|
|
self.sideband.save_configuration()
|
|
self.map_settings_load_states()
|
|
|
|
return None
|
|
|
|
def map_get_source(self):
|
|
source = None
|
|
if self.sideband.config["map_use_offline"]:
|
|
source = self.map_get_offline_source()
|
|
|
|
if source == None:
|
|
source = MapSource.from_provider("osm", cache_dir=self.map_cache, quad_key=False)
|
|
|
|
return source
|
|
|
|
def map_update_source(self, source=None):
|
|
ns = source or self.map_get_source()
|
|
if self.map != None:
|
|
|
|
if source != None:
|
|
maxz = source.max_zoom
|
|
minz = source.min_zoom
|
|
if self.map.zoom > maxz:
|
|
mz = maxz; px, py = self.map_get_zoom_center(); self.map.set_zoom_at(mz, px, py)
|
|
|
|
if self.map.zoom < minz:
|
|
mz = minz; px, py = self.map_get_zoom_center(); self.map.set_zoom_at(mz, px, py)
|
|
|
|
m = self.map
|
|
nlat = self.map.lat
|
|
nlon = self.map.lon
|
|
if nlat < -89: nlat = -89
|
|
if nlat > 89: nlat = 89
|
|
if nlon < -179: nlon = -179
|
|
if nlon > 179: nlon = 179
|
|
self.map.center_on(nlat,nlon)
|
|
|
|
|
|
self.map.map_source = ns
|
|
|
|
def map_layers_action(self, sender=None):
|
|
try:
|
|
ml = self.map_layer
|
|
layers = []
|
|
if self.sideband.config["map_use_offline"]:
|
|
layers.append("offline")
|
|
|
|
if self.sideband.config["map_use_online"]:
|
|
layers.append("osm")
|
|
layers.append("ve")
|
|
|
|
if ml == None: ml = layers[0]
|
|
|
|
if not ml in layers:
|
|
ml = layers[0]
|
|
|
|
mli = layers.index(ml)
|
|
mli = (mli+1)%len(layers)
|
|
ml = layers[mli]
|
|
|
|
source = None
|
|
if ml == "offline": source = self.map_get_offline_source()
|
|
if ml == "osm": source = MapSource.from_provider("osm", cache_dir=self.map_cache, quad_key=False)
|
|
if ml == "ve": source = MapSource.from_provider("ve", cache_dir=self.map_cache, quad_key=True)
|
|
|
|
if source != None:
|
|
self.map_layer = ml
|
|
self.map_update_source(source)
|
|
except Exception as e:
|
|
RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR)
|
|
|
|
map_nav_divisor = 12
|
|
map_nav_zoom = 0.25
|
|
def map_nav_left(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
bb = self.map.get_bbox()
|
|
lat_span = abs(bb[0] - bb[2])
|
|
lon_span = abs(bb[1] - bb[3])
|
|
span = min(lat_span, lon_span)
|
|
delta = (-span/self.map_nav_divisor)*modifier
|
|
self.map.center_on(self.map.lat, self.map.lon+delta)
|
|
|
|
def map_nav_right(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
bb = self.map.get_bbox()
|
|
lat_span = abs(bb[0] - bb[2])
|
|
lon_span = abs(bb[1] - bb[3])
|
|
span = min(lat_span, lon_span)
|
|
delta = (span/self.map_nav_divisor)*modifier
|
|
self.map.center_on(self.map.lat, self.map.lon+delta)
|
|
|
|
def map_nav_up(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
bb = self.map.get_bbox()
|
|
lat_span = abs(bb[0] - bb[2])
|
|
lon_span = abs(bb[1] - bb[3])
|
|
span = min(lat_span, lon_span)
|
|
delta = (span/self.map_nav_divisor)*modifier
|
|
self.map.center_on(self.map.lat+delta, self.map.lon)
|
|
|
|
def map_nav_down(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
bb = self.map.get_bbox()
|
|
lat_span = abs(bb[0] - bb[2])
|
|
lon_span = abs(bb[1] - bb[3])
|
|
span = min(lat_span, lon_span)
|
|
delta = (-span/self.map_nav_divisor)*modifier
|
|
self.map.center_on(self.map.lat+delta, self.map.lon)
|
|
|
|
def map_get_zoom_center(self):
|
|
bb = self.map.get_bbox()
|
|
slat = (bb[2]-bb[0])/2; slon = (bb[3]-bb[1])/2
|
|
zlat = bb[0]+slat; zlon = bb[1]+slon
|
|
return self.map.get_window_xy_from(zlat, zlon, self.map.zoom)
|
|
|
|
def map_nav_zoom_out(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
zd = -self.map_nav_zoom*modifier
|
|
if self.map.zoom+zd > self.map.map_source.min_zoom:
|
|
px, py = self.map_get_zoom_center()
|
|
self.map.animated_diff_scale_at(zd, px, py)
|
|
|
|
def map_nav_zoom_in(self, sender=None, modifier=1.0):
|
|
if self.map != None:
|
|
zd = self.map_nav_zoom*modifier
|
|
if self.map.zoom+zd < self.map.map_source.max_zoom or self.map.scale < 3.0:
|
|
px, py = self.map_get_zoom_center()
|
|
self.map.animated_diff_scale_at(zd, px, py)
|
|
|
|
def map_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("map_screen"):
|
|
self.map_open(sender=sender, direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.map_init()
|
|
def o(dt):
|
|
self.map_open(sender=sender, no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def map_init(self):
|
|
if not self.root.ids.screen_manager.has_screen("map_screen"):
|
|
msource = self.map_get_source()
|
|
mzoom = self.sideband.config["map_zoom"]
|
|
mlat = self.sideband.config["map_lat"]; mlon = self.sideband.config["map_lon"]
|
|
if mzoom > msource.max_zoom: mzoom = msource.max_zoom
|
|
if mzoom < msource.min_zoom: mzoom = msource.min_zoom
|
|
if mlat < -89: mlat = -89
|
|
if mlat > 89: mlat = 89
|
|
if mlon < -179: mlon = -179
|
|
if mlon > 179: mlon = 179
|
|
|
|
self.map_screen = Builder.load_string(layout_map_screen)
|
|
self.map_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.map_screen)
|
|
|
|
from mapview import MapView
|
|
mapview = MapView(map_source=msource, zoom=mzoom, lat=mlat, lon=mlon)
|
|
mapview.snap_to_zoom = False
|
|
mapview.double_tap_zoom = True
|
|
self.map = mapview
|
|
self.map_screen.ids.map_layout.map = mapview
|
|
self.map_screen.ids.map_layout.add_widget(self.map_screen.ids.map_layout.map)
|
|
|
|
def map_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
self.root.ids.screen_manager.current = "map_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if not hasattr(self, "map_markers") or self.map_markers == None:
|
|
self.map_markers = {}
|
|
|
|
def am_job(dt):
|
|
self.map_update_markers()
|
|
Clock.schedule_once(am_job, 0.15)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def map_settings_load_states(self):
|
|
if self.map_settings_screen != None:
|
|
self.map_settings_screen.ids.map_use_online.active = self.sideband.config["map_use_online"]
|
|
self.map_settings_screen.ids.map_use_offline.active = self.sideband.config["map_use_offline"]
|
|
self.map_settings_screen.ids.map_storage_external.active = self.sideband.config["map_storage_external"]
|
|
|
|
def map_settings_init(self):
|
|
self.map_settings_load_states()
|
|
def map_settings_save(sender=None, event=None):
|
|
self.sideband.config["map_storage_external"] = self.map_settings_screen.ids.map_storage_external.active
|
|
self.sideband.config["map_use_online"] = self.map_settings_screen.ids.map_use_online.active
|
|
self.sideband.config["map_use_offline"] = self.map_settings_screen.ids.map_use_offline.active
|
|
self.sideband.save_configuration()
|
|
|
|
def external_toggle(sender=None, event=None):
|
|
self.sideband.config["map_storage_path"] = None
|
|
map_settings_save()
|
|
|
|
def offline_toggle(sender=None, event=None):
|
|
if self.map_settings_screen.ids.map_use_offline.active:
|
|
# self.map_settings_screen.ids.map_use_online.active = False
|
|
if self.sideband.config["map_storage_file"] == None:
|
|
self.map_select_file_action()
|
|
else:
|
|
self.map_settings_screen.ids.map_use_online.active = True
|
|
map_settings_save(); self.map_update_source()
|
|
|
|
def online_toggle(sender=None, event=None):
|
|
if self.map_settings_screen.ids.map_use_online.active:
|
|
# self.map_settings_screen.ids.map_use_offline.active = False
|
|
pass
|
|
else:
|
|
self.map_settings_screen.ids.map_use_offline.active = True
|
|
map_settings_save(); self.map_update_source()
|
|
|
|
|
|
self.map_settings_screen.ids.map_use_offline.bind(active=offline_toggle)
|
|
self.map_settings_screen.ids.map_use_online.bind(active=online_toggle)
|
|
self.map_settings_screen.ids.map_storage_external.bind(active=external_toggle)
|
|
|
|
def map_settings_action(self, sender=None, direction="left"):
|
|
if not self.root.ids.screen_manager.has_screen("map_settings_screen"):
|
|
self.map_settings_screen = Builder.load_string(layout_map_settings_screen)
|
|
self.map_settings_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.map_settings_screen)
|
|
self.map_settings_screen.ids.map_config_info.text = "\n\nSideband can use map sources from the Internet, or a map source stored locally on this device in MBTiles format."
|
|
self.map_settings_screen.ids.map_settings_scrollview.effect_cls = ScrollEffect
|
|
self.map_settings_init()
|
|
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
self.root.ids.screen_manager.current = "map_settings_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
def update_cache_size(dt):
|
|
size = self.sideband.get_map_cache_size()
|
|
size_str = RNS.prettysize(size)
|
|
self.map_settings_screen.ids.map_cache_button.text = f"Clear {size_str} map cache"
|
|
if size > 0.0:
|
|
self.map_settings_screen.ids.map_cache_button.disabled = False
|
|
else:
|
|
self.map_settings_screen.ids.map_cache_button.disabled = True
|
|
self.map_settings_screen.ids.map_cache_button.text = f"No data in map cache"
|
|
|
|
Clock.schedule_once(update_cache_size, 0.35)
|
|
|
|
def map_clear_cache(self, sender=None):
|
|
yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject)
|
|
no_button = MDRectangleFlatButton(text="No",font_size=dp(18))
|
|
dialog = MDDialog(
|
|
title="Clear map cache?",
|
|
buttons=[ yes_button, no_button ],
|
|
# elevation=0,
|
|
)
|
|
def dl_yes(s):
|
|
dialog.dismiss()
|
|
self.sideband.clear_map_cache()
|
|
|
|
def cb(dt):
|
|
self.map_settings_action()
|
|
self.map_settings_screen.ids.map_cache_button.disabled = True
|
|
Clock.schedule_once(cb, 1.2)
|
|
|
|
def dl_no(s):
|
|
dialog.dismiss()
|
|
|
|
yes_button.bind(on_release=dl_yes)
|
|
no_button.bind(on_release=dl_no)
|
|
dialog.open()
|
|
|
|
def close_location_error_dialog(self, sender=None):
|
|
if hasattr(self, "location_error_dialog") and self.location_error_dialog != None:
|
|
self.location_error_dialog.dismiss()
|
|
|
|
def map_object_list(self, sender):
|
|
pass
|
|
|
|
def map_show(self, location, retry=0):
|
|
max_tries = 6
|
|
if hasattr(self, "map") and self.map:
|
|
mz = 16
|
|
lat = location["latitude"]
|
|
lon = location["longitude"]
|
|
if mz > self.map.map_source.max_zoom: mz = self.map.map_source.max_zoom
|
|
if mz < self.map.map_source.min_zoom: mz = self.map.map_source.min_zoom
|
|
self.map.zoom = mz
|
|
self.map.trigger_update(True)
|
|
self.map.center_on(lat,lon)
|
|
self.map.trigger_update(True)
|
|
else:
|
|
if retry < max_tries:
|
|
def j(dt):
|
|
self.map_show(location, retry=retry+1)
|
|
Clock.schedule_once(j, 0.5)
|
|
|
|
def map_show_peer_location(self, context_dest):
|
|
location = self.sideband.peer_location(context_dest)
|
|
if not location:
|
|
self.location_error_dialog = MDDialog(
|
|
title="No Location",
|
|
text="No recent location updates have been received from this peer. You can use the the [b]Situation Map[/b] to manually search for earlier telemetry.",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.close_location_error_dialog
|
|
)
|
|
],
|
|
)
|
|
self.location_error_dialog.open()
|
|
else:
|
|
self.map_action()
|
|
self.map_show(location)
|
|
|
|
def map_own_location_action(self, context_dest):
|
|
self.sideband.update_telemetry()
|
|
location = self.sideband.peer_location(self.sideband.lxmf_destination.hash)
|
|
if not location:
|
|
self.location_error_dialog = MDDialog(
|
|
title="No Location",
|
|
text="Your location is currently unknown. Make sure the relevant telemetry sensors and permissions have been enabled.",
|
|
buttons=[
|
|
MDRectangleFlatButton(
|
|
text="OK",
|
|
font_size=dp(18),
|
|
on_release=self.close_location_error_dialog
|
|
)
|
|
],
|
|
)
|
|
self.location_error_dialog.open()
|
|
else:
|
|
self.map_action()
|
|
self.map_show(location)
|
|
|
|
def map_display_telemetry(self, sender=None, event=None):
|
|
alt_event = False
|
|
if sender != None:
|
|
if hasattr(sender, "last_touch"):
|
|
if hasattr(sender.last_touch, "button"):
|
|
if sender.last_touch.button == "right":
|
|
alt_event = True
|
|
|
|
if alt_event:
|
|
try:
|
|
if hasattr(sender, "source_dest"):
|
|
self.sideband.request_latest_telemetry(from_addr=sender.source_dest)
|
|
toast("Telemetry request sent")
|
|
except Exception as e:
|
|
RNS.log(f"Could not request telemetry update: {e}", RNS.LOG_ERROR)
|
|
else:
|
|
self.object_details_action(sender)
|
|
|
|
def map_display_own_telemetry(self, sender=None):
|
|
self.sideband.update_telemetry()
|
|
self.object_details_action(source_dest=self.sideband.lxmf_destination.hash,from_telemetry=True)
|
|
|
|
def close_sub_map_action(self, sender=None):
|
|
self.map_action(direction="right")
|
|
|
|
def object_details_action(self, sender=None, from_conv=False, from_objects=False, from_telemetry=False, source_dest=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("object_details_screen"):
|
|
self.object_details_open(sender=sender, from_conv=from_conv, from_objects=from_objects, from_telemetry=from_telemetry, source_dest=source_dest, direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.object_details_init()
|
|
def o(dt):
|
|
self.object_details_open(sender=sender, from_conv=from_conv, from_objects=from_objects, from_telemetry=from_telemetry, source_dest=source_dest, no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def object_details_init(self):
|
|
self.object_details_screen = ObjectDetails(self)
|
|
|
|
def object_details_open(self, sender=None, from_conv=False, from_objects=False, from_telemetry=False, source_dest=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
if self.sideband.config["telemetry_enabled"] == True:
|
|
self.sideband.update_telemetry()
|
|
|
|
if source_dest != None:
|
|
telemetry_source = source_dest
|
|
else:
|
|
if sender != None and hasattr(sender, "source_dest") and sender.source_dest != None:
|
|
telemetry_source = sender.source_dest
|
|
else:
|
|
telemetry_source = None
|
|
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
|
|
if telemetry_source == None:
|
|
if self.include_objects and not self.include_conversations:
|
|
self.objects_action(direction="right")
|
|
else:
|
|
self.conversations_action(direction="right")
|
|
|
|
else:
|
|
Clock.schedule_once(lambda dt: self.object_details_screen.set_source(telemetry_source, from_conv=from_conv, from_objects=from_objects, from_telemetry=from_telemetry), 0.0)
|
|
|
|
def vj(dt):
|
|
self.root.ids.screen_manager.current = "object_details_screen"
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
Clock.schedule_once(vj, 0.15)
|
|
|
|
def map_create_marker(self, source, telemetry, appearance):
|
|
try:
|
|
l = telemetry["location"]
|
|
a_icon = appearance[0]
|
|
a_fg = appearance[1]; a_bg = appearance[2]
|
|
marker = CustomMapMarker(lat=l["latitude"], lon=l["longitude"], icon_bg=a_bg)
|
|
marker.app = self
|
|
marker.source_dest = source
|
|
marker.location_time = l["last_update"]
|
|
marker.icon = MDMapIconButton(
|
|
icon=a_icon, icon_color=a_fg,
|
|
md_bg_color=a_bg, theme_icon_color="Custom",
|
|
icon_size=dp(32),
|
|
on_release=self.map_display_telemetry,
|
|
)
|
|
marker.icon._default_icon_pad = dp(16)
|
|
marker.icon.source_dest = marker.source_dest
|
|
marker.add_widget(marker.icon)
|
|
|
|
########
|
|
# marker.badge = MDMapIconButton(
|
|
# icon="network-strength-2", icon_color=[0,0,0,1],
|
|
# md_bg_color=[1,1,1,1], theme_icon_color="Custom",
|
|
# icon_size=dp(18),
|
|
# )
|
|
# marker.badge._default_icon_pad = dp(5)
|
|
# marker.icon.add_widget(marker.badge)
|
|
########
|
|
|
|
return marker
|
|
|
|
except Exception as e:
|
|
RNS.log("Could not create map marker for "+RNS.prettyhexrep(source)+": "+str(e), RNS.LOG_ERROR)
|
|
return None
|
|
|
|
def map_update_markers(self, sender=None):
|
|
RNS.log("Updating map markers", RNS.LOG_DEBUG)
|
|
earliest = time.time() - self.sideband.config["map_history_limit"]
|
|
telemetry_entries = self.sideband.list_telemetry(after=earliest)
|
|
own_address = self.sideband.lxmf_destination.hash
|
|
changes = False
|
|
|
|
# Add own marker if available
|
|
retain_own = False
|
|
own_telemetry = self.sideband.get_telemetry()
|
|
own_appearance = [
|
|
self.sideband.config["telemetry_icon"],
|
|
self.sideband.config["telemetry_fg"],
|
|
self.sideband.config["telemetry_bg"]
|
|
]
|
|
|
|
skip_entries = []
|
|
if self.sideband.config["telemetry_display_trusted_only"]:
|
|
for telemetry_source in telemetry_entries:
|
|
try:
|
|
if not self.sideband.is_trusted(telemetry_source):
|
|
skip_entries.append(telemetry_source)
|
|
except:
|
|
pass
|
|
for skip_entry in skip_entries:
|
|
try:
|
|
telemetry_entries.pop(skip_entry)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
if own_telemetry != None and "location" in own_telemetry and own_telemetry["location"] != None and own_telemetry["location"]["latitude"] != None and own_telemetry["location"]["longitude"] != None:
|
|
retain_own = True
|
|
|
|
if not own_address in self.map_markers:
|
|
marker = self.map_create_marker(own_address, own_telemetry, own_appearance)
|
|
if marker != None:
|
|
self.map_markers[own_address] = marker
|
|
self.map_screen.ids.map_layout.map.add_marker(marker)
|
|
changes = True
|
|
|
|
else:
|
|
marker = self.map_markers[own_address]
|
|
o = own_telemetry["location"]
|
|
if o["last_update"] > marker.location_time or (hasattr(self, "own_appearance_changed") and self.own_appearance_changed):
|
|
marker.location_time = o["last_update"]
|
|
marker.lat = o["latitude"]
|
|
marker.lon = o["longitude"]
|
|
marker.icon.icon = own_appearance[0]
|
|
marker.icon.icon_color = own_appearance[1]
|
|
marker.icon.md_bg_color = own_appearance[2]
|
|
self.own_appearance_changed = False
|
|
changes = True
|
|
|
|
stale_markers = []
|
|
for marker in self.map_markers:
|
|
if not marker in telemetry_entries:
|
|
if marker == own_address:
|
|
if not retain_own:
|
|
stale_markers.append(marker)
|
|
else:
|
|
stale_markers.append(marker)
|
|
|
|
for marker in stale_markers:
|
|
RNS.log("Removing stale marker: "+str(marker), RNS.LOG_DEBUG)
|
|
try:
|
|
to_remove = self.map_markers[marker]
|
|
self.map_screen.ids.map_layout.map.remove_marker(to_remove)
|
|
self.map_markers.pop(marker)
|
|
changes = True
|
|
except Exception as e:
|
|
RNS.log("Error while removing map marker: "+str(e), RNS.LOG_ERROR)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while updating own map marker: "+str(e), RNS.LOG_ERROR)
|
|
|
|
for telemetry_source in telemetry_entries:
|
|
try:
|
|
skip = False
|
|
if telemetry_source == own_address:
|
|
skip = True
|
|
elif telemetry_source in self.map_markers:
|
|
marker = self.map_markers[telemetry_source]
|
|
newest_timestamp = telemetry_entries[telemetry_source][0][0]
|
|
if newest_timestamp <= marker.location_time:
|
|
skip = True
|
|
|
|
latest_viewable = None
|
|
if not skip:
|
|
for telemetry_entry in sorted(telemetry_entries[telemetry_source], key=lambda t: t[0], reverse=True):
|
|
telemetry_timestamp = telemetry_entry[0]
|
|
telemetry_data = telemetry_entry[1]
|
|
t = Telemeter.from_packed(telemetry_data)
|
|
if t != None:
|
|
telemetry = t.read_all()
|
|
if "location" in telemetry and telemetry["location"] != None and telemetry["location"]["latitude"] != None and telemetry["location"]["longitude"] != None:
|
|
latest_viewable = telemetry
|
|
break
|
|
elif "connection_map" in telemetry:
|
|
# TODO: Telemetry entries with connection map sensor types are skipped for now,
|
|
# until a proper rendering mechanism is implemented
|
|
break
|
|
|
|
if latest_viewable != None:
|
|
l = latest_viewable["location"]
|
|
if not telemetry_source in self.map_markers:
|
|
marker = self.map_create_marker(telemetry_source, latest_viewable, self.sideband.peer_appearance(telemetry_source))
|
|
if marker != None:
|
|
self.map_markers[telemetry_source] = marker
|
|
self.map_screen.ids.map_layout.map.add_marker(marker)
|
|
changes = True
|
|
else:
|
|
marker = self.map_markers[telemetry_source]
|
|
marker.location_time = latest_viewable["time"]["utc"]
|
|
marker.lat = l["latitude"]
|
|
marker.lon = l["longitude"]
|
|
appearance = self.sideband.peer_appearance(telemetry_source)
|
|
marker.icon.icon = appearance[0]
|
|
marker.icon.icon_color = appearance[1]
|
|
marker.icon.md_bg_color = appearance[2]
|
|
changes = True
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while updating map entry for "+RNS.prettyhexrep(telemetry_source)+": "+str(e), RNS.LOG_ERROR)
|
|
|
|
self.last_map_update = time.time()
|
|
if changes:
|
|
self.map.trigger_update(True)
|
|
|
|
### Guide screen
|
|
######################################
|
|
def close_guide_action(self, sender=None):
|
|
self.open_conversations(direction="right")
|
|
|
|
def guide_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
self.root.ids.screen_manager.current = "guide_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def guide_action(self, sender=None, direction="left"):
|
|
if self.guide_view: self.guide_open()
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.init_guide_view()
|
|
def o(dt): self.guide_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def init_guide_view(self, sender=None):
|
|
if not self.guide_view: self.guide_view = Guide(self)
|
|
|
|
|
|
#################################################
|
|
# Unimplemented Screens #
|
|
#################################################
|
|
def broadcasts_action(self, sender=None, direction="left"):
|
|
if self.root.ids.screen_manager.has_screen("broadcasts_screen"):
|
|
self.broadcasts_open(direction=direction)
|
|
else:
|
|
self.loader_action(direction=direction)
|
|
def final(dt):
|
|
self.broadcasts_init()
|
|
def o(dt):
|
|
self.broadcasts_open(no_transition=True)
|
|
Clock.schedule_once(o, ll_ot)
|
|
Clock.schedule_once(final, ll_ft)
|
|
|
|
def broadcasts_open(self, sender=None, direction="left", no_transition=False):
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.no_transition
|
|
else:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
self.root.ids.screen_manager.transition.direction = direction
|
|
|
|
info = "The [b]Local Broadcasts[/b] feature will allow you to send and listen for local broadcast transmissions on all connected interfaces.\n\n[b]Local Broadcasts[/b] makes it easy to establish public information exchange with anyone in direct radio range, or even with large areas far away using the [i]Remote Broadcast Repeater[/i] feature.\n\nThese features are not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support the project."
|
|
if self.theme_cls.theme_style == "Dark":
|
|
info = "[color=#"+dark_theme_text_color+"]"+info+"[/color]"
|
|
self.broadcasts_screen.ids.broadcasts_info.text = info
|
|
|
|
self.root.ids.screen_manager.current = "broadcasts_screen"
|
|
self.root.ids.nav_drawer.set_state("closed")
|
|
self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current)
|
|
|
|
if no_transition:
|
|
self.root.ids.screen_manager.transition = self.slide_transition
|
|
|
|
def broadcasts_init(self):
|
|
if not self.root.ids.screen_manager.has_screen("broadcasts_screen"):
|
|
def link_exec(sender=None, event=None):
|
|
def lj():
|
|
webbrowser.open("https://unsigned.io/donate")
|
|
threading.Thread(target=lj, daemon=True).start()
|
|
|
|
self.broadcasts_screen = Builder.load_string(layout_broadcasts_screen)
|
|
self.broadcasts_screen.app = self
|
|
self.root.ids.screen_manager.add_widget(self.broadcasts_screen)
|
|
|
|
self.broadcasts_screen.ids.broadcasts_scrollview.effect_cls = ScrollEffect
|
|
self.broadcasts_screen.ids.broadcasts_info.bind(on_ref_press=link_exec)
|
|
|
|
class CustomOneLineIconListItem(OneLineIconListItem):
|
|
icon = StringProperty()
|
|
|
|
class DialogItem(OneLineIconListItem):
|
|
divider = None
|
|
icon = StringProperty()
|
|
|
|
class MDMapIconButton(MDIconButton):
|
|
pass
|
|
|
|
class UIScaling(BoxLayout):
|
|
pass
|
|
|
|
if not args.daemon:
|
|
from kivy.base import ExceptionManager, ExceptionHandler
|
|
class SidebandExceptionHandler(ExceptionHandler):
|
|
def handle_exception(self, e):
|
|
etype = type(e)
|
|
if etype != SystemExit:
|
|
import traceback
|
|
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
|
|
RNS.log(f"An unhandled {str(type(e))} exception occurred: {str(e)}", RNS.LOG_ERROR)
|
|
RNS.log(exception_info, RNS.LOG_ERROR)
|
|
return ExceptionManager.PASS
|
|
else:
|
|
return ExceptionManager.RAISE
|
|
|
|
def run():
|
|
if args.daemon:
|
|
RNS.log("Starting Sideband in daemon mode")
|
|
sideband = SidebandCore(
|
|
None,
|
|
config_path=args.config,
|
|
is_client=False,
|
|
verbose=(args.verbose or __debug_build__),
|
|
quiet=(args.interactive and not args.verbose),
|
|
is_daemon=True,
|
|
rns_config_path=args.rnsconfig,
|
|
)
|
|
|
|
sideband.version_str = "v"+__version__+" "+__variant__
|
|
sideband.start()
|
|
|
|
if args.interactive:
|
|
while not sideband.getstate("core.started") == True: time.sleep(0.1)
|
|
import importlib
|
|
if importlib.util.find_spec('prompt_toolkit') != None:
|
|
from .sideband import console
|
|
console.attach(sideband)
|
|
else:
|
|
print("Could not start Sideband console, since the \"prompt-toolkit\" module is not available")
|
|
print("You can install it with \"pip install prompt-toolkit\"")
|
|
|
|
else:
|
|
while True: time.sleep(5)
|
|
else:
|
|
ExceptionManager.add_handler(SidebandExceptionHandler())
|
|
SidebandApp().run()
|
|
|
|
if __name__ == "__main__":
|
|
run()
|
|
|
|
if __name__ == "sbapp.main":
|
|
run()
|