アプリから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()

Intent.ACTION_VIEWを使うことでURLに対応するアクティビティが起動します。この方法では次のようなアプリが起動できます。

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

解説

Androidのアプリデータ共有については、androidモジュールにすでにaction_sendが入っているのでそれを使います。定型文やWebページ抜粋などを加工して、Twitterやメールのクライアントに渡すだけでよければ簡単にできます。
ちなみに「image/png」とすればfilename引数の画像ファイルも送信できるので、写真の連携なんかもできそうです。

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 Androidandroidモジュール(※SL4Aやpygameandroidモジュールとは別です)を使えば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 AndroidJavaではなく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/」の下をどーんと公開しちゃうのもありかもしれません。
(何が起こっても一切責任持ちませんけど...)