Quantcast
Channel: AFPy's Planet
Viewing all articles
Browse latest Browse all 3409

[tshirtman] Building a background application on android with Kivy.

$
0
0

Kivy runs on android using the python-for-android project, which support android services. This works in a simple way, you basically bundle two main.py in your application, one for the UI, one for the service. The UI can start the services on start. From that point though, things may be a little uneasy. Why? Because you have two applications, and now you have to make them talk to each over if you want to do anything useful.

Android’s way of having an Activity and a Service talk to each other is Broadcast signals, which can be limited to part of your applications, howether, it’s not straightforward to use them with pyjnius, which is the magical interface we use to use java code from python.

Another way is to use network, i’ve been doing it with twisted in the past, setting a twisted server in the Service, and using twisted as a client in the Activity, howether, i find this to be heavy lifting for the trivial task of communicating between two programs on the same device. And including Twisted in your app, certainly add some weight to it.

Howether, in order to support TUIO, kivy ships with a simple OSC implementation, OSC is a simple connectionless network protocol, that allow you to pack messages, and send them to an ip/port/api URI, turns out we don’t need anything more, and a connectionless protocol avoid us dealing with disconnections (like the UI being closed, or the service not being started yet) that could give us some headaches (and certainly gave me some). We just need to have both part of the program listen for OSC messages, and have them send data to each other, if confirmation is needed, it’s possible to have a messages been sent back on a specific api.

So let’s get started.

getting a minimal kivy app

The usual things, let’s put a simple UI with a Button.

from kivy.app import App
from kivy.lang import Builder

kv = '''
Button:
    text: 'push me!'
'''

class ServiceApp(App):
    def build(self):
        return Builder.load_string(kv)

if __name__ == '__main__':
    ServiceApp().run()

here we just load a kv string that defines a button, and return the result, nothing fancy.

getting a minimal service

from time import sleep

if __name__ == '__main__':
    while True:
        sleep(.1)

Yeah, nothing much needed, actually, the sleep isn’t even needed, but the program have to run, and we’ll need this loop anyway.

starting the service

For your service to run, you need to tell your UI to start it.

from kivy.app import App
from kivy.lang import Builder
from kivy.utils import platform

kv = '''
Button:
    text: 'push me!'
'''

class ServiceApp(App):
    def build(self):
        if platform == 'android':
            from android import AndroidService
            service = AndroidService('my pong service', 'running')
            service.start('service started')
            self.service = service

        return Builder.load_string(kv)

if __name__ == '__main__':
    ServiceApp().run()

We make the test for android, so you can still test your app on desktop, by starting manually both parts.

Packaging them for android

both files must be named main.py, the UI one is at the root of the project, the other one is in a service directory directly under the root of the project.

├── main.py
└── service
    └── main.py

To package, i’ll be using buildozer.

As we are using network don’t forget to add the NETWORK permission when editing buildozer.spec

buildozer init
editor buildozer.spec
buildozer android debug deploy run logcat

After some time, you should see the (not very exciting) app start on your plugged android device.

setting up OSC

Whatever the side, OSC needs a port to listen on, and to have functions to call when things happen. The basic setup is simple.

from kivy.lib import osc

def some_api_callback(message, *args):
   print("got a message! %s" % message)

osc.init()
oscid = osc.listen(ipAddr='0.0.0.0', port=someport)
osc.bind(oscid, some_api_callback, '/some_api')

and then

osc.readQueue(oscid)

needs to be called regularly.

for the service, we’ll just put this call in the loop:

from time import sleep
from kivy.lib import osc

service = 3000

def some_api_callback(message, *args):
   print("got a message! %s" % message)

if __name__ == '__main__':
    osc.init()
    oscid = osc.listen(ipAddr='127.0.0.1', port=service)
    osc.bind(oscid, some_api_callback, '/some_api')

    while True:
        osc.readQueue(oscid)
        sleep(.1)

And for UI, we’ll use kivy.clock.Clock’s schedule_interval method.

from kivy.app import App
from kivy.lang import Builder
from kivy.lib import osc
from kivy.utils import platform
from kivy.clock import Clock

activityport = 3001

def some_api_callback(message, *args):
   print("got a message! %s" % message)

