From 13a4d9c2806d1fade3b152b94480a099811e31af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86=20=D9=88=D9=84=DB=8C?= =?UTF-8?q?=E2=80=8C=D8=B2=D8=A7=D8=AF=D9=87?= Date: Fri, 13 Feb 2026 21:46:03 +0330 Subject: [PATCH 1/3] Fix android service containing PySide6 codes crashes the service due to Qt preparations not done and Qt libs not loaded --- .../java/org/kivy/android/PythonService.java | 231 ++++++++++++++++++ .../build/templates/AndroidManifest.tmpl.xml | 6 +- 2 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java diff --git a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java new file mode 100644 index 0000000000..8cb20bd3e5 --- /dev/null +++ b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java @@ -0,0 +1,231 @@ +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 org.qtproject.qt.android.bindings.QtService; + +public class PythonService extends QtService implements Runnable { + private static final String TAG = "PythonQtService"; + + // 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 setEnvironmentVariable(String key, String value) { + /** + * Sets an environment variable based on key/value. + **/ + try { + android.system.Os.setenv(key, value, true); + } catch (Exception e) { + Log.e(TAG, "Unable set environment variable:" + key + "=" + value); + e.printStackTrace(); + } + } + + 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() { + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (pythonThread != null) { + Log.v(TAG, "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 smallIconName = extras.getString("smallIconName"); + String contentTitle = extras.getString("contentTitle"); + String contentText = extras.getString("contentText"); + Notification notification; + Context context = getApplicationContext(); + Intent contextIntent = new Intent(context, PythonActivity.class); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + + // Unspecified icon uses default. + int smallIconId = context.getApplicationInfo().icon; + if (smallIconName != null) { + if (!smallIconName.equals("")){ + int resId = getResources().getIdentifier(smallIconName, "mipmap", + getPackageName()); + if (resId ==0) { + resId = getResources().getIdentifier(smallIconName, "drawable", + getPackageName()); + } + if (resId !=0) { + smallIconId = resId; + } + } + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // This constructor is deprecated + notification = new Notification( + smallIconId, 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, contentTitle, contentText, 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" + getServiceId(); + String channelName = "Background Service" + getServiceId(); + 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(contentTitle); + builder.setContentText(contentText); + builder.setContentIntent(pIntent); + builder.setSmallIcon(smallIconId); + notification = builder.build(); + } + startForeground(getServiceId(), notification); + } + + @Override + public void onDestroy() { + super.onDestroy(); + pythonThread = null; + if (autoRestartService && startIntent != null) { + Log.v(TAG, "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 service 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; + + Log.v(TAG, "Setting env vars for start.c and Python to use"); + setEnvironmentVariable("ANDROID_ENTRYPOINT", app_root + "/" + serviceEntrypoint); + setEnvironmentVariable("ANDROID_ARGUMENT", app_root); + setEnvironmentVariable("ANDROID_APP_PATH", app_root); + setEnvironmentVariable("ANDROID_PRIVATE", androidPrivate); + setEnvironmentVariable("ANDROID_UNPACK", app_root); + setEnvironmentVariable("PYTHONHOME", pythonHome); + setEnvironmentVariable("PYTHONPATH", pythonPath + ":" + app_root + ":" + app_root + "/lib"); + setEnvironmentVariable("PYTHONOPTIMIZE", "2"); + + super.onCreate(); + + 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/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml index 1385bdbd03..dd2da647c3 100644 --- a/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml +++ b/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml @@ -96,7 +96,11 @@ {% if foreground_type %} android:foregroundServiceType="{{ foreground_type }}" {% endif %} - android:process=":service_{{ name }}" /> + android:process=":service_{{ name }}" + android:exported="true"> + + {% endfor %} {% for name in native_services %} From e503e81dd8c018055f2865c1b0c3ef814b9bb2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86=20=D9=88=D9=84=DB=8C?= =?UTF-8?q?=E2=80=8C=D8=B2=D8=A7=D8=AF=D9=87?= Date: Fri, 13 Feb 2026 21:50:14 +0330 Subject: [PATCH 2/3] Fixing the CI repoted error 'Imports not contiguous' - Fix error: ':spotlessJavaCheck' --- .../java/org/kivy/android/PythonService.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java index 8cb20bd3e5..6470470d43 100644 --- a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java +++ b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java @@ -13,12 +13,9 @@ 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 org.qtproject.qt.android.bindings.QtService; public class PythonService extends QtService implements Runnable { @@ -77,7 +74,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { Log.v(TAG, "service exists, do not start again"); return startType(); } - //intent is null if OS restarts a STICKY service + + //intent is null if OS restarts a STICKY service if (intent == null) { Context context = getApplicationContext(); intent = getThisDefaultIntent(context, ""); @@ -124,24 +122,25 @@ protected void doStartForeground(Bundle extras) { PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); - // Unspecified icon uses default. - int smallIconId = context.getApplicationInfo().icon; - if (smallIconName != null) { - if (!smallIconName.equals("")){ - int resId = getResources().getIdentifier(smallIconName, "mipmap", - getPackageName()); - if (resId ==0) { - resId = getResources().getIdentifier(smallIconName, "drawable", - getPackageName()); - } - if (resId !=0) { - smallIconId = resId; + // Unspecified icon uses default. + int smallIconId = context.getApplicationInfo().icon; + + if (smallIconName != null) { + if (!smallIconName.equals("")){ + int resId = getResources().getIdentifier(smallIconName, "mipmap", + getPackageName()); + if (resId ==0) { + resId = getResources().getIdentifier(smallIconName, "drawable", + getPackageName()); + } + if (resId !=0) { + smallIconId = resId; + } } } - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - // This constructor is deprecated + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // This constructor is deprecated notification = new Notification( smallIconId, serviceTitle, System.currentTimeMillis()); try { @@ -172,6 +171,7 @@ protected void doStartForeground(Bundle extras) { builder.setSmallIcon(smallIconId); notification = builder.build(); } + startForeground(getServiceId(), notification); } From 28d482155de72d98827406b11bf27a18bd75fd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86=20=D9=88=D9=84=DB=8C?= =?UTF-8?q?=E2=80=8C=D8=B2=D8=A7=D8=AF=D9=87?= Date: Sat, 14 Feb 2026 18:41:09 +0330 Subject: [PATCH 3/3] change the jave service code so it passes spotless checks --- .../java/org/kivy/android/PythonService.java | 128 +++++++++++------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java index 6470470d43..2bf37573d6 100644 --- a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java +++ b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java @@ -1,24 +1,27 @@ 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.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; import android.os.Process; +import android.util.Log; + import java.io.File; -import android.app.NotificationManager; -import android.app.NotificationChannel; -import android.graphics.Color; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import org.qtproject.qt.android.bindings.QtService; public class PythonService extends QtService implements Runnable { + private static final String TAG = "PythonQtService"; // Thread for Python code @@ -31,7 +34,7 @@ public class PythonService extends QtService implements Runnable { private String pythonHome; private String pythonPath; private String serviceEntrypoint; - // Argument to pass to Python code, + // Argument to pass to Python code private String pythonServiceArgument; public static PythonService mService = null; @@ -42,7 +45,7 @@ public class PythonService extends QtService implements Runnable { public void setEnvironmentVariable(String key, String value) { /** * Sets an environment variable based on key/value. - **/ + */ try { android.system.Os.setenv(key, value, true); } catch (Exception e) { @@ -74,8 +77,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { Log.v(TAG, "service exists, do not start again"); return startType(); } - - //intent is null if OS restarts a STICKY service + + // intent is null if OS restarts a STICKY service if (intent == null) { Context context = getApplicationContext(); intent = getThisDefaultIntent(context, ""); @@ -89,10 +92,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); - boolean serviceStartAsForeground = ( - extras.getString("serviceStartAsForeground").equals("true") - ); + boolean serviceStartAsForeground = + extras.getString("serviceStartAsForeground").equals("true"); pythonServiceArgument = extras.getString("pythonServiceArgument"); + pythonThread = new Thread(this); pythonThread.start(); @@ -116,52 +119,69 @@ protected void doStartForeground(Bundle extras) { String smallIconName = extras.getString("smallIconName"); String contentTitle = extras.getString("contentTitle"); String contentText = extras.getString("contentText"); + Notification notification; Context context = getApplicationContext(); Intent contextIntent = new Intent(context, PythonActivity.class); - PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pIntent = + PendingIntent.getActivity( + context, + 0, + contextIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); // Unspecified icon uses default. int smallIconId = context.getApplicationInfo().icon; - + if (smallIconName != null) { - if (!smallIconName.equals("")){ - int resId = getResources().getIdentifier(smallIconName, "mipmap", - getPackageName()); - if (resId ==0) { - resId = getResources().getIdentifier(smallIconName, "drawable", - getPackageName()); + if (!smallIconName.isEmpty()) { + int resId = + getResources() + .getIdentifier(smallIconName, "mipmap", getPackageName()); + if (resId == 0) { + resId = + getResources() + .getIdentifier(smallIconName, "drawable", getPackageName()); } - if (resId !=0) { - smallIconId = resId; + if (resId != 0) { + smallIconId = resId; } } } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // This constructor is deprecated - notification = new Notification( - smallIconId, serviceTitle, System.currentTimeMillis()); + notification = new Notification(smallIconId, 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); + Method func = + notification + .getClass() + .getMethod( + "setLatestEventInfo", + Context.class, + CharSequence.class, + CharSequence.class, + PendingIntent.class); func.invoke(notification, context, contentTitle, contentText, pIntent); - } catch (NoSuchMethodException | IllegalAccessException | - IllegalArgumentException | InvocationTargetException e) { + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + // ignored } } 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" + getServiceId(); String channelName = "Background Service" + getServiceId(); - NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE); + 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); + NotificationManager manager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.createNotificationChannel(chan); Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); @@ -187,24 +207,25 @@ public void onDestroy() { } /** - * Stops the task gracefully when killed. - * Calling stopSelf() will trigger a onDestroy() call from the system. + * 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 service runtime/restart is managed by the OS. leave it running when app is closed + // sticky service 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"; + 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)); + + PythonUtil.loadLibraries( + app_root_file, new File(getApplicationInfo().nativeLibraryDir)); this.mService = this; Log.v(TAG, "Setting env vars for start.c and Python to use"); @@ -224,8 +245,11 @@ public void run(){ // Native part public static native void nativeStart( - String androidPrivate, String androidArgument, - String serviceEntrypoint, String pythonName, - String pythonHome, String pythonPath, + String androidPrivate, + String androidArgument, + String serviceEntrypoint, + String pythonName, + String pythonHome, + String pythonPath, String pythonServiceArgument); -} +} \ No newline at end of file