Reputation: 327
I want to my service continue working after closing the app, but I can't do it. I heard I should use startForeground()
but how to do it in python? Code of app:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from jnius import autoclass
from kivy.uix.label import Label
class MyApp(App):
def build(self):
fl = FloatLayout()
try:
service = autoclass('org.test.myapp.ServiceMyservice')
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
service.start(mActivity, "")
except Exception as error:
fl.add_widget(Label(text=str(error), font_size=(40)))
return fl
if __name__ == '__main__':
MyApp().run()
Code of my service/main.py
:
import pickle, socket, jnius
for x in range(5):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = 'example-78945.portmap.host'
port = 78945
s.connect((host,port))
s.send(('hello world').encode('utf-8'))
Code of ServiceMyservice.java
:
package org.test.myapp.ServiceMyservice;
import android.content.Intent;
import android.content.Context;
import org.kivy.android.PythonService;
import android.app.Notification;
import android.app.Service;
public class ServiceMyservice extends PythonService {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
protected int getServiceId() {
return 1;
}
static public void start(Context ctx, String pythonServiceArgument) {
Intent intent = new Intent(ctx, ServiceMyservice.class);
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
intent.putExtra("androidArgument", argument);
intent.putExtra("serviceTitle", "My Application");
intent.putExtra("serviceDescription", "Myservice");
intent.putExtra("serviceEntrypoint", "./service/main.py");
intent.putExtra("pythonName", "myservice");
intent.putExtra("serviceStartAsForeground", true);
intent.putExtra("pythonHome", argument);
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
ctx.startService(intent);
}
static public void stop(Context ctx) {
Intent intent = new Intent(ctx, ServiceMyservice.class);
ctx.stopService(intent);
}
}
Service starts and works, but after closing the app, service closes too. How to fix it????
Upvotes: 3
Views: 1952
Reputation: 21
@omdo: Thanks for your answer.
your approach of adding permission works for me. Though I know the solution is add permission of FOREGROUND_SERVICE, I tried to add:
from android.permissions import request_permissions, Permission
request_permissions([Permission.FOREGROUND_SERVICE])
but doesn't work, until I revised buildozer.spec with your suggestion.
Upvotes: 0
Reputation: 161
This workaround is basically making the service got an auto restart. That means your service will start from beginning. And yes, this is hardcoding.
Add a string argument to start() method in Service template file
mine was restart argument. This will be an extra for the activity intent to pass to onStartCommand() method that triggered by ctx.startService() method. Then put 'autoRestartService' with that restart argument value.
My .buildozer/android/platform/build-<your arch>/dists/<your app>/templates/Service.tmpl.java:
package {{ args.package }};
import android.content.Intent;
import android.content.Context;
import org.kivy.android.PythonService;
public class Service{{ name|capitalize }} extends PythonService {
{% if sticky %}
@Override
public int startType() {
return START_STICKY;
}
{% endif %}
@Override
protected int getServiceId() {
return {{ service_id }};
}
/*add 'restart' String argument to the start() method*/
static public void start(Context ctx, String pythonServiceArgument, String restart) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
intent.putExtra("androidArgument", argument);
intent.putExtra("serviceTitle", "{{ args.name }}");
intent.putExtra("serviceDescription", "{{ name|capitalize }}");
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
intent.putExtra("pythonName", "{{ name }}");
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
intent.putExtra("pythonHome", argument);
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
intent.putExtra("autoRestartService", restart); /*<-- add this line*/
ctx.startService(intent);
}
static public void stop(Context ctx) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
ctx.stopService(intent);
}
}
Set the autoRestartService value within PythonService's onStartCommand()
Take a look at PythonService's onDestroy() method below. onDestroy() method will be triggered if the service is killed (caused by closing the app or swiping from recent app). There is an option wether restart the service or not depends on autoRestartService value. So set it within the onStartCommand() method by getting it from intent extras.
My .buildozer/android/platform/build-<your arch>/dists/<your app>/src/main/org/kivy/android/PythonService.java:
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;
public class PythonService extends Service implements Runnable {
// 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 setAutoRestartService(boolean restart) {
autoRestartService = restart;
}
public int startType() {
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (pythonThread != null) {
Log.v("python service", "service exists, do not start again");
return START_NOT_STICKY;
}
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");
autoRestartService = (
extras.getString("autoRestartService").equals("true") //this will return boolean for autoRestartservice
);
pythonThread = new Thread(this);
pythonThread.start();
if (serviceStartAsForeground) {
doStartForeground(extras);
}
return startType();
}
protected int getServiceId() {
return 1;
}
protected void doStartForeground(Bundle extras) {
String serviceTitle = extras.getString("serviceTitle");
String serviceDescription = extras.getString("serviceDescription");
Notification notification;
Context context = getApplicationContext();
Intent contextIntent = new Intent(context, PythonActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
notification = new Notification(
context.getApplicationInfo().icon, 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, serviceTitle, serviceDescription, 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"; //TODO: make this configurable
String channelName = "Background Service"; //TODO: make this configurable
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(serviceTitle);
builder.setContentText(serviceDescription);
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground(getServiceId(), notification);
}
@Override
public void onDestroy() {
super.onDestroy();
pythonThread = null;
if (autoRestartService && startIntent != null) {
Log.v("python service", "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);
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;
nativeStart(
androidPrivate, androidArgument,
serviceEntrypoint, pythonName,
pythonHome, pythonPath,
pythonServiceArgument);
stopSelf();
}
// Native part
public static native void nativeStart(
String androidPrivate, String androidArgument,
String serviceEntrypoint, String pythonName,
String pythonHome, String pythonPath,
String pythonServiceArgument);
}
there is setAutoRestartService() method there, but we cant call it because it is non-static method.
Last thing, buildozer.spec
Add FOREGROUND_SERVICE permission and service to buildozer.spec.
android.permissions = FOREGROUND_SERVICE
...
services = myservice:./path/to/your-service.py:foreground
Now start the service by giving 'true' string as third positional argument.
activity = autoclass('org.kivy.android.PythonActivity').mActivity
service = autoclass('com.omdo.example.ServiceMyservice')
service.start(activity, '', 'true')
Note: I don't really understands Java, maybe someone can make it simpler.
Reference:
Upvotes: 3