From 58e840c6e053c7a5209ff25b27fa4a9fc0dd69f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86?= Date: Fri, 5 Dec 2025 23:12:15 +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 8ccff2027a..b2331d32fc 100644 --- a/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml +++ b/pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml @@ -93,7 +93,11 @@ {% endif %} {% for name in service_names %} + android:process=":service_{{ name }}" + android:exported="true"> + + {% endfor %} {% for name in native_services %} From a0dd5a20733f62ce5c166fe1a2a5a9b600277110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86?= Date: Sat, 13 Dec 2025 10:39:36 +0330 Subject: [PATCH 2/3] Fixing the CI repoted error 'Imports not contiguous' --- .../qt/build/src/main/java/org/kivy/android/PythonService.java | 3 --- 1 file changed, 3 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..88e070970b 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 { From e8e9b433602753076eb8d68ae550dd938e8789af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=A7=D8=B4=DA=A9=D8=A7=D9=86?= Date: Sat, 13 Dec 2025 16:41:36 +0330 Subject: [PATCH 3/3] Fix error: ':spotlessJavaCheck' --- .../java/org/kivy/android/PythonService.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 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 88e070970b..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 @@ -74,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, ""); @@ -121,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 { @@ -169,6 +171,7 @@ protected void doStartForeground(Bundle extras) { builder.setSmallIcon(smallIconId); notification = builder.build(); } + startForeground(getServiceId(), notification); }