Search code examples
androidpythonandroid-servicekivy

Kivy service stops when app is closed


I'm starting a service from my Kivy app:

service = autoclass('net.saband.myapp.ServiceMyservice')
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
service.start(mActivity, '')

It works. And if I close my app using the back button my service still works. But if I close my app by swiping it out from the recent apps list the service dies.

I've found this issue and tried to use startForeground guided by this article. It works but the notification is not removable so this solution doesn't suit me.

I've read this question and it looks like I could be helped using START_STICKY... but it's kivy service so how can I implement it? I've tried to edit Service.tmpl.java in my python-for-android templates and change this:

public class Service{{ name|capitalize }} extends PythonService {
    {% if sticky %}
    @Override
    public int startType() {
        return START_STICKY;
    }
    {% endif %}
...

to this:

public class Service{{ name|capitalize }} extends PythonService {

    @Override
    public int startType() {
        return START_STICKY;
    }

(Yes, I understand that {% if sticky %} means that I can set it somewhere but in the official docs there is not a word about it.)

But nothing changed, the service still dies. According the logs the restart is scheduled:

11-17 22:52:07.140  1496  1511 I ActivityManager: Killing 29431:net.saband.myapp/u0a122 (adj 9): remove task
11-17 22:52:07.219  1496  3404 I WindowState: WIN DEATH: Window{3c605b3 u0 net.saband.myapp/org.kivy.android.PythonActivity}
11-17 22:52:07.220  1496  3404 W WindowManager: Force-removing child win Window{5ed4ff u0 SurfaceView} from container Window{3c605b3 u0 net.saband.myapp/org.kivy.android.PythonActivity}
11-17 22:52:07.225  1496  2871 W WindowManager: Failed looking up window
11-17 22:52:07.225  1496  2871 W WindowManager: java.lang.IllegalArgumentException: Requested window android.os.BinderProxy@c7f2770 does not exist
11-17 22:52:07.225  1496  2871 W WindowManager:     at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:8821)
11-17 22:52:07.225  1496  2871 W WindowManager:     at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:8812)
11-17 22:52:07.225  1496  2871 W WindowManager:     at com.android.server.wm.WindowState$DeathRecipient.binderDied(WindowState.java:1212)
11-17 22:52:07.225  1496  2871 W WindowManager:     at android.os.BinderProxy.sendDeathNotice(Binder.java:558)
11-17 22:52:07.225  1496  2871 I WindowState: WIN DEATH: null
11-17 22:52:07.247  1496  3311 D ActivityManager: cleanUpApplicationRecord -- 29431
11-17 22:52:07.250  1496  3538 I ActivityManager: Killing 29366:net.saband.myapp:service_myservice/u0a122 (adj 8): remove task
11-17 22:52:07.304  1496  3557 D ActivityManager: cleanUpApplicationRecord -- 29366
11-17 22:52:07.305  1496  3557 W ActivityManager: Scheduling restart of crashed service net.saband.myapp/.ServiceMyservice in 1000ms

But nothing happens.

I need the service to continue to work even when the app is closed by swiping it out from the recent apps list. And I need removable notifications. That's all. Many apps can do it. But is there a way to do it with Kivy and python-for-android?

Thanks.


Solution

  • I did it. But this required changing the java code and the solution is hardcoded. It's strange and unpleasantly that python-for-android developers didn't foresee this.

    Well, the solution.

    Open file .buildozer/android/platform/build/dists/myapp/src/org/kivy/android/PythonService.java. In function startType() change START_NOT_STICKY to START_STICKY:

    public int startType() {
        return START_STICKY;
    }
    

    Now the service will be restarted. But this is not enough because after restart in function onStartCommand(Intent intent, int flags, int startId) intent will be null so we will get an error:

    E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.os.Bundle android.content.Intent.getExtras()' on a null object reference
    

    So we need to add the if statement in this function:

    @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;
        }
        if (intent != null) { 
            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");
            pythonServiceArgument = extras.getString("pythonServiceArgument");
    
            pythonThread = new Thread(this);
            pythonThread.start();
    
            if (canDisplayNotification()) {
                doStartForeground(extras);
            }
        } else {
            pythonThread = new Thread(this);
            pythonThread.start();
        }
    
        return startType();
    }
    

    But this is steel not enough because now we have another error in nativeStart function call because there are no extras:

    F DEBUG   : Abort message: 'art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: GetStringUTFChars received NULL jstring'
    

    So I've added the null check and some default values (2 of them is hardcoded) to run() function:

    @Override
    public void run(){
        String package_root = getFilesDir().getAbsolutePath();
        String app_root =  package_root + "/app";
        File app_root_file = new File(app_root);
        PythonUtil.loadLibraries(app_root_file);
        this.mService = this;
    
        if (androidPrivate == null) {
            androidPrivate = package_root;
        }
        if (androidArgument == null) {
            androidArgument = app_root;
        }
        if (serviceEntrypoint == null) {
            serviceEntrypoint = "./service/main.py"; // hardcoded
        }
        if (pythonName == null) {
            pythonName = "myservice"; // hardcoded
        }
        if (pythonHome == null) {
            pythonHome = app_root;
        }
        if (pythonPath == null) {
            pythonPath = package_root;
        }
        if (pythonServiceArgument == null) {
            pythonServiceArgument = app_root+":"+app_root+"/lib";
        }
    
        nativeStart(
            androidPrivate, androidArgument,
            serviceEntrypoint, pythonName,
            pythonHome, pythonPath,
            pythonServiceArgument);
        stopSelf();
    }
    

    Now it works.