如何在Python Kivy上实现Android应用程序关闭后服务仍然继续运行

3

我希望我的服务在关闭应用程序后继续运行,但是我不知道如何实现。我听说应该使用startForeground(),但是如何在Python中实现呢?应用程序代码:

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()

我的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'))

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);
    }
}

服务已经启动并正常工作,但在关闭应用程序后,服务也会关闭。如何修复?
2个回答

3
这个解决方法基本上是使服务自动重启。这意味着您的服务将从头开始运行。是的,这是硬编码。
在Service模板文件的start()方法中添加一个字符串参数。
我的是“restart”参数。这将成为活动意图的额外参数,传递给由ctx.startService()方法触发的onStartCommand()方法。然后将“autoRestartService”与该restart参数值一起使用。
我的.buildozer/android/platform/build-/dists//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);
    }
}

在PythonService的onStartCommand()方法中设置autoRestartService值

请查看下面的PythonService的onDestroy()方法。如果服务被关闭(由于关闭应用程序或从最近的应用程序中滑动),将触发onDestroy()方法。根据autoRestartService值,有一个选项是否重新启动服务。因此,在onStartCommand()方法中通过从意图附加项获取它来设置它。

我的.buildozer/android/platform/build-/dists//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://dev59.com/7VYN5IYBdhLWcg3wXnP7
            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);
}

这里有一个setAutoRestartService()方法,但我们无法调用它,因为它是非静态方法。

最后一件事,buildozer.spec

在buildozer.spec中添加FOREGROUND_SERVICE权限和服务。

android.permissions = FOREGROUND_SERVICE
...

services = myservice:./path/to/your-service.py:foreground

现在通过将“true”字符串作为第三个位置参数来启动服务。
activity = autoclass('org.kivy.android.PythonActivity').mActivity
service = autoclass('com.omdo.example.ServiceMyservice')
service.start(activity, '', 'true')

注意: 我并不真正理解Java,希望有人能让它更简单易懂。

参考:


也许解释一下您提供的代码会更有帮助。附上参考链接+1。 - abhivemp
@abhivemp 忘了提到,这个答案已经更新了。 - omdo
谢谢,这对我有用,但我遇到了一个问题,可能你已经解决了。它与服务通知点击有关。如果应用程序仍在运行,则返回焦点到应用程序。另一种情况是,如果我通过从最近的应用程序列表中滑动它来关闭应用程序,则服务将重新启动,并且当我单击其通知时,只会显示黑屏,而不是打开应用程序。在doStartForeground()方法中,我尝试了contextIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);但它没有帮助。我该如何实现这个? - volion80

0

@omdo: 感谢您的回答。

您添加权限的方法对我很有用。 虽然我知道解决方案是添加 FOREGROUND_SERVICE 权限, 但我还是尝试了添加:

from android.permissions import request_permissions, Permission

request_permissions([Permission.FOREGROUND_SERVICE])

但是直到我按照你的建议修改了buildozer.spec文件,它才能正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接