Android用(ちょっと)本格的なミュージックプレーヤの開発 part7

2年以上前に公開した
Android用(ちょっと)本格的なミュージックプレーヤの開発
シリーズですが、メール、コメントなどで今でも質問を頂き、
音を出す部分に関しても需要がありそうなので
時間がかかりそうですがここにひっそりと更新していきます。

ミュージックプレーヤで音を出すには1

Androidで音を鳴らしたい場合
基本的には
 Android MediaPlayerクラス
を使用します。
かなり多機能なので再生に関してはこのクラスを全面的に
使用すればいいのですが、

画像出典・参考

このMediaPlayer、一種の有限ステートマシンになっています。
この状態を正しく管理してやることが必要です。
通常ゲームなどで mp3 をBGMとしてかけるだけなら
基本の"型"どおりのコードを貼り付けておけばよいのですが
(ちょっと)本格的なプレーヤではもう少しデリケートに
扱う必要があります。

ミュージックプレーヤで音を出すには2

もう1つ重要なのが、MediaPlayerを管理する場所です。
ゲームなどで音を鳴らすなら Activity 配下のクラスで
適当に音を鳴らせばいいのですが、(ちょっと)本格的な
プレーヤではそうはいきません。

ミュージックプレーヤでは、通常 part1〜6で作成したような
ライブラリから再生する曲を選択したあとは、基本的に
画面(アクティビティ)は閉じて、他のアプリの裏で再生したり、
ウィジェットや通知領域からの操作も受け付ける必要があります。

よって、MediaPlayerの管理はActivityではなく、
Service で行う必要があります。
サービスで MediaPlayer を管理しておいて
アクティビティやウィジェットから
そのサービスに接続、リモートでコマンドやリクエストを
送信して、サービス側でこれを受け取って各種操作を
行ったり、現在再生中のトラックの情報やアルバムアートを
アクティビティ側に送信し返したり という処理が必要に
なります。
このあたりは part6までとは比べ物にならないほど
面倒な仕組みを多用するので記事にするのをサボっていたわけです

最小限のサービス

というわけで今回は
最小限のサービスを作って、Mainアクティビティから
テストコマンドが送れるようにしてみます。

なお、ソースのベースは partX で作成した近代化改修版
TestPlayer2 で HOMEのわざとらしいテンプレートだけ
削除したものを使用しています。

最小限のサービス

public class TestPlayerService extends Service {

    enum ACTION
    {
        TEST0 { @Override public String getCmd(){return "link.canter.hiroumauma.testplayer2.ACTION_TEST0"; }},
        TEST1 { @Override public String getCmd(){return "link.canter.hiroumauma.testplayer2.ACTION_TEST1"; }},;
        public String getCmd(){return name(); }
    }

    private final IBinder mBinder = (IBinder)new TestPlayerServiceLocalBinder();

    public class TestPlayerServiceLocalBinder extends Binder
    {
        TestPlayerService getService()
        {
            return TestPlayerService.this;
        }
    }

    @Override public void onCreate(){
        super.onCreate();
    }

    @Override public void onDestroy(){
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        Toast.makeText(this, "TestPlayerService#onBind"+":"+ intent, Toast.LENGTH_SHORT).show();
        return mBinder;
    }

