diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index 4496290..08f28e0 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -10,7 +10,7 @@ source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements, version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py -android.numeric_version = 20231114 +android.numeric_version = 20231203 # Cryptography recipe is currently broken, using RNS-internal crypto for now requirements = kivy==2.2.1,libbz2,pillow,qrcode==7.3.1,usb4a,usbserial4a @@ -25,7 +25,8 @@ android.presplash_color = #00000000 orientation = portrait fullscreen = 0 -android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT,ACCESS_NETWORK_STATE,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,MANAGE_EXTERNAL_STORAGE +android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT,ACCESS_NETWORK_STATE,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,MANAGE_EXTERNAL_STORAGE,ACCESS_BACKGROUND_LOCATION + android.api = 30 android.minapi = 24 android.ndk = 25b diff --git a/sbapp/main.py b/sbapp/main.py index abba940..d58003b 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1,6 +1,6 @@ __debug_build__ = False __disable_shaders__ = False -__version__ = "0.7.6" +__version__ = "0.7.7" __variant__ = "beta" import sys diff --git a/sbapp/services/sidebandservice.py b/sbapp/services/sidebandservice.py index 479e40a..cdc79dd 100644 --- a/sbapp/services/sidebandservice.py +++ b/sbapp/services/sidebandservice.py @@ -30,8 +30,7 @@ if RNS.vendor.platformutils.get_platform() == "android": from android import python_act android_api_version = autoclass('android.os.Build$VERSION').SDK_INT - - Context = autoclass('android.content.Context') + Intent = autoclass('android.content.Intent') BitmapFactory = autoclass('android.graphics.BitmapFactory') Icon = autoclass("android.graphics.drawable.Icon") @@ -60,6 +59,7 @@ class SidebandService(): 0x0483: [0x5740], # ST CDC 0x2E8A: [0x0005, 0x000A], # Raspberry Pi Pico } + def android_notification(self, title="", content="", ticker="", group=None, context_id=None): if android_api_version < 26: return @@ -121,12 +121,78 @@ class SidebandService(): built_notification = notification.build() self.notification_service.notify(0, built_notification) + def check_permission(self, permission): + if RNS.vendor.platformutils.is_android(): + try: + result = self.android_service.checkSelfPermission("android.permission."+permission) + if result == 0: + return True + + except Exception as e: + RNS.log("Error while checking permission: "+str(e), RNS.LOG_ERROR) + + return False + + else: + return False + + def background_location_allowed(self): + if not RNS.vendor.platformutils.is_android(): + return False + + perms = ["ACCESS_FINE_LOCATION","ACCESS_COARSE_LOCATION","ACCESS_BACKGROUND_LOCATION"] + for perm in perms: + if not self.check_permission(perm): + return False + + return True + + def android_location_callback(self, **kwargs): + self._raw_gps = kwargs + self._last_gps_update = time.time() + + def should_update_location(self): + if self.sideband.config["telemetry_enabled"] and self.sideband.config["telemetry_s_location"] and self.background_location_allowed(): + return True + else: + return False + + def update_location_provider(self): + if RNS.vendor.platformutils.is_android(): + if self.should_update_location(): + if not self._gps_started: + RNS.log("Starting service location provider", RNS.LOG_DEBUG) + + if self.gps == None: + from plyer import gps + self.gps = gps + + self.gps.configure(on_location=self.android_location_callback) + self.gps.start(minTime=self._gps_stale_time, minDistance=self._gps_min_distance) + self._gps_started = True + + else: + if self._gps_started: + RNS.log("Stopping service location provider", RNS.LOG_DEBUG) + if self.gps != None: + self.gps.stop() + self._gps_started = False + self._raw_gps = None + + def get_location(self): + return self._last_gps_update, self._raw_gps + def __init__(self): self.argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') self.app_dir = self.argument self.multicast_lock = None self.wake_lock = None self.should_run = False + self.gps = None + self._gps_started = False + self._gps_stale_time = 299 + self._gps_min_distance = 25 + self._raw_gps = None self.android_service = None self.app_context = None @@ -149,9 +215,7 @@ class SidebandService(): self.discover_usb_devices() - self.sideband = SidebandCore(self, is_service=True, android_app_dir=self.app_dir, verbose=__debug_build__) - self.sideband.service_context = self.android_service - self.sideband.owner_service = self + self.sideband = SidebandCore(self, is_service=True, android_app_dir=self.app_dir, verbose=__debug_build__, owner_service=self, service_context=self.android_service) if self.sideband.config["debug"]: Logger.setLevel(LOG_LEVELS["debug"]) @@ -161,6 +225,7 @@ class SidebandService(): if RNS.vendor.platformutils.is_android(): RNS.log("Discovered USB devices: "+str(self.usb_devices), RNS.LOG_EXTREME) + self.update_location_provider() def discover_usb_devices(self): diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 84b8c9f..cc97a2d 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -90,6 +90,7 @@ class SidebandCore(): PERIODIC_JOBS_INTERVAL = 60 PERIODIC_SYNC_RETRY = 360 TELEMETRY_INTERVAL = 60 + SERVICE_TELEMETRY_INTERVAL = 300 IF_CHANGE_ANNOUNCE_MIN_INTERVAL = 6 # In seconds AUTO_ANNOUNCE_RANDOM_MIN = 90 # In minutes @@ -103,7 +104,7 @@ class SidebandCore(): # stream logger self.log_announce(destination_hash, app_data, dest_type=SidebandCore.aspect_filter) - def __init__(self, owner_app, is_service=False, is_client=False, android_app_dir=None, verbose=False): + def __init__(self, owner_app, is_service=False, is_client=False, android_app_dir=None, verbose=False, owner_service=None, service_context=None): self.is_service = is_service self.is_client = is_client self.db = None @@ -132,6 +133,8 @@ class SidebandCore(): self.state_lock = Lock() self.rpc_connection = None self.service_stopped = False + self.service_context = service_context + self.owner_service = owner_service self.app_dir = plyer.storagepath.get_home_dir()+"/.config/sideband" self.cache_dir = self.app_dir+"/cache" @@ -2244,6 +2247,23 @@ class SidebandCore(): threading.Thread(target=telemetry_job, daemon=True).start() + def run_service_telemetry(self): + if not self.telemetry_running: + self.telemetry_running = True + def telemetry_job(): + while self.telemetry_running: + try: + self.update_telemetry() + except Exception as e: + import traceback + exception_info = "".join(traceback.TracebackException.from_exception(e).format()) + RNS.log(f"An {str(type(e))} occurred while updating service telemetry: {str(e)}", RNS.LOG_ERROR) + RNS.log(exception_info, RNS.LOG_ERROR) + + time.sleep(SidebandCore.SERVICE_TELEMETRY_INTERVAL) + + threading.Thread(target=telemetry_job, daemon=True).start() + def stop_telemetry(self): self.telemetry_running = False self.telemeter.stop_all() @@ -2298,7 +2318,10 @@ class SidebandCore(): def update_telemeter_config(self): if self.config["telemetry_enabled"] == True: if self.telemeter == None: - self.telemeter = Telemeter() + if self.service_context == None: + self.telemeter = Telemeter() + else: + self.telemeter = Telemeter(android_context=self.service_context, service=True, location_provider=self.owner_service) sensors = ["location", "information", "battery", "pressure", "temperature", "humidity", "magnetic_field", "ambient_light", "gravity", "angular_velocity", "acceleration", "proximity"] for sensor in sensors: @@ -2561,6 +2584,8 @@ class SidebandCore(): if self.is_service or self.is_standalone: while True: time.sleep(SidebandCore.PERIODIC_JOBS_INTERVAL) + self.owner_service.update_location_provider() + if self.config["lxmf_periodic_sync"] == True: if self.getpersistent("lxmf.lastsync") == None: self.setpersistent("lxmf.lastsync", time.time()) @@ -2660,6 +2685,8 @@ class SidebandCore(): if self.is_standalone or self.is_client: if self.config["telemetry_enabled"]: self.run_telemetry() + elif self.is_service: + self.run_service_telemetry() def __add_localinterface(self, delay=None): self.interface_local_adding = True diff --git a/sbapp/sideband/sense.py b/sbapp/sideband/sense.py index 8a41f63..53b90bb 100644 --- a/sbapp/sideband/sense.py +++ b/sbapp/sideband/sense.py @@ -40,7 +40,7 @@ class Telemeter(): RNS.log("An error occurred while unpacking telemetry. The contained exception was: "+str(e), RNS.LOG_ERROR) return None - def __init__(self, from_packed=False): + def __init__(self, from_packed=False, android_context=None, service=False, location_provider=None): self.sids = { Sensor.SID_TIME: Time, Sensor.SID_RECEIVED: Received, @@ -80,10 +80,15 @@ class Telemeter(): if not self.from_packed: self.enable("time") + self.location_provider = location_provider + self.android_context = android_context + self.service = service + def synthesize(self, sensor): if sensor in self.available: if not sensor in self.sensors: self.sensors[sensor] = self.sids[self.available[sensor]]() + self.sensors[sensor]._telemeter = self self.sensors[sensor].active = True self.sensors[sensor].synthesized = True @@ -92,6 +97,7 @@ class Telemeter(): if sensor in self.available: if not sensor in self.sensors: self.sensors[sensor] = self.sids[self.available[sensor]]() + self.sensors[sensor]._telemeter = self if not self.sensors[sensor].active: self.sensors[sensor].start() @@ -150,6 +156,28 @@ class Telemeter(): return rendered + def check_permission(self, permission): + if RNS.vendor.platformutils.is_android(): + if self.android_context != None: + try: + result = self.android_context.checkSelfPermission("android.permission."+permission) + if result == 0: + return True + + except Exception as e: + RNS.log("Error while checking permission: "+str(e), RNS.LOG_ERROR) + + return False + + else: + from android.permissions import check_permission + return check_permission("android.permission."+permission) + + else: + return False + + + class Sensor(): SID_NONE = 0x00 SID_TIME = 0x01 @@ -169,6 +197,7 @@ class Sensor(): SID_RECEIVED = 0x10 def __init__(self, sid = None, stale_time = None): + self._telemeter = None self._sid = sid or Sensor.SID_NONE self._stale_time = stale_time self._data = None @@ -238,6 +267,13 @@ class Sensor(): def render(self, relative_to=None): return None + def check_permission(self, permission): + if self._telemeter != None: + return self._telemeter.check_permission(permission) + else: + from android.permissions import check_permission + return check_permission("android.permission."+permission) + class Time(Sensor): SID = Sensor.SID_TIME STALE_TIME = 0.1 @@ -587,6 +623,7 @@ class Location(Sensor): self._last_update = None self._min_distance = Location.MIN_DISTANCE self._accuracy_target = Location.ACCURACY_TARGET + self._query_method = None self.latitude = None self.longitude = None @@ -619,12 +656,15 @@ class Location(Sensor): def setup_sensor(self): if RNS.vendor.platformutils.is_android(): - from android.permissions import request_permissions, check_permission - if check_permission("android.permission.ACCESS_COARSE_LOCATION") and check_permission("android.permission.ACCESS_FINE_LOCATION"): - self.gps.configure(on_location=self.android_location_callback) - self.gps.start(minTime=self._stale_time, minDistance=self._min_distance) - + if self._telemeter and self._telemeter.service: + self._location_provider = self._telemeter.location_provider + else: + if self.check_permission("ACCESS_COARSE_LOCATION") and self.check_permission("ACCESS_FINE_LOCATION"): + self.gps.configure(on_location=self.android_location_callback) + self.gps.start(minTime=self._stale_time, minDistance=self._min_distance) + self._location_provider = self + self.update_data() def teardown_sensor(self): @@ -669,33 +709,37 @@ class Location(Sensor): } elif RNS.vendor.platformutils.is_android(): - if "lat" in self._raw: - self.latitude = self._raw["lat"] - if "lon" in self._raw: - self.longitude = self._raw["lon"] - if "altitude" in self._raw: - self.altitude = self._raw["altitude"] - if "speed" in self._raw: - self.speed = self._raw["speed"] - if self.speed < 0: - self.speed = 0 - if "bearing" in self._raw: - self.bearing = self._raw["bearing"] - if "accuracy" in self._raw: - self.accuracy = self._raw["accuracy"] - if self.accuracy < 0: - self.accuracy = 0 + if self._location_provider != None: + if self._location_provider != self: + self._last_update, self._raw = self._location_provider.get_location() - if self.accuracy != None and self.accuracy <= self._accuracy_target: - self.data = { - "latitude": round(self.latitude, 6), - "longitude": round(self.longitude, 6), - "altitude": round(self.altitude, 2), - "speed": round(self.speed, 2), - "bearing": round(self.bearing, 2), - "accuracy": round(self.accuracy, 2), - "last_update": int(self._last_update), - } + if "lat" in self._raw: + self.latitude = self._raw["lat"] + if "lon" in self._raw: + self.longitude = self._raw["lon"] + if "altitude" in self._raw: + self.altitude = self._raw["altitude"] + if "speed" in self._raw: + self.speed = self._raw["speed"] + if self.speed < 0: + self.speed = 0 + if "bearing" in self._raw: + self.bearing = self._raw["bearing"] + if "accuracy" in self._raw: + self.accuracy = self._raw["accuracy"] + if self.accuracy < 0: + self.accuracy = 0 + + if self.accuracy != None and self.accuracy <= self._accuracy_target: + self.data = { + "latitude": round(self.latitude, 6), + "longitude": round(self.longitude, 6), + "altitude": round(self.altitude, 2), + "speed": round(self.speed, 2), + "bearing": round(self.bearing, 2), + "accuracy": round(self.accuracy, 2), + "last_update": int(self._last_update), + } except: self.data = None