kv = '''
Button:
    text: 'push me!'
'''

class ServiceApp(App):
    def build(self):
        if platform == 'android':
            from android import AndroidService
            service = AndroidService('my pong service', 'running')
            service.start('service started')
            self.service = service

        osc.init()
        oscid = osc.listen(ipAddr='127.0.0.1', port=activityport)
        osc.bind(oscid, some_api_callback, '/some_api')
        Clock.schedule_interval(lambda *x: osc.readQueue(oscid), 0)

        return Builder.load_string(kv)

if __name__ == '__main__':
    ServiceApp().run()

Now, both sides can receive messages, that’s a good first step, but nothing will really happen, since none of them send any message.

sending messagse

The osc api to send message is very simple:

osc.sendMsg(api, data_list, port=someport, ipAddr=someaddress)

now, ipAddr is by default localhost, which is fine for us, so we only need to find the api we want to hit, the data list, and the port, which will be the one the other side listens on.

Let’s make our button send a message to the Service.

from kivy.app import App
from kivy.lang import Builder
from kivy.lib import osc
from kivy.clock import Clock

activityport = 3001
serviceport = 3000

def some_api_callback(message, *args):
   print("got a message! %s" % message)

kv = '''
Button:
    text: 'push me!'
    on_press: app.ping()
'''

class ServiceApp(App):
    def build(self):
        if platform == 'android':
            from android import AndroidService
            service = AndroidService('my pong service', 'running')
            service.start('service started')
            self.service = service

        osc.init()
        oscid = osc.listen(ipAddr='127.0.0.1', port=activityport)
        osc.bind(oscid, some_api_callback, '/some_api')
        Clock.schedule_interval(lambda *x: osc.readQueue(oscid), 0)

        return Builder.load_string(kv)

    def ping(self):
        osc.sendMsg('/some_api', ['ping', ], port=someotherport)


if __name__ == '__main__':
    ServiceApp().run()

Yes, at that point, you can run it, and see that when you press the button, your adb logcat on android, yay!

Now, let’s make our service answer, and our UI display the answer!

from time import sleep
from kivy.lib import osc

serviceport = 3000
activityport = 3001

def some_api_callback(message, *args):
    print("got a message! %s" % message)
    answer_message()

def answer_message():
    osc.sendMsg('/some_api', [asctime(localtime()), ], port=activityport)

if __name__ == '__main__':
    osc.init()
    oscid = osc.listen(ipAddr='127.0.0.1', port=serviceport)
    osc.bind(oscid, some_api_callback, '/some_api')

    while True:
        osc.readQueue(oscid)
        sleep(.1)

and for the UI to answer:

from kivy.app import App
from kivy.lang import Builder
from kivy.lib import osc
from kivy.utils import platform
from kivy.clock import Clock

activityport = 3001
serviceport = 3000

kv = '''
Button:
    text: 'push me!'
    on_press: app.ping()
'''

class ServiceApp(App):
    def build(self):
        if platform == 'android':
            from android import AndroidService
            service = AndroidService('my pong service', 'running')
            service.start('service started')
            self.service = service

        osc.init()
        oscid = osc.listen(ipAddr='127.0.0.1', port=activityport)
        osc.bind(oscid, some_api_callback, '/some_api')
        Clock.schedule_interval(lambda *x: osc.readQueue(oscid), 0)

        return Builder.load_string(kv)

    def ping(self):
        osc.sendMsg('/some_api', ['ping', ], port=someotherport)

    def some_api_callback(self, message, *args):
        print("got a message! %s" % message)
        self.root.text += '\n%s' % message[2]

if __name__ == '__main__':
    ServiceApp().run()

The only thing a bit confusing here is that the real message is in message[2], don’t ask me why, it’s probably explained in some documenation i didn’t care enough to search for :).

conclusion

That’s not much code! And it should be quite easy to extend to allow for more complex patterns, detecting if your service is running or not can be done by making it send pings at regular intervals, you can also make your service fetch/work on data from somewhere else in the background, and pass it to the UI when it’s ready.

A slightly more complex demo based on this can be found here.


Viewing all articles
Browse latest Browse all 3409

Trending Articles