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..2bf37573d6 --- /dev/null +++ b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonService.java @@ -0,0 +1,255 @@ +package org.kivy.android; + +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 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 + 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.isEmpty()) { + 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) { + // ignored + } + } else { + // for android 8+ we need to create our own channel + 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); +} \ No newline at end of file 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 %}