「Building a background application on android with Kivy.」が期待通りのものだった話

kivyLauncher でバックグランドアプリケーションが作れれば世界が広がるよな、と。

実は一週間以上前からちょろっと探っていたんだけれど、今ひとつ説明がわかりにくい。一応 Kivy Planet の記事 の説明が最もまとまった説明なんだけれど、実際読んでてちょっとモヤモヤする。まず、期待通りのものなんだろうか、というのと、kivyLauncher でも通用するのか、という点。

さらっと読む限りポイントは滅茶苦茶少なくて、

  • サービスの「実装」はアプリケーションのフォルダの service/main.py に書く。
  • そのサービスを android のサービスに仕立て上げるのに、android.AndroidService を使う。
    • サービス側が「ワタシはサービスです」と主張するのではなくて、起動側が「お前はサービスだぞ」というノリ。なのはまぁそうだとは思うが。
    • 気持ち悪い気もするが「サービスの実体はこれ」と明示するのではなくて、暗黙で service/main.py になる。
    • つまり(アタシの理解では)クライアントとサービスの関係が一対一ってことである。これがキツい制約なのかどうかはやりたいこと・作り方によるだろう。
  • クライアントとサービスとのコミュニケーションは kivy.lib.osc で。(絶対ってわけではなくて、twisted でもいい、って書かれてたりする。)

そうなのだな、と読めばだいたいわかるのだが、裏方に隠れていることが多過ぎて、「ほんとにこんなんでサービスとして振舞うわけ?」と思っちゃう。要するにサービスのインスタンスがほんとうに「ちゃんとバックグラウンドで動き続けてくれるのか」と。kivyLauncher アプリのメインの GUI のアクティビティとともに生き死にしちゃうんでは意味がない。

Kivy Planet の記事の末尾に 「A slightly more complex demo based on this can be found here.」として紹介されてた here が検証に使う元ネタとして使いやすそうだったので、これを書き換えて検証してみる。

書き換える、と言っても、要は「クライアントが死んでも生き残ってくれちゃうのか?」を検証するに足るものであればいいので、まずはサービス側をこのようにする:

service/main.py
 1 from kivy.lib import osc
 2 from time import localtime, asctime, sleep
 3 import uuid
 4 import base64
 5 
 6 _INSTANCE_ID = base64.urlsafe_b64encode(
 7     uuid.uuid4().bytes).rstrip('=')
 8 oscid = None
 9 
10 
11 def ping(*args):
12     # when client call '/ping', then we response as /message
13     osc.sendMsg(
14         '/message',
15         ["{}, {}".format(_INSTANCE_ID, oscid), ],
16         port=3002)
17 
18 
19 def send_date():
20     osc.sendMsg('/date', ["?? " + asctime(localtime()), ], port=3002)
21 
22 
23 if __name__ == '__main__':
24     osc.init()
25     global oscid
26     oscid = osc.listen(ipAddr='0.0.0.0', port=3000)
27     osc.bind(oscid, ping, '/ping')
28     while True:
29         osc.readQueue(oscid)
30         send_date()
31         sleep(.1)

「ワタシはサービスです」と主張する何かはなく、osc でのコミュニケーションを取っているだけ。(というか osc で、ということがサービスの証なのかもしれんけど。) アタシが書き換えたのはサービスの応答がこの main.py のインスタンスを特定するものを返すようにしただけ。

で、クライアント:

main.py
 1 __version__ = '0.1'
 2 
 3 from kivy.app import App
 4 from kivy.lang import Builder
 5 from kivy.lib import osc
 6 from kivy.clock import Clock
 7 
 8 kv = '''
 9 BoxLayout:
10     orientation: 'vertical'
11     BoxLayout:
12         size_hint_y: None
13         height: '30sp'
14         Button:
15             text: 'start service'
16             on_press: app.start_service()
17         Button:
18             text: 'stop service'
19             on_press: app.stop_service()
20 
21     ScrollView:
22         Label:
23             id: label
24             size_hint_y: None
25             height: self.texture_size[1]
26             text_size: self.size[0], None
27 
28     BoxLayout:
29         size_hint_y: None
30         height: '30sp'
31         Button:
32             text: 'ping'
33             on_press: app.send()
34         Button:
35             text: 'clear'
36             on_press: label.text = ''
37     Label:
38         id: date
39         size_hint_y: None
40         height: '30sp'
41 '''
42 
43 
44 class ClientServerApp(App):
45     def build(self):
46         self.service = None
47 
48         osc.init()
49         oscid = osc.listen(port=3002)
50 
51         # when we /ping to service, then our service will
52         # response as /message
53         osc.bind(oscid, self.display_message, '/message')
54 
55         # our service will send to /date unilaterally
56         osc.bind(oscid, self.date, '/date')
57 
58         self.root = Builder.load_string(kv)
59 
60         #
61         Clock.schedule_interval(
62             lambda *x: osc.readQueue(oscid), 0)
63 
64         return self.root
65 
66     def start_service(self):
67         from android import AndroidService
68         service = AndroidService('my pong service', 'running')
69         service.start('service started')
70         self.service = service
71 
72     def stop_service(self):
73         if self.service:
74             self.service.stop()
75             self.service = None
76 
77     def send(self, *args):
78         osc.sendMsg('/ping', [], port=3000)
79 
80     def display_message(self, message, *args):
81         self.root.ids.label.text += '%s\n' % message[2]
82 
83     def date(self, message, *args):
84         self.root.ids.date.text = message[2]
85 
86 
87 if __name__ == '__main__':
88     ClientServerApp().run()

android.txt はいつも通り kivyLauncher で動かすには必要だが、service/ フォルダ側にはいらない。一応:

android.txt
1 title=Kivy Service Demo
2 author=original author is tshirtman
3 orientation=portrait

