From 536c77396f28f9b4b4fe61daa47df10a52d739e1 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 20 Sep 2022 17:28:39 +0200 Subject: [PATCH] Android service setup --- sbapp/Makefile | 4 + sbapp/buildozer.spec | 4 +- sbapp/main.py | 1 + sbapp/patches/PythonService.java | 209 ++++++++++++++++++++++++++++++ sbapp/services/sidebandservice.py | 32 ++++- sbapp/sideband/core.py | 3 +- 6 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 sbapp/patches/PythonService.java diff --git a/sbapp/Makefile b/sbapp/Makefile index 9fc2fb0..9c4bc83 100644 --- a/sbapp/Makefile +++ b/sbapp/Makefile @@ -22,6 +22,10 @@ patchsdl: cp patches/HIDDeviceUSB.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/java/org/libsdl/app/HIDDeviceUSB.java cp patches/HIDDeviceUSB.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java + cp patches/PythonService.java .buildozer/android/platform/python-for-android/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java + cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a/build/bootstrap_builds/sdl2/src/main/java/org/kivy/android/PythonService.java + cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/java/org/kivy/android/PythonService.java + injectxml: mkdir -p .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/res/xml mkdir -p .buildozer/android/platform/build-arm64-v8a/dists/sideband/templates diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index 6f53110..4bf5783 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -25,7 +25,7 @@ android.presplash_color = #00000000 orientation = all fullscreen = 0 -android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE +android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE android.api = 30 android.minapi = 27 android.ndk = 19b @@ -34,7 +34,7 @@ android.accept_sdk_license = True android.arch = arm64-v8a #android.logcat_filters = *:S python:D -# services = sidebandservice:services/sidebandservice.py:foreground +services = sidebandservice:services/sidebandservice.py:foreground android.manifest.intent_filters = patches/intent-filter.xml [buildozer] diff --git a/sbapp/main.py b/sbapp/main.py index 17a71e1..c50f779 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -96,6 +96,7 @@ class SidebandApp(MDApp): self.guide_action() self.app_state = SidebandApp.ACTIVE + self.start_android_service() def start_android_service(self): service = autoclass('io.unsigned.sideband.ServiceSidebandservice') diff --git a/sbapp/patches/PythonService.java b/sbapp/patches/PythonService.java new file mode 100644 index 0000000..0d761f7 --- /dev/null +++ b/sbapp/patches/PythonService.java @@ -0,0 +1,209 @@ +package org.kivy.android; + +import android.os.Build; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import android.app.Service; +import android.os.IBinder; +import android.os.Bundle; +import android.content.Intent; +import android.content.Context; +import android.util.Log; +import android.app.Notification; +import android.app.PendingIntent; +import android.os.Process; +import java.io.File; + +//imports for channel definition +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.graphics.Color; +import android.graphics.BitmapFactory; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; + +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.MulticastLock; + +public class PythonService extends Service implements Runnable { + + // Thread for Python code + private Thread pythonThread = null; + + // Python environment variables + private String androidPrivate; + private String androidArgument; + private String pythonName; + private String pythonHome; + private String pythonPath; + private String serviceEntrypoint; + // Argument to pass to Python code, + private String pythonServiceArgument; + + + public static PythonService mService = null; + private Intent startIntent = null; + + private boolean autoRestartService = false; + + public void setAutoRestartService(boolean restart) { + autoRestartService = restart; + } + + public int startType() { + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent arg0) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (pythonThread != null) { + Log.v("python service", "service exists, do not start again"); + return startType(); + } + //intent is null if OS restarts a STICKY service + if (intent == null) { + Context context = getApplicationContext(); + intent = getThisDefaultIntent(context, ""); + } + + startIntent = intent; + Bundle extras = intent.getExtras(); + androidPrivate = extras.getString("androidPrivate"); + androidArgument = extras.getString("androidArgument"); + serviceEntrypoint = extras.getString("serviceEntrypoint"); + pythonName = extras.getString("pythonName"); + pythonHome = extras.getString("pythonHome"); + pythonPath = extras.getString("pythonPath"); + boolean serviceStartAsForeground = ( + extras.getString("serviceStartAsForeground").equals("true") + ); + pythonServiceArgument = extras.getString("pythonServiceArgument"); + pythonThread = new Thread(this); + pythonThread.start(); + + if (serviceStartAsForeground) { + doStartForeground(extras); + } + + return startType(); + } + + protected int getServiceId() { + return 1; + } + + protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { + return null; + } + + protected void doStartForeground(Bundle extras) { + String serviceTitle = extras.getString("serviceTitle"); + String serviceDescription = extras.getString("serviceDescription"); + Notification notification; + Context context = getApplicationContext(); + Intent contextIntent = new Intent(context, PythonActivity.class); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + notification = new Notification( + context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis()); + try { + // prevent using NotificationCompat, this saves 100kb on apk + Method func = notification.getClass().getMethod( + "setLatestEventInfo", Context.class, CharSequence.class, + CharSequence.class, PendingIntent.class); + func.invoke(notification, context, serviceTitle, serviceDescription, pIntent); + } catch (NoSuchMethodException | IllegalAccessException | + IllegalArgumentException | InvocationTargetException e) { + } + } else { + // for android 8+ we need to create our own channel + // https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1 + String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a"; //TODO: make this configurable + String channelName = "Background Service"; //TODO: make this configurable + NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, + NotificationManager.IMPORTANCE_NONE); + + chan.setLightColor(Color.BLUE); + chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(chan); + + Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); + builder.setContentTitle("Sideband"); + builder.setContentText("Reticulum Running"); + builder.setContentIntent(pIntent); + + Bitmap icon_bitmap = BitmapFactory.decodeFile("/data/user/0/io.unsigned.sideband/files/app/assets/notification_icon.png"); + Icon service_icon = Icon.createWithBitmap(icon_bitmap); + // builder.setSmallIcon(context.getApplicationInfo().icon); + builder.setSmallIcon(service_icon); + notification = builder.build(); + + // Log.v("python service", "Testing WM locks..."); + // WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE); + // MulticastLock ml = wm.createMulticastLock("sometag"); + // Log.v("python service", "WM locks done"); + + + } + startForeground(getServiceId(), notification); + } + + @Override + public void onDestroy() { + super.onDestroy(); + pythonThread = null; + if (autoRestartService && startIntent != null) { + Log.v("python service", "service restart requested"); + startService(startIntent); + } + Process.killProcess(Process.myPid()); + } + + /** + * Stops the task gracefully when killed. + * Calling stopSelf() will trigger a onDestroy() call from the system. + */ + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + //sticky servcie runtime/restart is managed by the OS. leave it running when app is closed + if (startType() != START_STICKY) { + stopSelf(); + } + } + + @Override + public void run(){ + String app_root = getFilesDir().getAbsolutePath() + "/app"; + File app_root_file = new File(app_root); + PythonUtil.loadLibraries(app_root_file, + new File(getApplicationInfo().nativeLibraryDir)); + this.mService = this; + nativeStart( + androidPrivate, androidArgument, + serviceEntrypoint, pythonName, + pythonHome, pythonPath, + pythonServiceArgument); + stopSelf(); + } + + // Native part + public static native void nativeStart( + String androidPrivate, String androidArgument, + String serviceEntrypoint, String pythonName, + String pythonHome, String pythonPath, + String pythonServiceArgument); +} diff --git a/sbapp/services/sidebandservice.py b/sbapp/services/sidebandservice.py index 5073f0d..a048d21 100644 --- a/sbapp/services/sidebandservice.py +++ b/sbapp/services/sidebandservice.py @@ -1,14 +1,44 @@ import time +import RNS +from sideband.core import SidebandCore +from os import environ +from jnius import autoclass, cast + +Context = autoclass('android.content.Context') class sidebandservice(): def __init__(self): + self.argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') + self.multicast_lock = None + self.wake_lock = None + + self.service = autoclass('org.kivy.android.PythonService').mService + self.app_context = self.service.getApplication().getApplicationContext() + self.wifi_manager = self.app_context.getSystemService(Context.WIFI_SERVICE) + # The returned instance is an android.net.wifi.WifiManager + print("Sideband Service created") + self.take_locks() self.run() + def take_locks(self): + if self.multicast_lock == None: + self.multicast_lock = self.wifi_manager.createMulticastLock("sideband_service") + + if not self.multicast_lock.isHeld(): + RNS.log("Taking multicast lock") + self.multicast_lock.acquire() + RNS.log("Took lock") + + + def release_locks(): + if not self.multicast_lock == None and self.multicast_lock.isHeld(): + self.multicast_lock.release() + def run(self): while True: print("Service ping") - time.sleep(3) + time.sleep(5) sbs = sidebandservice() \ No newline at end of file diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 0f7f602..767d98e 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -705,7 +705,8 @@ class SidebandCore(): self.lxmf_destination.announce() def __start_jobs_immediate(self): - self.reticulum = RNS.Reticulum(configdir=self.rns_configdir) + # TODO: Reset loglevel + self.reticulum = RNS.Reticulum(configdir=self.rns_configdir, loglevel=7) RNS.log("Reticulum started, activating LXMF...") if RNS.vendor.platformutils.get_platform() == "android":