2016年12月16日

Qtで作成するAndroidサービス

この記事は、Qt Advent Calendar 2016 16日目用として作成した記事です。


■はじめに
この記事では、Qt5.7からサポートされた、Androidサービスの実装方法について、拙作の音楽プレーヤーの実例を交えて解説していきたいと思います。
XMDX_Qt2.png

Androidサービスは5.7で導入されたばかりで、まだ資料、ドキュメントがほとんどありません。
現時点で参考になるものといえば、KDABのブログ(https://www.kdab.com/qt-android-episode-7/https://www.kdab.com/qt-android-create-android-service-using-qt/)や、それに付随するサンプル(https://github.com/KDAB/android)を頼りにするしかありません。
また、サービスを実装するに当たりほぼ必須となるQtRemoteObjectsというコンポーネントがあるのですが、こちらはまだPlayGroundの段階でメインラインにマージされておらず、またサンプルコード以上のドキュメントも皆無に近いという状態です。

また、この辺に関する日本語の情報もほとんどないという現状はあるのですが、この記事では日本語の情報を提供することより、より実戦的なノウハウ、ハマりどころにフォーカスを当てて書こうと思います。

この領域は現時点で世の中にほとんどお手本がなく、「これが正解だ」と胸を張れる自信はないのですが、おつきあいください。

■動機
現在、私はLinux及びAndroid用のMDXプレーヤーを開発しています。
MDXというのは、二十数年前、X68000というパソコン用に策定された音楽データフォーマットです。現在ではWindows上で音源部分をエミュレーションで再生するソフトウェアが多数存在していますが、LinuxやAndroid用のプレーヤーがなかった(正確にはあることはあったが、自分の要件に合わなかった)ので、プレーヤーのコア部分は既存のものを利用させていただきつつ、UI部分はQtを使って自作をしています。
XMDX_Qt1.png

■設計
プレーヤーの非常にざっくりとした設計は以下のクラス図のようになります。
XMDX_Qt3.png
クラス図1


GUI部分はAndroidをメインターゲットとしているため、QMLで作成します。
プレーヤー部分はプレーヤーが現在再生している曲名、曲の長さ、現在の再生位置、等をプロパティバインディングで表示しています。また、ボタンが押されたときのアクションとして、プレーヤークラスのメソッドを呼び出します。
QMLからメソッドとして呼び出せるには、当該のメソッドがスロットまたはQ_INVOKABLEとして定義されている必要があります。

プレイリスト部分は、QMLのListViewからQObjectListをモデルとして参照しているという形です。こちらもcurrentIndexプロパティをバインドすることで、現在再生中の曲と選択状態を同期します。

■Androidにおける問題
さて、プレーヤーとしての実装は基本的には上記の構造で実現できますし、同一のコードでAndroid上で動かすことができます。が、2つ問題があります。
一つは、Androidアプリはバックグラウンドに回ると動作が停止してしまうということです。これについてはマニフェストファイルをいじることでバックグラウンドでも動作し続けるようにできます。(こちらのStep II: Add the service section(s) to your AndroidManifest.xml fileを参照)
もう一つが厄介で、Androidではアプリがバックグラウンドで動いていても、OSによって前触れなくプロセスがKILLされてしまいます。
筆者のスマホでは、アプリをバックグラウンドに持っていくだけなら演奏を続けてくれますが、その状態でWebブラウザなどを開くと演奏が中断されてしまいます。

■サービスの設計
これを防ぐためには、Androidのサービスを実装して、その中にプレーヤーの本体機能を移す必要があります。
落とされたら困る演奏機能はサービス側に実装して、画面であるアクティビティはサービスの状態表示とユーザーからの入力受付に専念するという構造です。
プロセス間通信にはQtRemoteObjectsを使用します。
QMLから参照するオブジェクトはプロパティでGUIとバインドされ、QMLの画面を更新するには、バインドされたプロパティが更新されたことをシグナルとして通知する必要があります。
QtRemoteObjectsはプロセス間でシグナル、スロットの呼び出しまで行えるというすぐれものです。

■マルチプラットフォーム対応
ここでまた問題があります。当初自分はAndroidとデスクトップLinuxをターゲットとして開発するつもりでいました。Androidに対応するためにサービスとアクティビティの2プロセスに分離するというアーキテクチャを前提としてしまうと今度はデスクトップOSで動かすことができません。

そこで、プレーヤーとプレイリストを抽象化して、QMLのGUI側から見た場合、それがローカルプロセス上で動く機能の実体であるか、リモートプロセスとの橋渡しをするプロキシであるかは意識しなくて済む設計にしました。
XMDX_Qt4.png

クラス図2


ここでQtRemoteObjectsを用いたクラス設計の実例を示します。
プロセス間通信をするにはサービス側、アクティビティ側にそれぞれ通信を担うクラスが必要になります。
QtRemoteObjectsではサービス側をSource、クライアント側をReplicaと呼んでいます。それとは別に、前述のマルチプラットフォーム対応などの事情もあり、アプリケーション側で独自にプロキシクラスを実装しており、それぞれClientProxy、ServiceProxyと命名しています。

この図では端折っていますが、プレイリストの管理機能本体もサービス側に持ってくる必要があります。連続プレイやシャッフルプレイの曲の切り替わりの際、サービス側でプレイリストを保持していないと次に再生すべき曲がわからないためです。
デスクトップOS上で動かす場合は前記のクラス図1の構成で動かします。(初期化時のオブジェクト生成が異なるので、そこは#ifdefで分けます)


■QtRemoteObjectsの使い方
前記のSourceおよびReplicaのソース(実際にはヘッダー)は定義ファイルから、QtRemoteObjectsに含まれるrepcというツールを用いて自動生成されます。
QtRemoteObjectsはqmakeの拡張を伴うため、ツールのビルド、インストールが別途必要になります。

git clone https://code.qt.io/cgit/playground/qtremoteobjects.git
cd qtremoteobjects.git
$(Android用Qtのパス)/bin/qmake
make && make install

ここでのターゲットはAndroidなので、Androidのクロスビルド用のqmakeを指定する必要があります。
また、ANDROID_NDK_ROOT環境変数を設定している必要があります。

次に、プロセス間通信をするためのクラス定義を記述します。
クラス定義は.repという拡張子(おそらくReplicaの略)を用います。
ここではPlayerクラスのリモート通信を定義するPlayer.repファイルを作ります。
repファイルの書式については以下の記述を見ればだいたい見当つくとは思うのですが、私が探した限り正式なドキュメントはありません。

class Player
{
SIGNAL(isPlayingChanged(bool isPlaying));
SIGNAL(isSongLoadedChanged(bool isSongLoaded));
SIGNAL(titleChanged(QString title));
SIGNAL(fileNameChanged(QString fileName));
SIGNAL(durationChanged(float duration));
SIGNAL(durationStringChanged(QString duration));
SIGNAL(currentPositionChanged(float currentPosition));
SIGNAL(currentPositionStringChanged(QString currentPosition));
SIGNAL(songPlayFinished());

// 曲をロードする。
// 演奏せずにファイル情報を取得するだけの場合はrenderWavをfalseにする。
SLOT(bool loadSong(bool renderWav, const QString& fileName, const QString& pdxPath, unsigned loops, bool enableFadeout));
// 演奏開始
SLOT(bool startPlay());
// 演奏中断
SLOT(bool stopPlay());
// 曲中のシーク(引数は曲頭からの秒数)
SLOT(bool setCurrentPosition(float position));

// 曲送り、曲戻し
SLOT(bool stepForward());
SLOT(bool stepBackward());

// インデックスを指定
SLOT(bool playFileByIndex(int index));

// 曲名を取得
SLOT(QString title());
// ファイル名を取得
SLOT(QString fileName());
// 曲長(秒)を取得
SLOT(float duration());
// 曲長(秒)をmm:ssフォーマットで取得
SLOT(QString durationString());
// 演奏中の位置(秒)を取得
SLOT(float currentPosition());
// 演奏中の位置(秒)をmm:ssフォーマットで取得
SLOT(QString currentPositionString());
// 演奏中かどうか取得
SLOT(bool isPlaying());
// 曲がロードされているかどうか取得
SLOT(bool isSongLoaded());
}

後半のtitle()以降のメソッドはSLOTである必要はなくQ_INVOKABLEであればよいのですが、QtRemoteObjectsではメソッドはSLOTとしてしか定義できないようなので仕方なくSLOTとします。

次に、.proファイルには以下のように記述します。
REPC_REPLICA += repファイルを追加するとReplicaクラス
REPC_SOURCE += repファイルを追加するとSourceクラスのヘッダーが生成されます。

android:{
QT+= androidextras remoteobjects

REPC_REPLICA += player.rep
REPC_SOURCE += player.rep
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android

SOURCES += \
playerservice.cpp \
playerclientproxy.cpp \
playerserviceproxy.cpp

HEADERS += \
playerservice.h \
playerclientproxy.h \
playerserviceproxy.h
}

REPC_REPLICAおよびREPC_SOURCEの記述に対応してrepcというツールが起動してヘッダーが生成されます。ビルドフォルダを確認してrep_player_replica.hとrep_player_source.hが生成されていれば成功です。
複数のクラスをリモートに対応させたい場合、qmakeはSOURCES += a.cpp b.cppのように連続して書けますが、REPCに関しては1行に1つずつ書かないといけないようです。

■サービス、アクティビティの寿命
サービスにするとOSによって終了されなくなるのですが、今度は使っていないときでも通知領域に残り続けるという問題が発生します。そこで、サービス側のプレーヤーは再生停止状態が10分間続いたら自身でサービスを終了するようにします。


また、この施策を取ると、一時停止状態で放置した場合にまれにですがアクティビティがバックグラウンドで生きていながらサービスが先に終了してしまうということが起こるので、アクティビティが全面に出たらサービスを再度開始するという対策を入れます。

■最後に
実際のソースコードはhttps://github.com/eighttails/XMDXで取得できますので、興味のある方はそちらを参照ください。
本記事では説明の簡素化のためにクラスを一部端折って説明しています。実際のクラス名とは若干異なることをご了承ください。

AndroidServiceはもQtRemoteObjectsも資料が少なくて難儀しました。
ただ、QtRemoteObjectsはAndroidでサービスを実装するにはほぼ必須なので、いつまでもPlaygroundではまずいんじゃないかと思いました。
ラベル:QT QML android X68000 MDX
posted by eighttails at 00:00| Comment(0) | XMDX | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

※ブログオーナーが承認したコメントのみ表示されます。