diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index e6b5516..18e8586 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -10,9 +10,9 @@ source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements, version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py -android.numeric_version = 20240920 +android.numeric_version = 20241013 -requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,android,able_recipe +requirements = kivy==2.3.0,libbz2,pillow==10.2.0,qrcode==7.3.1,usb4a,usbserial4a,able_recipe,libwebp,libogg,libopus,opusfile,numpy,cryptography,ffpyplayer,codec2,pycodec2,sh,pynacl,typing-extensions #android.gradle_dependencies = com.android.support:support-compat:28.0.0 #android.enable_androidx = True @@ -29,7 +29,8 @@ android.presplash_color = #00000000 orientation = portrait fullscreen = 0 -android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE,ACCESS_NETWORK_STATE,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,MANAGE_EXTERNAL_STORAGE,ACCESS_BACKGROUND_LOCATION,RECORD_AUDIO +#android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE,ACCESS_NETWORK_STATE,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,MANAGE_EXTERNAL_STORAGE,ACCESS_BACKGROUND_LOCATION,RECORD_AUDIO +android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_SCAN,BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT,ACCESS_NETWORK_STATE,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,MANAGE_EXTERNAL_STORAGE,ACCESS_BACKGROUND_LOCATION,RECORD_AUDIO,REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,FOREGROUND_SERVICE_CONNECTED_DEVICE android.api = 31 android.minapi = 24 diff --git a/sbapp/kivymd/uix/filemanager/filemanager.py b/sbapp/kivymd/uix/filemanager/filemanager.py index 4073764..728c796 100755 --- a/sbapp/kivymd/uix/filemanager/filemanager.py +++ b/sbapp/kivymd/uix/filemanager/filemanager.py @@ -649,7 +649,8 @@ class MDFileManager(MDRelativeLayout): return dirs, files - except OSError: + except OSError as e: + print("Filemanager OSError: "+str(e)) return None, None def close(self) -> None: diff --git a/sbapp/main.py b/sbapp/main.py index 954fd96..24c2b8c 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1,7 +1,7 @@ __debug_build__ = False __disable_shaders__ = False -__version__ = "0.9.7" -__variant__ = "beta" +__version__ = "1.1.3" +__variant__ = "" import sys import argparse @@ -300,6 +300,7 @@ class SidebandApp(MDApp): self.hardware_rnode_ready = False self.hardware_modem_ready = False self.hardware_serial_ready = False + self.hw_error_dialog = None self.final_load_completed = False self.service_last_available = 0 @@ -406,6 +407,20 @@ class SidebandApp(MDApp): 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 @@ -418,22 +433,25 @@ class SidebandApp(MDApp): def check_errors(dt): if self.sideband.getpersistent("startup.errors.rnode") != None: - 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.dismiss() - yes_button.bind(on_release=dl_yes) - self.hw_error_dialog.open() + 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 Clock.schedule_once(check_errors, 1.5) @@ -708,7 +726,18 @@ class SidebandApp(MDApp): if check_permission(bt_permission_name): RNS.log("Have bluetooth connect permissions", RNS.LOG_DEBUG) - self.sideband.setpersistent("permissions.bluetooth", True) + + 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) @@ -799,18 +828,36 @@ class SidebandApp(MDApp): def request_bluetooth_permissions(self): if RNS.vendor.platformutils.get_platform() == "android": - if not check_permission("android.permission.BLUETOOTH_CONNECT"): - RNS.log("Requesting bluetooth permission", RNS.LOG_DEBUG) - request_permissions(["android.permission.BLUETOOTH_CONNECT"]) + 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 on_new_intent(self, intent): - RNS.log("Received intent", RNS.LOG_DEBUG) 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) + + 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) @@ -890,6 +937,29 @@ class SidebandApp(MDApp): 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="OK", + 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 ], + # elevation=0, + ) + def dl_yes(s): + self.hw_error_dialog.dismiss() + self.hw_error_dialog.is_open = False + yes_button.bind(on_release=dl_yes) + self.hw_error_dialog.open() + self.hw_error_dialog.is_open = True + if self.root.ids.screen_manager.current == "messages_screen": self.messages_view.update() @@ -1645,8 +1715,6 @@ class SidebandApp(MDApp): ate_dialog.open() else: - self.sideband.config["map_storage_path"] = None - self.sideband.save_configuration() if RNS.vendor.platformutils.get_platform() == "android": toast("No file access, check permissions!") else: @@ -1779,7 +1847,11 @@ class SidebandApp(MDApp): return self.sideband.ui_started_recording() - self.audio_msg_mode = LXMF.AM_CODEC2_2400 + if self.sideband.config["hq_ptt"]: + self.audio_msg_mode = LXMF.AM_OPUS_OGG + else: + self.audio_msg_mode = LXMF.AM_CODEC2_2400 + self.message_attach_action(attach_type="audio", nodialog=True) if self.rec_dialog == None: self.message_init_rec_dialog() @@ -2719,6 +2791,14 @@ class SidebandApp(MDApp): 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_print_command(sender=None, event=None): if not sender.focus: in_cmd = self.settings_screen.ids.settings_print_command.text @@ -2882,9 +2962,15 @@ class SidebandApp(MDApp): 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_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 @@ -4010,6 +4096,15 @@ class SidebandApp(MDApp): self.sideband.save_configuration() + def hardware_rnode_ble_toggle_action(self, sender=None, event=None): + if sender.active: + self.sideband.config["hw_rnode_ble"] = True + self.request_bluetooth_permissions() + else: + self.sideband.config["hw_rnode_ble"] = False + + self.sideband.save_configuration() + def hardware_rnode_framebuffer_toggle_action(self, sender=None, event=None): if sender.active: self.sideband.config["hw_rnode_enable_framebuffer"] = True @@ -4115,6 +4210,7 @@ class SidebandApp(MDApp): t_btd = "" self.hardware_rnode_screen.ids.hardware_rnode_bluetooth.active = self.sideband.config["hw_rnode_bluetooth"] + self.hardware_rnode_screen.ids.hardware_rnode_ble.active = self.sideband.config["hw_rnode_ble"] self.hardware_rnode_screen.ids.hardware_rnode_framebuffer.active = self.sideband.config["hw_rnode_enable_framebuffer"] self.hardware_rnode_screen.ids.hardware_rnode_advanced_cfg.active = self.sideband.config["hw_rnode_advanced_cfg"] @@ -4181,6 +4277,7 @@ class SidebandApp(MDApp): self.hardware_rnode_screen.ids.hardware_rnode_beaconinterval.bind(on_text_validate=save_connectivity) self.hardware_rnode_screen.ids.hardware_rnode_beacondata.bind(on_text_validate=save_connectivity) self.hardware_rnode_screen.ids.hardware_rnode_bluetooth.bind(active=self.hardware_rnode_bt_toggle_action) + self.hardware_rnode_screen.ids.hardware_rnode_ble.bind(active=self.hardware_rnode_ble_toggle_action) self.hardware_rnode_screen.ids.hardware_rnode_framebuffer.bind(active=self.hardware_rnode_framebuffer_toggle_action) self.hardware_rnode_screen.ids.hardware_rnode_advanced_cfg.bind(active=self.hardware_rnode_advanced_cfg_toggle_action) @@ -6060,37 +6157,35 @@ This short guide will give you a basic introduction to the concepts that underpi This also means that openCom Companion operates differently than what you might be used to. It does not need a connection to a server on the Internet to function, and you do not have an account anywhere.""" guide_text3 = """ -[size=18dp][b]Operating Principles[/b][/size][size=5dp]\n \n[/size]When openCom Companion is started on your device for the first time, it randomly generates a set of cryptographic keys. These keys are then used to create an LXMF address for your use. Any other endpoint in [i]any[/i] Reticulum network will be able to send data to this address, as long as there is [i]some sort of physical connection[/i] between your device and the remote endpoint. You can also move around to other Reticulum networks with this address, even ones that were never connected to the network the address was created on, or that didn't exist when the address was created. The address is yours to keep and control for as long (or short) a time you need it, and you can always delete it and create a new one.""" +[size=18dp][b]Operating Principles[/b][/size][size=5dp]\n \n[/size]When openCom Companion is started on your device for the first time, it randomly generates a 512-bit Reticulum Identity Key. This cryptographic key is then used to create an LXMF address for your use, and in turn to secure any communication to your address. Any other endpoint in [i]any[/i] Reticulum network will be able to send data to your address, as long as there is [i]some sort of physical connection[/i] between your device and the remote endpoint. You can also move around to other Reticulum networks with this address, even ones that were never connected to the network the address was created on, or that didn't exist when the address was created.\n\nYour LXMF address is yours to keep and control for as long (or short) a time you need it, and you can always delete it and create a new one. You identity keys and corresponding addresses are never registered on or controlled by any external servers or services, and will never leave your device, unless you manually export them for backup.""" guide_text4 = """ -[size=18dp][b]Becoming Reachable[/b][/size][size=5dp]\n \n[/size]To establish reachability for any Reticulum address on a network, an [i]announce[/i] must be sent. openCom Companion does not do this automatically by default, but can be configured to do so every time the program starts. To send an announce manually, press the [i]Announce[/i] button in the [i]Conversations[/i] section of the program. When you send an announce, you make your LXMF address reachable for real-time messaging to the entire network you are connected to. Even in very large networks, you can expect global reachability for your address to be established in under a minute. - -If you don't move to other places in the network, and keep connected through the same hubs or gateways, it is generally not necessary to send an announce more often than once every week. If you change your entry point to the network, you may want to send an announce, or you may just want to stay quiet.""" - +[size=18dp][b]Becoming Reachable[/b][/size][size=5dp]\n \n[/size]To establish reachability for any Reticulum destination on a network, an [i]announce[/i] must be sent. By default, openCom Companion will announce automatically when necessary, but if you want to stay silent, automatic announces can be disabled in [b]Preferences[/b].\n\nTo send an announce manually, press the [i]Announce[/i] button in the [i]Conversations[/i] section of the program. When you send an announce, you make your LXMF address reachable for real-time messaging to the entire network you are connected to. Even in very large networks, you can expect global reachability for your address to be established in under a minute.""" + + guide_text10 = """ +[size=18dp][b]Getting Connected[/b][/size][size=5dp]\n \n[/size]If you already have Reticulum connectivity set up on the device you are running Sideband on, no further configuration should be necessary, and Sideband will simply use the available Reticulum connectivity.\n\nIf you are running Sideband on a computer, you can configure interfaces in the Reticulum configuration file ([b]~/.reticulum/config[/b] by default). If you are running Sideband on an Android device, you can configure various interface types in the [b]Connectivity[/b] section. By default, only an [i]AutoInterface[/i] is enabled, which will connect you automatically with any other local devices on the same WiFi and/or Ethernet networks. This may or may not include Reticulum Transport Nodes, which can route your traffic to wider networks.\n\nYou can enable any or all of the other available interface types to gain wider connectivity. For more specific information on interface types, configuration options, and how to effectively build your own Reticulum networks, see the [b]Reticulum Manual[b].""" + guide_text5 = """ [size=18dp][b]Relax & Disconnect[/b][/size][size=5dp]\n \n[/size]If you are not connected to the network, it is still possible for other people to message you, as long as one or more [i]Propagation Nodes[/i] exist on the network. These nodes pick up and hold encrypted in-transit messages for offline users. Messages are always encrypted before leaving the originators device, and nobody else than the intended recipient can decrypt messages in transit. -The Propagation Nodes also distribute copies of messages between each other, such that even the failure of almost every node in the network will still allow users to sync their waiting messages. If all Propagation Nodes disappear or are destroyed, users can still communicate directly. Reticulum and LXMF will degrade gracefully all the way down to single users communicating directly via long-range data radios. Anyone can start up new propagation nodes and integrate them into existing networks without permission or coordination. Even a small and cheap device like a Rasperry Pi can handle messages for millions of users. LXMF networks are designed to be quite resilient, as long as there are people using them.""" +The Propagation Nodes also distribute copies of messages between each other, such that even the failure of almost every node in the network will still allow users to sync their waiting messages. If all Propagation Nodes disappear or are destroyed, users can still communicate directly.\n\nReticulum and LXMF will degrade gracefully all the way down to single users communicating directly via long-range data radios. Anyone can start up new propagation nodes and integrate them into existing networks without permission or coordination. Even a small and cheap device like a Rasperry Pi can handle messages for millions of users. LXMF networks are designed to be quite resilient, as long as there are people using them.""" guide_text6 = """ -[size=18dp][b]Packets Find A Way[/b][/size][size=5dp]\n \n[/size]Connections in Reticulum networks can be wired or wireless, span many intermediary hops, run over fast links or ultra-low bandwidth radio, tunnel over the Invisible Internet (I2P), private networks, satellite connections, serial lines or anything else that Reticulum can carry data over. In most cases it will not be possible to know what path data takes in a Reticulum network, and no transmitted packets carries any identifying characteristics, apart from a destination address. There is no source addresses in Reticulum. As long as you do not reveal any connecting details between your person and your LXMF address, you can remain anonymous. Sending messages to others does not reveal [i]your[/i] address to anyone else than the intended recipient.""" +[size=18dp][b]Packets Find A Way[/b][/size][size=5dp]\n \n[/size]Connections in Reticulum networks can be wired or wireless, span many intermediary hops, run over fast links or ultra-low bandwidth radio, tunnel over the Invisible Internet (I2P), private networks, satellite connections, serial lines or anything else that Reticulum can carry data over.\n\nIn most cases it will not be possible to know what path packets takes in a Reticulum network, and apart from a destination hash, no transmitted packets carries any identifying characteristics. In Reticulum, [i]there is no source addresses[/i].\n\nAs long as you do not reveal any connecting details between your person and your LXMF address, you can remain anonymous. Sending messages to others does not reveal [i]your[/i] address to anyone else than the intended recipient.""" guide_text7 = """ -[size=18dp][b]Be Yourself, Be Unknown, Stay Free[/b][/size][size=5dp]\n \n[/size]Even with the above characteristics in mind, you [b]must remember[/b] that LXMF and Reticulum is not a technology that can guarantee anonymising connections that are already de-anonymised! If you use openCom Companion to connect to TCP Reticulum hubs over the clear Internet, from a network that can be tied to your personal identity, an adversary may learn that you are generating LXMF traffic. If you want to avoid this, it is recommended to use I2P to connect to Reticulum hubs on the Internet. Or only connecting from within pure Reticulum networks, that take one or more hops to reach connections that span the Internet. This is a complex topic, with many more nuances than can be covered here. You are encouraged to ask on the various Reticulum discussion forums if you are in doubt. - -If you use Reticulum and LXMF on hardware that does not carry any identifiers tied to you, it is possible to establish a completely free and anonymous communication system with Reticulum and LXMF clients.""" +[size=18dp][b]Be Yourself, Be Unknown, Stay Free[/b][/size][size=5dp]\n \n[/size]Even with the above characteristics in mind, you [b]must remember[/b] that LXMF and Reticulum is not a technology that can guarantee anonymising connections that are already de-anonymised! If you use openCom Companion to connect to TCP Reticulum hubs over the clear Internet, from a network that can be tied to your personal identity, an adversary may learn that you are generating LXMF traffic. If you want to avoid this, it is recommended to use I2P to connect to Reticulum hubs on the Internet. Or only connecting from within pure Reticulum networks, that take one or more hops to reach connections that span the Internet. This is a complex topic, with many more nuances than can be covered here. You are encouraged to ask on the various Reticulum discussion forums if you are in doubt. If you use Reticulum and LXMF on hardware that does not carry any identifiers tied to you, it is possible to establish a completely free and identification-less communication system with Reticulum and LXMF clients.""" guide_text8 = """""" guide_text9 = """ -[size=18dp][b]Please Support The Upstream Project[/b][/size][size=5dp]\n \n[/size]It took Mark Qvist more than seven years to design and built the entire ecosystem of software and hardware that supports openCom Companion and the openCom line of RNodes. If this project is valuable to you, please go to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support his project with a donation. Every donation directly makes the entire Reticulum project possible. - -Thank you very much for using Free Communications Systems. +[size=18dp][b]Please Support The Upstream Project[/b][/size][size=5dp]\n \n[/size]It took Mark Qvist more than seven years to design and built the entire ecosystem of software and hardware that supports openCom Companion and the openCom line of RNodes. If this project is valuable to you, please go to [u][ref=link]https://unsigned.io/donate[/ref][/u] to support his project with a donation. Every donation directly makes the entire Reticulum project possible. Thank you very much for using Free Communications Systems. """ info1 = guide_text1 info2 = guide_text8 info3 = guide_text2 info4 = guide_text3 + info10 = guide_text10 info5 = guide_text4 info6 = guide_text5 info7 = guide_text6 @@ -6107,6 +6202,7 @@ Thank you very much for using Free Communications Systems. info7 = "[color=#"+dark_theme_text_color+"]"+info7+"[/color]" info8 = "[color=#"+dark_theme_text_color+"]"+info8+"[/color]" info9 = "[color=#"+dark_theme_text_color+"]"+info9+"[/color]" + info10 = "[color=#"+dark_theme_text_color+"]"+info10+"[/color]" self.guide_screen.ids.guide_info1.text = info1 self.guide_screen.ids.guide_info2.text = info2 self.guide_screen.ids.guide_info3.text = info3 @@ -6116,6 +6212,7 @@ Thank you very much for using Free Communications Systems. self.guide_screen.ids.guide_info7.text = info7 self.guide_screen.ids.guide_info8.text = info8 self.guide_screen.ids.guide_info9.text = info9 + self.guide_screen.ids.guide_info10.text = info10 self.guide_screen.ids.guide_info9.bind(on_ref_press=link_exec) self.guide_screen.ids.guide_scrollview.effect_cls = ScrollEffect diff --git a/sbapp/patches/AndroidManifest.tmpl.xml b/sbapp/patches/AndroidManifest.tmpl.xml index 936455d..549e30d 100644 --- a/sbapp/patches/AndroidManifest.tmpl.xml +++ b/sbapp/patches/AndroidManifest.tmpl.xml @@ -54,6 +54,7 @@ {{ args.extra_manifest_application_arguments }} android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}" android:hardwareAccelerated="true" + android:requestLegacyExternalStorage="true" > {% for l in args.android_used_libs %} diff --git a/sbapp/patches/PythonService.java b/sbapp/patches/PythonService.java index b867d01..3fd0c19 100644 --- a/sbapp/patches/PythonService.java +++ b/sbapp/patches/PythonService.java @@ -144,11 +144,13 @@ public class PythonService extends Service implements Runnable { Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); builder.setContentTitle("openCom Companion Active"); // builder.setContentText("Reticulum Active"); + // builder.setContentTitle("Reticulum available"); + // builder.setContentText("Reticulum available"); builder.setContentIntent(pIntent); // builder.setOngoing(true); String files_path = context.getFilesDir().getPath(); - Bitmap icon_bitmap = BitmapFactory.decodeFile(files_path+"/app/assets/notification_icon.png"); + Bitmap icon_bitmap = BitmapFactory.decodeFile(files_path+"/app/assets/notification_icon_black.png"); Icon service_icon = Icon.createWithBitmap(icon_bitmap); // builder.setSmallIcon(context.getApplicationInfo().icon); builder.setSmallIcon(service_icon); diff --git a/sbapp/services/occservice.py b/sbapp/services/occservice.py index dc1116c..205fc3a 100644 --- a/sbapp/services/occservice.py +++ b/sbapp/services/occservice.py @@ -38,10 +38,12 @@ if RNS.vendor.platformutils.get_platform() == "android": AndroidString = autoclass('java.lang.String') NotificationManager = autoclass('android.app.NotificationManager') Context = autoclass('android.content.Context') + JString = autoclass('java.lang.String') if android_api_version >= 26: NotificationBuilder = autoclass('android.app.Notification$Builder') NotificationChannel = autoclass('android.app.NotificationChannel') + RingtoneManager = autoclass('android.media.RingtoneManager') from usb4a import usb from usbserial4a import serial4a @@ -89,33 +91,39 @@ class SidebandService(): self.notification_channel = NotificationChannel(channel_id, channel_name, NotificationManager.IMPORTANCE_DEFAULT) self.notification_channel.enableVibration(True) + self.notification_channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), None) self.notification_channel.setShowBadge(True) self.notification_service.createNotificationChannel(self.notification_channel) notification = NotificationBuilder(self.app_context, channel_id) notification.setContentTitle(title) notification.setContentText(AndroidString(content)) + notification.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) # if group != None: # notification.setGroup(group_id) if not self.notification_small_icon: - path = self.sideband.notification_icon + # path = self.sideband.notification_icon + path = self.sideband.notif_icon_black bitmap = BitmapFactory.decodeFile(path) self.notification_small_icon = Icon.createWithBitmap(bitmap) notification.setSmallIcon(self.notification_small_icon) + # notification.setLargeIcon(self.notification_small_icon) # large_icon_path = self.sideband.icon # bitmap_icon = BitmapFactory.decodeFile(large_icon_path) # notification.setLargeIcon(bitmap_icon) - if not self.notification_intent: - notification_intent = Intent(self.app_context, python_act) - notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - notification_intent.setAction(Intent.ACTION_MAIN) - notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) - self.notification_intent = PendingIntent.getActivity(self.app_context, 0, notification_intent, 0) + notification_intent = Intent(self.app_context, python_act) + notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + notification_intent.setAction(Intent.ACTION_MAIN) + notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) + if context_id != None: + cstr = f"conversation.{context_id}" + notification_intent.putExtra(JString("intent_action"), JString(cstr)) + self.notification_intent = PendingIntent.getActivity(self.app_context, 0, notification_intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT) notification.setContentIntent(self.notification_intent) notification.setAutoCancel(True) @@ -149,6 +157,18 @@ class SidebandService(): return True + def update_power_restrictions(self): + package_name = "io.unsigned.sideband" + if RNS.vendor.platformutils.is_android(): + if android_api_version >= 28: + if self.power_manager != None: + if self.power_manager.isIgnoringBatteryOptimizations(package_name): + self.power_restricted = False + else: + self.power_restricted = True + + self.sideband.setstate("android.power_restricted", self.power_restricted) + def android_location_callback(self, **kwargs): self._raw_gps = kwargs self._last_gps_update = time.time() @@ -200,6 +220,7 @@ class SidebandService(): self.app_context = None self.wifi_manager = None self.power_manager = None + self.power_restricted = False self.usb_devices = [] self.usb_device_filter = SidebandService.usb_device_filter @@ -235,6 +256,7 @@ class SidebandService(): self.sideband.start() self.update_connectivity_type() + self.update_power_restrictions() if RNS.vendor.platformutils.is_android(): RNS.log("Discovered USB devices: "+str(self.usb_devices), RNS.LOG_EXTREME) @@ -354,7 +376,13 @@ class SidebandService(): else: rs = "Interface Down" - stat += "[b]openCom device[/b]\n{rs}\n\n".format(rs=rs) + bs = "" + bat_state = self.sideband.interface_rnode.get_battery_state_string() + bat_percent = self.sideband.interface_rnode.get_battery_percent() + if bat_state != "unknown": + bs = f"\nBattery at {bat_percent}%" + + stat += f"[b]openCom device[/b]\n{rs}{bs}\n\n" if self.sideband.interface_modem != None: if self.sideband.interface_modem.online: diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 0447a32..a1c9a0e 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -151,7 +151,9 @@ class SidebandCore(): self.default_lxm_limit = 128*1000 self.state_db = {} self.state_lock = Lock() + self.message_router = None self.rpc_connection = None + self.rpc_lock = Lock() self.service_stopped = False self.service_context = service_context self.owner_service = owner_service @@ -198,6 +200,7 @@ class SidebandCore(): self.icon_32 = self.asset_dir+"/icon_32.png" self.icon_macos = self.asset_dir+"/icon_macos.png" self.notification_icon = self.asset_dir+"/notification_icon.png" + self.notif_icon_black = self.asset_dir+"/notification_icon_black.png" os.environ["TELEMETER_GEOID_PATH"] = os.path.join(self.asset_dir, "geoids") @@ -218,6 +221,7 @@ class SidebandCore(): self.last_lxmf_announce = 0 self.last_if_change_announce = 0 self.interface_local_adding = False + self.interface_rnode_adding = False self.next_auto_announce = time.time() + 60*(random.random()*(SidebandCore.AUTO_ANNOUNCE_RANDOM_MAX-SidebandCore.AUTO_ANNOUNCE_RANDOM_MIN)+SidebandCore.AUTO_ANNOUNCE_RANDOM_MIN) try: @@ -482,6 +486,7 @@ class SidebandCore(): self.config["hw_rnode_beacondata"] = None self.config["hw_rnode_bt_device"] = None self.config["hw_rnode_bluetooth"] = False + self.config["hw_rnode_ble"] = False self.config["hw_modem_baudrate"] = 57600 self.config["hw_modem_databits"] = 8 self.config["hw_modem_stopbits"] = 1 @@ -575,11 +580,15 @@ class SidebandCore(): self.config["display_style_in_contact_list"] = False if not "lxm_limit_1mb" in self.config: self.config["lxm_limit_1mb"] = True + if not "hq_ptt" in self.config: + self.config["hq_ptt"] = False if not "input_language" in self.config: self.config["input_language"] = None if not "allow_predictive_text" in self.config: self.config["allow_predictive_text"] = False + if not "block_predictive_text" in self.config: + self.config["block_predictive_text"] = False if not "connect_transport" in self.config: self.config["connect_transport"] = False @@ -655,6 +664,8 @@ class SidebandCore(): self.config["hw_rnode_beacondata"] = None if not "hw_rnode_bluetooth" in self.config: self.config["hw_rnode_bluetooth"] = False + if not "hw_rnode_ble" in self.config: + self.config["hw_rnode_ble"] = False if not "hw_rnode_enable_framebuffer" in self.config: self.config["hw_rnode_enable_framebuffer"] = False if not "hw_rnode_bt_device" in self.config: @@ -820,6 +831,7 @@ class SidebandCore(): self.update_ignore_invalid_stamps() except Exception as e: RNS.log("Error while reloading configuration: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) def __save_config(self): RNS.log("Saving openCom Companion configuration...", RNS.LOG_DEBUG) @@ -925,12 +937,13 @@ class SidebandCore(): RNS.log("No active propagation node configured") else: try: - self.active_propagation_node = dest - self.config["last_lxmf_propagation_node"] = dest - self.message_router.set_outbound_propagation_node(dest) - - RNS.log("Active propagation node set to: "+RNS.prettyhexrep(dest)) - self.__save_config() + if self.message_router: + self.active_propagation_node = dest + self.config["last_lxmf_propagation_node"] = dest + self.message_router.set_outbound_propagation_node(dest) + + RNS.log("Active propagation node set to: "+RNS.prettyhexrep(dest)) + self.__save_config() except Exception as e: RNS.log("Error while setting LXMF propagation node: "+str(e), RNS.LOG_ERROR) @@ -1263,7 +1276,8 @@ class SidebandCore(): self.message_router.handle_outbound(message) else: if message.state == LXMF.LXMessage.DELIVERED: - self.setpersistent(f"telemetry.{RNS.hexrep(message.destination_hash, delimit=False)}.last_request_success_timebase", message.request_timebase) + delivery_timebase = int(time.time()) + self.setpersistent(f"telemetry.{RNS.hexrep(message.destination_hash, delimit=False)}.last_request_success_timebase", delivery_timebase) self.setstate(f"telemetry.{RNS.hexrep(message.destination_hash, delimit=False)}.request_sending", False) if message.destination_hash == self.config["telemetry_collector"]: self.pending_telemetry_request = False @@ -1278,13 +1292,8 @@ class SidebandCore(): else: if self.is_client: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - - self.rpc_connection.send({"request_latest_telemetry": {"from_addr": from_addr}}) - response = self.rpc_connection.recv() - return response - + return self.service_rpc_request({"request_latest_telemetry": {"from_addr": from_addr}}) + except Exception as e: RNS.log("Error while requesting latest telemetry over RPC: "+str(e), RNS.LOG_DEBUG) RNS.trace_exception(e) @@ -1357,17 +1366,12 @@ class SidebandCore(): else: if self.is_client: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - - self.rpc_connection.send({"send_latest_telemetry": { + return self.service_rpc_request({"send_latest_telemetry": { "to_addr": to_addr, "stream": stream, "is_authorized_telemetry_request": is_authorized_telemetry_request} }) - response = self.rpc_connection.recv() - return response - + except Exception as e: RNS.log("Error while sending latest telemetry over RPC: "+str(e), RNS.LOG_DEBUG) RNS.trace_exception(e) @@ -1545,7 +1549,7 @@ class SidebandCore(): RNS.log("Service heartbeat did not recover after retry", RNS.LOG_DEBUG) return False else: - RNS.log("Service heartbeat recovered at"+str(time), RNS.LOG_DEBUG) + RNS.log("Service heartbeat recovered at"+str(now), RNS.LOG_DEBUG) return True else: return True @@ -1575,11 +1579,7 @@ class SidebandCore(): return True else: def set(): - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"setstate": (prop, val)}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"setstate": (prop, val)}) try: set() @@ -1601,11 +1601,7 @@ class SidebandCore(): return True else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"latest_telemetry": (latest_telemetry, latest_packed_telemetry)}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"latest_telemetry": (latest_telemetry, latest_packed_telemetry)}) except Exception as e: RNS.log("Error while setting telemetry over RPC: "+str(e), RNS.LOG_DEBUG) return False @@ -1622,11 +1618,7 @@ class SidebandCore(): return True else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"set_debug": debug}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"set_debug": debug}) except Exception as e: RNS.log("Error while setting log level over RPC: "+str(e), RNS.LOG_DEBUG) return False @@ -1640,15 +1632,26 @@ class SidebandCore(): return True else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"set_ui_recording": recording}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"set_ui_recording": recording}) except Exception as e: RNS.log("Error while setting UI recording status over RPC: "+str(e), RNS.LOG_DEBUG) return False + def service_rpc_request(self, request): + # RNS.log("Running service RPC call: "+str(request), RNS.LOG_DEBUG) + try: + with self.rpc_lock: + if self.rpc_connection == None: + self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) + self.rpc_connection.send(request) + response = self.rpc_connection.recv() + return response + + except Exception as e: + if not type(e) == ConnectionRefusedError: + RNS.log(f"An error occurred while executing the service RPC request: {request}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + def getstate(self, prop, allow_cache=False): with self.state_lock: if not self.service_stopped: @@ -1666,11 +1669,7 @@ class SidebandCore(): return None else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"getstate": prop}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"getstate": prop}) except Exception as e: RNS.log("Error while retrieving state "+str(prop)+" over RPC: "+str(e), RNS.LOG_DEBUG) @@ -1705,11 +1704,7 @@ class SidebandCore(): return self._get_plugins_info() else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"get_plugins_info": True}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"get_plugins_info": True}) except Exception as e: ed = "Error while getting plugins info over RPC: "+str(e) RNS.log(ed, RNS.LOG_DEBUG) @@ -1744,11 +1739,7 @@ class SidebandCore(): return self._get_destination_establishment_rate(destination_hash) else: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - self.rpc_connection.send({"get_destination_establishment_rate": destination_hash}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"get_destination_establishment_rate": destination_hash}) except Exception as e: ed = "Error while getting destination link etablishment rate over RPC: "+str(e) RNS.log(ed, RNS.LOG_DEBUG) @@ -1832,11 +1823,15 @@ class SidebandCore(): elif "get_lxm_progress" in call: args = call["get_lxm_progress"] connection.send(self.get_lxm_progress(args["lxm_hash"])) + elif "get_lxm_stamp_cost" in call: + args = call["get_lxm_stamp_cost"] + connection.send(self.get_lxm_stamp_cost(args["lxm_hash"])) else: connection.send(None) except Exception as e: RNS.log("Error on client RPC connection: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) try: connection.close() except: @@ -3185,6 +3180,25 @@ class SidebandCore(): self.lxmf_announce(attached_interface=self.interface_local) threading.Thread(target=job, daemon=True).start() + if hasattr(self, "interface_rnode") and self.interface_rnode != None: + if len(self.interface_rnode.hw_errors) > 0: + self.setpersistent("runtime.errors.rnode", self.interface_rnode.hw_errors[0]) + self.interface_rnode.hw_errors = [] + + # if not self.interface_rnode_adding: + # RNS.log("Hardware error on RNodeInterface, scheduling re-init", RNS.LOG_DEBUG) + # if self.interface_rnode in RNS.Transport.interfaces: + # RNS.Transport.interfaces.remove(self.interface_rnode) + # del self.interface_rnode + # self.interface_rnode = None + # self.interface_rnode_adding = True + # def job(): + # self.__add_rnodeinterface(delay=5) + # if self.config["start_announce"] == True: + # time.sleep(12) + # self.lxmf_announce(attached_interface=self.interface_rnode) + # threading.Thread(target=job, daemon=True).start() + if (now - last_multicast_lock_check > 120): RNS.log("Checking multicast and wake locks", RNS.LOG_DEBUG) self.owner_service.take_locks() @@ -3503,6 +3517,167 @@ class SidebandCore(): self.interface_local = None self.interface_local_adding = False + def __add_rnodeinterface(self, delay=None): + self.interface_rnode_adding = True + if delay: + time.sleep(delay) + + try: + RNS.log("Adding RNode Interface...", RNS.LOG_DEBUG) + target_device = None + if len(self.owner_app.usb_devices) > 0: + # TODO: Add more intelligent selection here + target_device = self.owner_app.usb_devices[0] + + # if target_device or self.config["hw_rnode_bluetooth"]: + if target_device != None: + target_port = target_device["port"] + else: + target_port = None + + bt_device_name = None + ble_dispatcher = None + rnode_allow_bluetooth = False + if self.getpersistent("permissions.bluetooth"): + if self.config["hw_rnode_bluetooth"]: + RNS.log("Allowing RNode bluetooth", RNS.LOG_DEBUG) + rnode_allow_bluetooth = True + ble_dispatcher = RNS.Interfaces.Android.RNodeMultiInterface.AndroidBLEDispatcher() + if self.config["hw_rnode_bt_device"] != None: + bt_device_name = self.config["hw_rnode_bt_device"] + + else: + RNS.log("Disallowing RNode bluetooth since config is disabled", RNS.LOG_DEBUG) + rnode_allow_bluetooth = False + else: + RNS.log("Disallowing RNode bluetooth due to missing permission", RNS.LOG_DEBUG) + rnode_allow_bluetooth = False + + if self.config["connect_rnode_ifac_netname"] == "": + ifac_netname = None + else: + ifac_netname = self.config["connect_rnode_ifac_netname"] + + if self.config["connect_rnode_ifac_passphrase"] == "": + ifac_netkey = None + else: + ifac_netkey = self.config["connect_rnode_ifac_passphrase"] + + if self.config["hw_rnode_atl_short"] == "": + atl_short = None + else: + atl_short = self.config["hw_rnode_atl_short"] + + if self.config["hw_rnode_atl_long"] == "": + atl_long = None + else: + atl_long = self.config["hw_rnode_atl_long"] + + if self.config["hw_rnode_secondary_modem"]: + if self.config["hw_rnode_sec_atl_short"] == "": + sec_atl_short = None + else: + sec_atl_short = self.config["hw_rnode_sec_atl_short"] + + if self.config["hw_rnode_sec_atl_long"] == "": + sec_atl_long = None + else: + sec_atl_long = self.config["hw_rnode_sec_atl_long"] + + subint_config = [[0]*11 for i in range(2)] + + # Primary modem + subint_config[0][0] = "Primary modem" # Name of interface + subint_config[0][1] = 0 # Virtual port + subint_config[0][2] = self.config["hw_rnode_frequency"] + subint_config[0][3] = self.config["hw_rnode_bandwidth"] + subint_config[0][4] = self.config["hw_rnode_tx_power"] + subint_config[0][5] = self.config["hw_rnode_spreading_factor"] + subint_config[0][6] = self.config["hw_rnode_coding_rate"] + subint_config[0][7] = False # flow control hardcoded to false for now + subint_config[0][8] = atl_short + subint_config[0][9] = atl_long + subint_config[0][10] = True # outgoing + + # Secondary modem + subint_config[1][0] = "Secondary modem" # Name of interface + subint_config[1][1] = 1 # Virtual port + subint_config[1][2] = self.config["hw_rnode_sec_frequency"] + subint_config[1][3] = self.config["hw_rnode_sec_bandwidth"] + subint_config[1][4] = self.config["hw_rnode_sec_tx_power"] + subint_config[1][5] = self.config["hw_rnode_sec_spreading_factor"] + subint_config[1][6] = self.config["hw_rnode_coding_rate"] + subint_config[1][7] = False # flow control hardcoded to false for now + subint_config[0][8] = sec_atl_short + subint_config[0][9] = sec_atl_long + subint_config[1][10] = True # outgoing + + rnodeinterface = RNS.Interfaces.Android.RNodeMultiInterface.RNodeMultiInterface( + RNS.Transport, + "RNodeInterface", + target_port, + subint_config, + ble_dispatcher = ble_dispatcher, + allow_bluetooth = rnode_allow_bluetooth, + target_device_name = bt_device_name, + ) + + rnodeinterface.start() + else: + rnodeinterface = RNS.Interfaces.Android.RNodeInterface.RNodeInterface( + RNS.Transport, + "RNodeInterface", + target_port, + frequency = self.config["hw_rnode_frequency"], + bandwidth = self.config["hw_rnode_bandwidth"], + txpower = self.config["hw_rnode_tx_power"], + sf = self.config["hw_rnode_spreading_factor"], + cr = self.config["hw_rnode_coding_rate"], + flow_control = None, + id_interval = self.config["hw_rnode_beaconinterval"], + id_callsign = self.config["hw_rnode_beacondata"], + allow_bluetooth = rnode_allow_bluetooth, + target_device_name = bt_device_name, + st_alock = atl_short, + lt_alock = atl_long, + ) + + rnodeinterface.OUT = True + + if RNS.Reticulum.transport_enabled(): + if_mode = Interface.Interface.MODE_FULL + if self.config["connect_ifmode_rnode"] == "gateway": + if_mode = Interface.Interface.MODE_GATEWAY + elif self.config["connect_ifmode_rnode"] == "access point": + if_mode = Interface.Interface.MODE_ACCESS_POINT + elif self.config["connect_ifmode_rnode"] == "roaming": + if_mode = Interface.Interface.MODE_ROAMING + elif self.config["connect_ifmode_rnode"] == "boundary": + if_mode = Interface.Interface.MODE_BOUNDARY + else: + if_mode = None + + self.reticulum._add_interface(rnodeinterface, mode = if_mode, ifac_netname = ifac_netname, ifac_netkey = ifac_netkey) + self.interface_rnode = rnodeinterface + + if rnodeinterface != None: + if len(rnodeinterface.hw_errors) > 0: + self.setpersistent("startup.errors.rnode", rnodeinterface.hw_errors[0]) + + if self.config["hw_rnode_enable_framebuffer"] == True: + if self.interface_rnode.online: + self.interface_rnode.display_image(sideband_fb_data) + self.interface_rnode.enable_external_framebuffer() + else: + self.interface_rnode.last_imagedata = sideband_fb_data + else: + if self.interface_rnode.online: + self.interface_rnode.disable_external_framebuffer() + + except Exception as e: + RNS.log("Error while adding RNode Interface. The contained exception was: "+str(e)) + self.interface_rnode = None + def _reticulum_log_debug(self, debug=False): self.log_verbose = debug if self.log_verbose: @@ -3663,161 +3838,7 @@ class SidebandCore(): if self.config["connect_rnode"]: self.setstate("init.loadingstate", "Starting RNode") - try: - RNS.log("Adding RNode Interface...", RNS.LOG_DEBUG) - target_device = None - if len(self.owner_app.usb_devices) > 0: - # TODO: Add more intelligent selection here - target_device = self.owner_app.usb_devices[0] - - # if target_device or self.config["hw_rnode_bluetooth"]: - if target_device != None: - target_port = target_device["port"] - else: - target_port = None - - bt_device_name = None - ble_dispatcher = None - rnode_allow_bluetooth = False - if self.getpersistent("permissions.bluetooth"): - if self.config["hw_rnode_bluetooth"]: - RNS.log("Allowing RNode bluetooth", RNS.LOG_DEBUG) - rnode_allow_bluetooth = True - ble_dispatcher = RNS.Interfaces.Android.RNodeMultiInterface.AndroidBLEDispatcher() - if self.config["hw_rnode_bt_device"] != None: - bt_device_name = self.config["hw_rnode_bt_device"] - - else: - RNS.log("Disallowing RNode bluetooth since config is disabled", RNS.LOG_DEBUG) - rnode_allow_bluetooth = False - else: - RNS.log("Disallowing RNode bluetooth due to missing permission", RNS.LOG_DEBUG) - rnode_allow_bluetooth = False - - if self.config["connect_rnode_ifac_netname"] == "": - ifac_netname = None - else: - ifac_netname = self.config["connect_rnode_ifac_netname"] - - if self.config["connect_rnode_ifac_passphrase"] == "": - ifac_netkey = None - else: - ifac_netkey = self.config["connect_rnode_ifac_passphrase"] - - if self.config["hw_rnode_atl_short"] == "": - atl_short = None - else: - atl_short = self.config["hw_rnode_atl_short"] - - if self.config["hw_rnode_atl_long"] == "": - atl_long = None - else: - atl_long = self.config["hw_rnode_atl_long"] - - if self.config["hw_rnode_secondary_modem"]: - if self.config["hw_rnode_sec_atl_short"] == "": - sec_atl_short = None - else: - sec_atl_short = self.config["hw_rnode_sec_atl_short"] - - if self.config["hw_rnode_sec_atl_long"] == "": - sec_atl_long = None - else: - sec_atl_long = self.config["hw_rnode_sec_atl_long"] - - subint_config = [[0]*11 for i in range(2)] - - # Primary modem - subint_config[0][0] = "Primary modem" # Name of interface - subint_config[0][1] = 0 # Virtual port - subint_config[0][2] = self.config["hw_rnode_frequency"] - subint_config[0][3] = self.config["hw_rnode_bandwidth"] - subint_config[0][4] = self.config["hw_rnode_tx_power"] - subint_config[0][5] = self.config["hw_rnode_spreading_factor"] - subint_config[0][6] = self.config["hw_rnode_coding_rate"] - subint_config[0][7] = False # flow control hardcoded to false for now - subint_config[0][8] = atl_short - subint_config[0][9] = atl_long - subint_config[0][10] = True # outgoing - - # Secondary modem - subint_config[1][0] = "Secondary modem" # Name of interface - subint_config[1][1] = 1 # Virtual port - subint_config[1][2] = self.config["hw_rnode_sec_frequency"] - subint_config[1][3] = self.config["hw_rnode_sec_bandwidth"] - subint_config[1][4] = self.config["hw_rnode_sec_tx_power"] - subint_config[1][5] = self.config["hw_rnode_sec_spreading_factor"] - subint_config[1][6] = self.config["hw_rnode_coding_rate"] - subint_config[1][7] = False # flow control hardcoded to false for now - subint_config[0][8] = sec_atl_short - subint_config[0][9] = sec_atl_long - subint_config[1][10] = True # outgoing - - rnodeinterface = RNS.Interfaces.Android.RNodeMultiInterface.RNodeMultiInterface( - RNS.Transport, - "RNodeInterface", - target_port, - subint_config, - ble_dispatcher = ble_dispatcher, - allow_bluetooth = rnode_allow_bluetooth, - target_device_name = bt_device_name, - ) - - rnodeinterface.start() - else: - rnodeinterface = RNS.Interfaces.Android.RNodeInterface.RNodeInterface( - RNS.Transport, - "RNodeInterface", - target_port, - frequency = self.config["hw_rnode_frequency"], - bandwidth = self.config["hw_rnode_bandwidth"], - txpower = self.config["hw_rnode_tx_power"], - sf = self.config["hw_rnode_spreading_factor"], - cr = self.config["hw_rnode_coding_rate"], - flow_control = None, - id_interval = self.config["hw_rnode_beaconinterval"], - id_callsign = self.config["hw_rnode_beacondata"], - allow_bluetooth = rnode_allow_bluetooth, - target_device_name = bt_device_name, - st_alock = atl_short, - lt_alock = atl_long, - ) - - rnodeinterface.OUT = True - - if RNS.Reticulum.transport_enabled(): - if_mode = Interface.Interface.MODE_FULL - if self.config["connect_ifmode_rnode"] == "gateway": - if_mode = Interface.Interface.MODE_GATEWAY - elif self.config["connect_ifmode_rnode"] == "access point": - if_mode = Interface.Interface.MODE_ACCESS_POINT - elif self.config["connect_ifmode_rnode"] == "roaming": - if_mode = Interface.Interface.MODE_ROAMING - elif self.config["connect_ifmode_rnode"] == "boundary": - if_mode = Interface.Interface.MODE_BOUNDARY - else: - if_mode = None - - self.reticulum._add_interface(rnodeinterface, mode = if_mode, ifac_netname = ifac_netname, ifac_netkey = ifac_netkey) - self.interface_rnode = rnodeinterface - - if rnodeinterface != None: - if len(rnodeinterface.hw_errors) > 0: - self.setpersistent("startup.errors.rnode", rnodeinterface.hw_errors[0]) - - if self.config["hw_rnode_enable_framebuffer"] == True: - if self.interface_rnode.online: - self.interface_rnode.display_image(sideband_fb_data) - self.interface_rnode.enable_external_framebuffer() - else: - self.interface_rnode.last_imagedata = sideband_fb_data - else: - if self.interface_rnode.online: - self.interface_rnode.disable_external_framebuffer() - - except Exception as e: - RNS.log("Error while adding RNode Interface. The contained exception was: "+str(e)) - self.interface_rnode = None + self.__add_rnodeinterface() elif self.config["connect_serial"]: self.setstate("init.loadingstate", "Starting Serial Interface") @@ -4079,12 +4100,7 @@ class SidebandCore(): else: if self.is_client: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - - self.rpc_connection.send({"get_lxm_progress": {"lxm_hash": lxm_hash}}) - response = self.rpc_connection.recv() - return response + return self.service_rpc_request({"get_lxm_progress": {"lxm_hash": lxm_hash}}) except Exception as e: RNS.log("Error while getting LXM progress over RPC: "+str(e), RNS.LOG_DEBUG) @@ -4105,12 +4121,37 @@ class SidebandCore(): RNS.log("An error occurred while getting message transfer progress: "+str(e), RNS.LOG_ERROR) return None + def _service_get_lxm_stamp_cost(self, lxm_hash): + if not RNS.vendor.platformutils.is_android(): + return False + else: + if self.is_client: + try: + return self.service_rpc_request({"get_lxm_stamp_cost": { "lxm_hash": lxm_hash } }) + + except Exception as e: + RNS.log("Error while sending message over RPC: "+str(e), RNS.LOG_DEBUG) + RNS.trace_exception(e) + return False + else: + return False + def get_lxm_stamp_cost(self, lxm_hash): - try: - return self.message_router.get_outbound_lxm_stamp_cost(lxm_hash) - except Exception as e: - RNS.log("An error occurred while getting message transfer stamp cost: "+str(e), RNS.LOG_ERROR) - return None + if self.allow_service_dispatch and self.is_client: + try: + return self._service_get_lxm_stamp_cost(lxm_hash) + + except Exception as e: + RNS.log("Error while getting message transfer stamp cost: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) + return False + + else: + try: + return self.message_router.get_outbound_lxm_stamp_cost(lxm_hash) + except Exception as e: + RNS.log("An error occurred while getting message transfer stamp cost: "+str(e), RNS.LOG_ERROR) + return None def _service_send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None): if not RNS.vendor.platformutils.is_android(): @@ -4118,10 +4159,7 @@ class SidebandCore(): else: if self.is_client: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - - self.rpc_connection.send({"send_message": { + return self.service_rpc_request({"send_message": { "content": content, "destination_hash": destination_hash, "propagation": propagation, @@ -4131,8 +4169,6 @@ class SidebandCore(): "image": image, "audio": audio} }) - response = self.rpc_connection.recv() - return response except Exception as e: RNS.log("Error while sending message over RPC: "+str(e), RNS.LOG_DEBUG) @@ -4147,17 +4183,12 @@ class SidebandCore(): else: if self.is_client: try: - if self.rpc_connection == None: - self.rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) - - self.rpc_connection.send({"send_command": { + return self.service_rpc_request({"send_command": { "content": content, "destination_hash": destination_hash, "propagation": propagation} }) - response = self.rpc_connection.recv() - return response - + except Exception as e: RNS.log("Error while sending command over RPC: "+str(e), RNS.LOG_DEBUG) RNS.trace_exception(e) @@ -4431,6 +4462,7 @@ class SidebandCore(): self.notify(title=self.peer_display_name(context_dest), content=notification_content, group="LXM", context_id=RNS.hexrep(context_dest, delimit=False)) except Exception as e: RNS.log("Could not post notification for received message: "+str(e), RNS.LOG_ERROR) + RNS.trace_exception(e) def ptt_playback(self, message): ptt_timeout = 60 @@ -4529,7 +4561,7 @@ class SidebandCore(): thread.start() self.setstate("core.started", True) - RNS.log("openCom Companion Core "+str(self)+" version "+str(self.version_str)+" started") + RNS.log("openCom Companion Core "+str(self)+" "+str(self.version_str)+" started") def stop_webshare(self): if self.webshare_server != None: diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 145f25c..09f0307 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -89,13 +89,13 @@ MDNavigationLayout: on_release: root.ids.screen_manager.app.announces_action(self) - OneLineIconListItem: - text: "Local Broadcasts" - on_release: root.ids.screen_manager.app.broadcasts_action(self) + # OneLineIconListItem: + # text: "Local Broadcasts" + # on_release: root.ids.screen_manager.app.broadcasts_action(self) - IconLeftWidget: - icon: "radio-tower" - on_release: root.ids.screen_manager.app.broadcasts_action(self) + # IconLeftWidget: + # icon: "radio-tower" + # on_release: root.ids.screen_manager.app.broadcasts_action(self) OneLineIconListItem: @@ -807,6 +807,14 @@ MDScreen: text_size: self.width, None height: self.texture_size[1] + MDLabel: + id: guide_info10 + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + MDLabel: id: guide_info5 markup: True @@ -1678,6 +1686,22 @@ MDScreen: disabled: False active: False + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(24),dp(0)] + height: dp(48) + + MDLabel: + text: "Use high-quality voice for PTT" + font_style: "H6" + + MDSwitch: + id: settings_hq_ptt + pos_hint: {"center_y": 0.3} + disabled: False + active: False + MDBoxLayout: orientation: "horizontal" size_hint_y: None @@ -1822,20 +1846,20 @@ MDScreen: pos_hint: {"center_y": 0.3} active: False - # MDBoxLayout: - # orientation: "horizontal" - # size_hint_y: None - # padding: [0,0,dp(24),dp(0)] - # height: dp(48) + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(24),dp(0)] + height: dp(48) - # MDLabel: - # text: "Allow Predictive Text" - # font_style: "H6" + MDLabel: + text: "Block Predictive Text" + font_style: "H6" - # MDSwitch: - # id: settings_allow_predictive_text - # pos_hint: {"center_y": 0.3} - # active: False + MDSwitch: + id: settings_block_predictive_text + pos_hint: {"center_y": 0.3} + active: False # MDBoxLayout: # orientation: "vertical" @@ -1844,7 +1868,7 @@ MDScreen: # padding: [0, dp(24), 0, dp(24)] # MDRectangleFlatIconButton: - # id: hardware_rnode_button + # id: input_language_button # icon: "translate" # text: "Input Languages" # padding: [dp(0), dp(14), dp(0), dp(14)] @@ -2579,6 +2603,21 @@ MDScreen: pos_hint: {"center_y": 0.3} active: False + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + padding: [0,0,dp(24),dp(0)] + height: dp(48) + + MDLabel: + text: "Device requires BLE" + font_style: "H6" + + MDSwitch: + id: hardware_rnode_ble + pos_hint: {"center_y": 0.3} + active: False + MDLabel: id: hardware_rnode_info markup: True diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index 8798723..2047ecf 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -194,6 +194,15 @@ class Messages(): self.details_dialog.open() def update(self, limit=8): + if self.app.sideband.config["block_predictive_text"]: + if self.ids.message_text.input_type != "null": + self.ids.message_text.input_type = "null" + self.ids.message_text.keyboard_suggestions = False + else: + if self.ids.message_text.input_type != "text": + self.ids.message_text.input_type = "text" + self.ids.message_text.keyboard_suggestions = True + for new_message in self.app.sideband.list_messages(self.context_dest, after=self.latest_message_timestamp,limit=limit): self.new_messages.append(new_message) @@ -1229,6 +1238,7 @@ MDScreen: MDTextField: id: message_text + input_type: "text" keyboard_suggestions: True multiline: True hint_text: "Write message" diff --git a/setup.py b/setup.py index fbec966..3a4ebe0 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_variant() -> str: version = re.findall(version_regex, version_file_data, re.M)[0] return version except IndexError: - raise ValueError(f"Unable to find version string in {version_file}.") + return None __version__ = get_version() __variant__ = get_variant() @@ -67,7 +67,10 @@ package_data = { ] } -print("Packaging openCom Companion "+__version__+" "+__variant__) +variant_str = "" +if __variant__: + variant_str = " "+__variant__ +print("Packaging openCom Companion "+__version__+variant_str) setuptools.setup( name="occ", @@ -96,8 +99,8 @@ setuptools.setup( ] }, install_requires=[ - "rns>=0.7.8", - "lxmf>=0.5.3", + "rns>=0.8.4", + "lxmf>=0.5.7", "kivy>=2.3.0", "pillow>=10.2.0", "qrcode",