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 %}