    @Override
    public void onRebind(Intent intent)
    {
        Toast.makeText(this, "TestPlayerService#onRebind"+":"+ intent, Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onUnbind(Intent intent)
    {
        Toast.makeText(this, "TestPlayerService#onUnbind"+":"+ intent, Toast.LENGTH_SHORT).show();

        return true;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {

        String action = intent.getAction();

        if(action.equals(ACTION.TEST0.getCmd()))
        {
            Toast.makeText(this, "TestPlayerService#TEST0"+":"+ intent.getStringExtra("TEST_MSG"), Toast.LENGTH_SHORT).show();
        }else if(action.equals(ACTION.TEST1.getCmd())){
            Toast.makeText(this, "TestPlayerService#TEST1"+":"+ Integer.toString(intent.getIntExtra("TEST_ID",0)), Toast.LENGTH_SHORT).show();
        }

        return START_STICKY;
    }
}

これが今回作成した最低限のサービスです。

    enum ACTION
    {
        TEST0 { @Override public String getCmd(){return "link.canter.hiroumauma.testplayer2.ACTION_TEST0"; }},
        TEST1 { @Override public String getCmd(){return "link.canter.hiroumauma.testplayer2.ACTION_TEST1"; }},;
        public String getCmd(){return name(); }
    }

この部分はサービスが受け取れるコマンドのリストです。
今回はまだ中身がないので TEST0, TEST1 という名前を enum で並べたあと
enumをちょっと拡張してコマンドのフルネームも出せるようにしてあります。
(あとで効果を発揮します。)

    private final IBinder mBinder = (IBinder)new TestPlayerServiceLocalBinder();

    public class TestPlayerServiceLocalBinder extends Binder
    {
        TestPlayerService getService()
        {
            return TestPlayerService.this;
        }
    }

このあたりは後のための布石です。
ローカルバインダーを用いてアクティビティからサービスに直アクセスする技があるので
それの用意をしています。

その後
OnCreate, onDestroy
onBind, onRebind, onUnbind
などは今は何も書いていません。
基本的には名前の通りのメソッド達です。

最後
OnStartCommandが アクティビティやウィジェットから、
Intentを用いて要求が行われた際に働く部分です。

String action = intent.getAction();

で渡されたコマンドを受け取り

 if(action.equals(ACTION.TEST0.getCmd()))

こんな感じで用意しておいたコマンドを見つけることができます。
ここで、再生や一時停止、次曲送り、巻き戻し などのコマンドに
対する動きを作ればいいわけです。

最後に返している値でServiceの挙動が変わります。
STICKY は明示的に起動されて、明示的に止められるまで
背後で動作するモードです。

アクティビティ側の準備

ハリボテサービスができたのでアクティビティ側も準備します

まず

    public static TestPlayerService TestPlayerBoundService;
    public static boolean           IsTestPlayerServiceBound;

2つメンバを追加して

次に

    private ServiceConnection TestPlayerServiceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            Toast.makeText(Main.this, "Activity:onServiceConnected",Toast.LENGTH_SHORT).show();
            TestPlayerBoundService = ((TestPlayerService.TestPlayerServiceLocalBinder)service).getService();
        }

        public void onServiceDisconnected(ComponentName className) {
            TestPlayerBoundService = null;
            Toast.makeText(Main.this, "Activity:onServiceDisconnected",Toast.LENGTH_SHORT).show();
        }
    };

例のローカルバインダを使用して
TestPlayerBoundServiceを捕まえる
TestPlayerServiceConnection というのを作っておきます。

そうしたら

    public void doBindService(){
        bindService(new Intent(Main.this, TestPlayerService.class), TestPlayerServiceConnection, Context.BIND_AUTO_CREATE);
        IsTestPlayerServiceBound = true;
    }

    public void doUnbindService(){
        if(IsTestPlayerServiceBound){
            unbindService(TestPlayerServiceConnection);
            IsTestPlayerServiceBound = false;
        }
    }

サービスとアクティビティを Bing / Unbind するメソッドを追加
さらに

    public void CallService(ACTION action)
    {
        Intent intent = new Intent(this, TestPlayerService.class);
        intent.setAction(action.getCmd());
        switch(action)
        {
            case TEST0:
                intent.putExtra("TEST_MSG", "The quick brown fox jumps over the lazy dog");
                break;
            case TEST1:
                intent.putExtra("TEST_ID", 0523);
                break;
            default:
                break;
        }

        startService(intent);
    }

サービスにコマンドを与えるメソッドも作ってみました。
このメソッドの引数 ACTION は
先ほど Service側に用意した TestPlayerService.ACTION です。
ACTIONは enum で作ってあるので switch でズラズラかいて
大丈夫です。
setAction はコマンドのフルネームが必要ですが .getCmd() と
すればOKです。
enumのちょい拡張でこのあたりはスッキリします。

最後に
onCreate() 内に  doBindService(); を追記して
一通りの準備は完了です。

AndroidManifest

サービスを起動する場合
AndroidManifest.xmlに編集が必要です。

application の中に

<service android:name=".TestPlayerService"/>

などと追記します。(nameは読み替えて下さい)

以上で最低限のServiceの完成です。

テスト

早速作成したサービスにアクセスしてみます。
今回は、TEST0 コマンドでちょっとした文字列をサービスに渡してみます。

partXで作成した サイドナビのツールボタンと設定ボタンを借りて

            case R.id.nav_tools:
                CallService(ACTION.TEST0);
                break;
            case R.id.nav_settings:
                doUnbindService();
                stopService(new Intent(this, TestPlayerService.class));
                break;

この2コマンドだけは最低限用意します。
TEST0 コマンドを送信するテストと
サービスをアンバインドして停止させるテストです。

せっかくなので実機で

◯アプリ起動直後に Serviceが起動

続いて
◯アクティビティからサービスに接続

◯サイドメニューから ツールで TEST0 コマンドを送信
Serviceが送ったメッセージ(The quick brown fox jumps over the lazy dog) を受け取って
いるのが確認できます。 これができれば 曲のURIなどを送ることもできるはずです。

◯最後に サイドメニューから 設定で サービスを停止
アンバインドされた旨が表示されました。

ちゃんと動いているようです。

これで最低限Activityから切り離された部分で動くServiceの
下地ができました。
今回のように文字列が送れれば、再生や一時停止コマンド、
再生したい音楽のURI送信などなど
色々なコマンドが簡単に追加できるはずです。

次回以降は
Serviceに、音楽再生機能を仮組みし、
Activity側にはプレーヤーの画面を作っていきます。

コメントを追加する