アプリからKivyLauncherを呼び出す - Kivy Advent Calendar 2013
Android/pyjniusばっかりは飽きたって? ごめんなさい。今回で最後にします。
KivyLauncherを使っていて今とは別のアプリを実行したい場合、終了させるとHome画面に戻って、再度KivyLauncherを立ち上げないといけないですよね。
結構気になりますよね? はい。
要はKivyアプリからKivyLauncher自体を立ち上げてしまえばいいんです。重複では起動しないので問題は起こらないはず...たぶん?
(/sdcard/kivy/back/)
android.txt | お約束 |
main.py | プログラム本体 |
(main.py)
from kivy.uix.button import Button from kivy.app import App from jnius import autoclass, cast class BackApp(App): PythonActivity = autoclass('org.renpy.android.PythonActivity') Intent = autoclass('android.content.Intent') Uri = autoclass('android.net.Uri') def back_to_launcher(self, instance): intent = self.Intent("org.renpy.LAUNCH", self.Uri.parse("kivy:/sdcard/kivy/")) intent.setClassName("org.kivy.pygame", "org.renpy.android.ProjectChooser") activity = cast('android.app.Activity', self.PythonActivity.mActivity) activity.startActivity(intent) def build(self): button = Button(text='back to launcher') button.bind(on_press=self.back_to_launcher) return button if __name__ == '__main__': BackApp().run()
解説
- 本当は「Kivyアプリ」から「別のKivyアプリ」を起動できないか試していたんですけど、うまくいかなかったんですね。でもランチャ部分 (org.renpy.android.ProjectChooser) なら実行できたので、まあ予定変更ということで...
Androidアプリを一覧表示 - Kivy Advent Calendar 2013
本当はアプリ起動まで持って行きたかったんですけどねー。ちょっと時間的に無茶でした…
現状はAndroidアプリの一覧を表示するだけです。まあソートしてないので、起動できたところで使いづらいったらありゃしませんが…
しかしAndroidのパッケージマネージャからデータを引っ張り出してますが、パーミッション要らないんですね。トロイの木馬には怪しいアプリが丸見えなのか…おお怖い怖い。
まあListViewの使い方の学習も兼ねていますので、その部分だけはAndroid以外の人も参考になるかと。
(/sdcard/kivy/launch/)
android.txt | お約束 |
fonts_ja.py | 使い回し |
main.py | スクリプト本体 |
(main.py)
from kivy.app import App from kivy.uix.listview import ListView, ListItemLabel from kivy.adapters.listadapter import ListAdapter from jnius import autoclass, cast import fonts_ja class PackageManager: PythonActivity = autoclass('org.renpy.android.PythonActivity') Intent = autoclass('android.content.Intent') def __init__(self): self.context = cast('android.content.Context', self.PythonActivity.mActivity) self.pm = self.context.getPackageManager() def query(self): intent = self.Intent() intent.setAction(self.Intent.ACTION_MAIN) intent.addCategory(self.Intent.CATEGORY_LAUNCHER) return self.pm.queryIntentActivities(intent, 0).toArray() def loadLabel(self, ri): return ri.loadLabel(self.pm).toString() class LauncherApp(App): def build(self): pm = PackageManager() data = pm.query() args_converter = lambda _i, _r: {'text': pm.loadLabel(_r), 'size_hint_y': None, 'height': 50} list_adapter = ListAdapter(data=data, args_converter=args_converter, cls=ListItemLabel, selection_mode='single', allow_empty_selection=False) root = ListView(adapter=list_adapter) return root if __name__ == '__main__': LauncherApp().run()
pyjniusを使ってWebブラウザを起動 - Kivy Advent Calendar 2013
ボタンを押すとブラウザで特定のWebページを開くだけ。
from kivy.uix.button import Button from kivy.app import App from jnius import autoclass, cast class BrowserApp(App): PythonActivity = autoclass('org.renpy.android.PythonActivity') Intent = autoclass('android.content.Intent') Uri = autoclass('android.net.Uri') def start_browser(self, instance): intent = self.Intent() intent.setAction(self.Intent.ACTION_VIEW) intent.setData(self.Uri.parse(instance.text)) currentActivity = cast('android.app.Activity', self.PythonActivity.mActivity) currentActivity.startActivity(intent) def build(self): button = Button(text='http://kivy.org/') button.bind(on_press=self.start_browser) return button if __name__ == '__main__': BrowserApp().run()
URI | |
http: https: | ブラウザ |
geo: | マップ |
market: | マーケット |
tel: | ダイアラ |
content://contacts | 連絡帳 |
テキストの共有 - Kivy Advent Calendar 2013
風邪を引いてしまいました。
体調が復帰してから解説書きます。ごめんなさい...
(/sdcard/kivy/sender/)
android.txt | お約束 |
main.py | スクリプト本体 |
fonts_ja.py | 使い回し |
(android.txt)
title=sendere author=cheeseshop orientation=portrait
(main.py)
# -*- coding: utf-8 -*- from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.textinput import TextInput from android import action_send import fonts_ja SUBJECT = u'2013-12-16 休み' BODY = ( u'おはようございます。\n' u'cheeseshopです。本日は\n' u'風邪による体調不良のため\n' u'全日休をいただきたいと思います。\n' u'大変申し訳ありませんが\n' u'どうかよろしくお願い致します。' ) class SenderApp(App): def do_send(self, instance): action_send( 'text/plain', subject=self.subject.text.encode('utf-8'), text=self.body.text.encode('utf-8') ) def build(self): root = BoxLayout(orientation='vertical') self.subject = TextInput(text=SUBJECT, size_hint=(1,0.1), multiline=False) self.body = TextInput(text=BODY, size_hint=(1,0.4)) button = Button(text='send', size_hint=(1,0.5)) button.bind(on_press=self.do_send) root.add_widget(self.subject) root.add_widget(self.body) root.add_widget(button) return root def on_pause(self): return True if __name__ == '__main__': SenderApp().run()
pyjniusを使って写真を撮る - Kivy Advent Calendar 2013
KivyにはCameraというウィジェットがあるのですが、これは写真撮影ではなくて動画を画面上に映すものでした。しかもAndroidでは未実装とのこと。仮にこれが動けばスクリーンショットを取る機能と組み合わせて写真撮影できなくもないですが...
できれば内蔵のカメラ機能を使いたい思っていろいろ探したところ、Kivyのソースの中にカメラを扱うサンプルがありました。
以下はそれの焼き直し版です。
このアプリを起動すると真っ暗ですが、タップするとカメラモードになります。
そして撮影すると縮小してアルバム風の画面にどんどん保存されていきます。
from kivy.app import App from kivy.clock import Clock from kivy.uix.stacklayout import StackLayout from kivy.uix.image import Image from jnius import autoclass, cast from android import activity from functools import partial from datetime import datetime from os.path import exists class ImageCaptureApp(App): RESULT_CODE = 0x5963 Intent = autoclass('android.content.Intent') PythonActivity = autoclass('org.renpy.android.PythonActivity') MediaStore = autoclass('android.provider.MediaStore') Uri = autoclass('android.net.Uri') parcel = partial(cast, 'android.os.Parcelable') def build(self): self.root = StackLayout() self.root.bind(on_touch_up=self.do_capture) activity.bind(on_activity_result=self.on_activity_result) return self.root def get_filename(self): i = 0 while True: i += 1 fn = '/sdcard/{:%Y%m%d_%H%M}_{:03d}.png'.format(datetime.now(), i) if not exists(fn): return fn def do_capture(self, instance, value): self.filename = self.get_filename() uri = self.parcel(self.Uri.parse('file://' + self.filename)) intent = self.Intent(self.MediaStore.ACTION_IMAGE_CAPTURE) intent.putExtra(self.MediaStore.EXTRA_OUTPUT, uri) self.PythonActivity.mActivity.startActivityForResult(intent, self.RESULT_CODE) def on_activity_result(self, requestCode, resultCode, intent): if requestCode == self.RESULT_CODE: Clock.schedule_once(partial(self.add_picture, self.filename), 0) def add_picture(self, filename, *args): self.root.add_widget(Image(source=filename, size=(320,320), size_hint=(None,None))) def on_pause(self): return True if __name__ == '__main__': ImageCaptureApp().run()
解説
MediaStore.ACTION_IMAGE_CAPTUREのインテントを使えばカメラが起動して、EXTRA_OUTPUTにURLをセットすればそこに画像を保存してくれます。
そこまではpyjniusを使って何とか出来てたのですが、問題はそこから「どうやってアプリに制御を戻すか」で、Kivyのサンプル(examples/android/tackpicture/) を見るまでよく分からなかったところです。
結局Python for Androidのandroidモジュール(※SL4Aやpygameのandroidモジュールとは別です)を使えばon_activity_resultイベントが発生という形で受け取れるので、そのタイミングで保存した画像ファイルを取り込んでいます。
既知の問題
カメラを撮影しないで終了するケースに対応していません(エラーにはならないですが真っ白けの画像が入ってしまいます)。
追記
https://github.com/kivy/plyer/
本当はこちらを使った方がいいのかな。ビルドが必要だけど...
後で試してみます。
pyjniusを使って喋らせる - Kivy Advent Calendar 2013
今回は日本語を喋らせるので、N2 TTS (https://play.google.com/store/apps/details?id=jp.kddilabs.n2tts) をインストールしてください。
さあ、今年の流行語大賞は...?
いやまあ、単に「へぇーボタン」の拡張版だったりするんだけど…
(/sdcard/kivy/speech/)
android.txt | お約束 |
fonts_ja.py | 日本語使うので |
main.py | スクリプト本体 |
(android.txt)
title=speech author=cheeseshop orientation=portrait
(main.py)
# -*- coding: utf-8 -*- from kivy.app import App from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from jnius import autoclass import fonts_ja SPEECHES = [ u'2013年流行語大賞は', u'がってん', u'へぇー', u'それな', u'おもて梨ぷしゃー', u'暇でしょ', u'わけがわからないよ', u'てへぺろ', u'あーね', u'にゃんぱすー', u'オレオレ手話', u'愛よ', u'生きねば', u'羅針盤まわすなー', u'に決定しました', ] class SpeechApp(App): Locale = autoclass('java.util.Locale') PythonActivity = autoclass('org.renpy.android.PythonActivity') TextToSpeech = autoclass('android.speech.tts.TextToSpeech') def do_speech(self, instance): self.tts.speak(instance.text, self.TextToSpeech.QUEUE_ADD, None) def build(self): self.tts = self.TextToSpeech(self.PythonActivity.mActivity, None) self.tts.setLanguage(self.Locale.JAPAN) self.root = GridLayout(cols=3) for text in SPEECHES: button = Button(text=text, on_press=self.do_speech) self.root.add_widget(button) return self.root if __name__ == '__main__': SpeechApp().run()
解説
(長くなりそうなので、今回はKivyLauncherからのAndroid機能へのアクセスに特化して書きます。KivyとSL4Aの比較についてはまた別の日に...)
Python for AndroidはJavaではなくCで書かれているので、基本的にJavaで作られているAndroid UIに触るためには何らかの工夫が必要です。
KivyLauncherにはpyjniusモジュールが内蔵されていて、これを使ってAndroidの機能に触ることができます。いわばPythonのctypesモジュールのJava版といえると思います。
Androidにテキストをしゃべらせる機能は、パーミッションも必要ないし、pyjniusの中でも比較的アクセス方法が簡単なのでよくサンプルに使われます。
twistedを使ってFTPサーバを立てる - Kivy Advent Calendar 2013
PCとAndroid端末とのファイルのやり取りには、USBつなぐのも面倒なのでDroidOverWifi (http://www.droidoverwifi.com/) なんかを使っています。いわばファイル編集機能特化のHTTPサーバです。
でもKivyLauncherだってtwistedが入っているんだし、それを使ってFTPサーバ作れないかなーと思って作ったのがこれです。
実行すると「IPアドレス:ポート番号」を表示してFTPサーバが立ち上り、FTPクライアントを使ってファイルのアップロード/ダウンロードができます(ポート番号は8021、PASVモードはオンにしてください)。
FFFTP (http://sourceforge.jp/projects/ffftp/) みたいなフォルダごと転送できるクライアントを使うと結構便利に使えます。
(/sdcard/kivy/ftpserver/)
android.txt | お約束 |
ipaddr.py | IPアドレスを表示させたいだけで作ったモジュール |
main.py | スクリプト本体 |
passwd.dat | パスワードファイル みちゃだめ |
(ipaddr.py)
import socket import fcntl import struct def get_ipaddr(): interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"] s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for iface in interfaces: ifreq = struct.pack('32s', iface) try: result = fcntl.ioctl(s.fileno(), 0x8915, ifreq) return socket.inet_ntoa(result[20:24]) except IOError: continue return None
(main.py)
from kivy import kivy_home_dir from kivy.app import App from kivy.uix.label import Label from kivy.support import install_twisted_reactor install_twisted_reactor() from twisted.protocols.ftp import FTPFactory, FTPRealm, FTPShell, IFTPShell from twisted.cred.portal import Portal from twisted.cred.checkers import AllowAnonymousAccess, FilePasswordDB, ANONYMOUS from twisted.internet import reactor from twisted.python.filepath import FilePath import ipaddr class MyFTPRealm(FTPRealm): HOME = kivy_home_dir #HOME = '/sdcard/kivy/' def requestAvatar(self, avatarId, mind, *interfaces): for iface in interfaces: if iface is IFTPShell: if avatarId is ANONYMOUS: avatar = FTPAnonymousShell(self.anonymousRoot) else: avatar = FTPShell(FilePath(self.HOME)) return IFTPShell, avatar, getattr(avatar, 'logout', lambda: None) raise NotImplementedError("Only IFTPShell interface is supported by this realm") class FTPServerApp(App): PORT = 8021 def build(self): portal = Portal( MyFTPRealm("./"), [AllowAnonymousAccess(), FilePasswordDB("passwd.dat")]) factory = FTPFactory(portal) reactor.listenTCP(self.PORT, factory) self.label = Label(text="FTP server started\n%s:%d" % (ipaddr.get_ipaddr(),self.PORT)) return self.label if __name__ == '__main__': FTPServerApp().run()
解説
最初にinstall_twisted_reactor()を実行すれば、Kivyアプリの開始・終了のタイミングでtwistedリアクタの開始・終了を行うようになります。
後はTwistedのプログラミングをすればOK。protocolクラスを使ってFTPサーバを組み立てていくわけですが、デフォルトのFTPRealmは「/home/(ユーザ名)」の下を公開しようとします。Androidはそんなディレクトリないので、サーバは立ち上がるけど認証後は550エラーを返し続けることになります。ここではMyFTPRealmを作って該当のメソッドを置き換えています。
FTPサーバで公開されるディレクトリ (kivy_home_dir) ですが、Androidだと「/sdcard/kivy/(アプリ名)/.kivy/」の下になります。ここはKivyのログファイルなどが書き込まれる場所なので無難といえば無難です。
とはいえ後でファイル移動させるのも面倒だし、どーせ自分しかLANにいませんよというなら「/sdcard/kivy/」や「/sdcard/」の下をどーんと公開しちゃうのもありかもしれません。
(何が起こっても一切責任持ちませんけど...)