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..6470470d43
--- /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;
+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 %}