オリジナルの「here」は検証には迷惑な構造になっていて、起動するや start_service() しておったが、無論そりゃぁおかしい。

「やや複雑」と言っている通りで少々は入り組んでるがこういうこと:

  • サービスは一方的に /date に投げ付け続けている
  • クライアントが /ping するとサービスが /message で応答

これだけ。

てわけで動かしてみた:

うんうん、期待通りね。

ところで、「サービスさんよ、お主、生きておるのか?」の問い合わせって出来ないんかなぁ、と思ったんだけど、これは ping 的なハートビートを必ず実装せい、てことなのかいね??

さーて、世界が広がったぞ。重力とか気圧とか、あるいはそもそも GPS の情報も、「記録し続けてみたい」かったりするのですね。例えば散歩の記録みたいなことは簡単に出来るよね。





と、ここまで書いてみてから AndroidService が「Old p4a toolchain doc」というカテゴリに属していることに気付く。うーん? 使っていいの?

代わりのもの、はServicesか。service/main.py なノリは同じだね。なんだこれ、start_service はあるのに stop_service について書かれてない。ないの? なかったら困るさね…。あ、あった。あらぁ? でもこれ、AndroidService と同じ場所じゃないのかね?

仕方ない、ひとまず試すか…:

main.py (再)
 1 __version__ = '0.1'
 2 
 3 from kivy.app import App
 4 from kivy.lang import Builder
 5 from kivy.lib import osc
 6 from kivy.clock import Clock
 7 import android
 8 
 9 kv = '''
10 BoxLayout:
11     orientation: 'vertical'
12     BoxLayout:
13         size_hint_y: None
14         height: '30sp'
15         Button:
16             text: 'start service'
17             on_press: app.start_service()
18         Button:
19             text: 'stop service'
20             on_press: app.stop_service()
21 
22     ScrollView:
23         Label:
24             id: label
25             size_hint_y: None
26             height: self.texture_size[1]
27             text_size: self.size[0], None
28 
29     BoxLayout:
30         size_hint_y: None
31         height: '30sp'
32         Button:
33             text: 'ping'
34             on_press: app.send()
35         Button:
36             text: 'clear'
37             on_press: label.text = ''
38     Label:
39         id: date
40         size_hint_y: None
41         height: '30sp'
42 '''
43 
44 
45 class ClientServerApp(App):
46     def build(self):
47         self.service = None
48 
49         osc.init()
50         oscid = osc.listen(port=3002)
51 
52         # when we /ping to service, then our service will
53         # response as /message
54         osc.bind(oscid, self.display_message, '/message')
55 
56         # our service will send to /date unilaterally
57         osc.bind(oscid, self.date, '/date')
58 
59         self.root = Builder.load_string(kv)
60 
61         #
62         Clock.schedule_interval(
63             lambda *x: osc.readQueue(oscid), 0)
64 
65         return self.root
66 
67     def start_service(self):
68         android.start_service(
69             title='my pong service',
70             description='service description...',
71             arg='running')
72 
73     def stop_service(self):
74         android.stop_service()
75 
76     def send(self, *args):
77         osc.sendMsg('/ping', [], port=3000)
78 
79     def display_message(self, message, *args):
80         self.root.ids.label.text += '%s\n' % message[2]
81 
82     def date(self, message, *args):
83         self.root.ids.date.text = message[2]
84 
85 
86 if __name__ == '__main__':
87     ClientServerApp().run()

どう考えても同じコードになってるような気がする。うーん、わがらん。ともあれこちらのコードでも同じ結果になった。そりゃぁそうだろうなぁという気もするし…。

ちなみに「kivyLauncher にこだわらないなら」、service/main.py 以外のスクリプトを使うことも出来る、けど今は kivyLauncher でお気楽に、がマイブームなのでアタシはまだやらない。






おっと…あぶない、あぶない。これは書いとかないと確実に迷子になる。なんかドキュメントが整理されてなくてやんなっちゃうんだけど、ともあれ偶然これを見つけてた。それ自体は「どーして動かないんですかぁ?」の内容が「そりゃそーだ」なトホホな質問なんだけど、注目すべきはそれではなくて arg = os.getenv('PYTHON_SERVICE_ARGUMENT')。これが start_service に渡す arg を取ってるらしい。

ので上の例だと例えば「サービスとクライアント両方でポート番号管理すんのやだ!」てことで:

service/main.py
 1 from kivy.lib import osc
 2 from time import localtime, asctime, sleep
 3 import uuid
 4 import base64
 5 
 6 _INSTANCE_ID = base64.urlsafe_b64encode(
 7     uuid.uuid4().bytes).rstrip('=')
 8 oscid = None
 9 ports = None
10 
11 def ping(*args):
12     # when client call '/ping', then we response as /message
13     osc.sendMsg(
14         '/message',
15         ["{}, {}".format(_INSTANCE_ID, oscid), ],
16         port=ports[1])
17 
18 
19 def send_date():
20     osc.sendMsg('/date', ["?? " + asctime(localtime()), ],
21                 port=ports[1])
22 
23 
24 if __name__ == '__main__':
25     import os
26     global ports
27     ports = map(
28         int, os.getenv('PYTHON_SERVICE_ARGUMENT').split(","))
29 
30     osc.init()
31     global oscid
32     oscid = osc.listen(ipAddr='0.0.0.0', port=ports[0])
33     osc.bind(oscid, ping, '/ping')
34     while True:
35         osc.readQueue(oscid)
36         send_date()
37         sleep(.1)
main.py (抜粋)
1     def start_service(self):
2         android.start_service(
3             title='my pong service',
4             description='service description...',
5             arg='3000,3002')

なんてことも出来る。

ていうかもちっとドキュメントちゃんとして…。