サービスチュートリアル2

高度なサービス

AndroidではcomやCORBAの様にAndroidではサービスに対してリモートメソッド呼び出しをする機構を提供している。ここでは簡易音楽再生サービスを例にそのやり方を見ていく。comやCORBAではIDLにてインターフェースを定義していたが、AndroidではAIDLでインターフェースを定義し、インターフェイスコンパイラがインターフェースのためのjavaソースを作成する。eclipseで実施する場合は自動でインターフェースコンパイラが起動されるため、開発者はAIDLファイルを作成すれば良い。AIDLファイルはjavaのソースファイルと同様にsrcディレクトリの適切なパッケージディレクトリに配置する。

まず、下記のAIDLの例を見てみよう。ほぼ、javaでインターフェースを定義するのと同じになる。注意事項としては基本的なデータ以外はParcellableインターフェースを継承してシリアル化が可能になっていないと引数に渡せないということと、IDLと同様に引数の受け渡し方向にin,out,inoutを指定する必要があると言うこととなる。

package com.suddenAngerSystem;

interface MusicServiceInterface {
	//指定されたパスのファイルを再生する
	void start(in String path);
	//一時停止する
	void pause();
	//再開する
	void unPause();
}
	

上記のファイルMusicServiceInterface.aidlを更新するとeclipseでは以下の様にgenディレクトリ配下に自動生成したソースが格納される。

次に、サービスの提供側を見てみよう。今回ポイントとなるのはonBindメソッドとなる。onBindメソッドにてMusicServiceInterface.Stubクラスのサブクラスのインスタンスを返しているが、これがサービスの使用側に提供されるインターフェースとなる。もちろんそれは先ほどのaidlに沿った物としてinterfaceImpl_に実装している。つまり、サービスの使用側がstartメソッドを使用するとサービスの提供側としてはinterfaceImpl_のstartメソッドにて受け取れる事になる。

package com.suddenAngerSystem;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.IBinder;

public class MusicService2 extends Service {
	private MediaPlayer player_;
	@Override
	public void onCreate() {
		super.onCreate();
		player_ = null;
	}

	@Override
	public void onDestroy() {
		//サービスが破棄される際には再生を停止する
		player_.stop();
		//サービスが破棄される際はリソースを解放する
		player_.release();
		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return interfaceImpl_;
	}
	private void start(String path) {
		//前の情報が残っている場合は削除(本当はsetDataSourceで切り替えれば良いのだが、なぜか動作が安定しない。)
		if(player_ != null) {
			player_.stop();
			player_.release();
		}
		//SDカード内のファイルを読んだプレイヤーを生成
		Uri.Builder builder = new Uri.Builder();
		builder.path(path);
		builder.scheme("file");
		player_ = MediaPlayer.create(this, builder.build());
		player_.start();
	}
	private void pause() {
		player_.pause();
	}
	private void unPause() {
		player_.start();
	}

	private MusicServiceInterface.Stub interfaceImpl_ = new MusicServiceInterface.Stub() {
		public void start(String path) throws android.os.RemoteException {
			MusicService2.this.start(path);
		}
		public void pause() throws android.os.RemoteException {
			MusicService2.this.pause();
		}
		public void unPause() throws android.os.RemoteException {
			MusicService2.this.unPause();
		}
	};
}
	

次に、サービスの使用側を見てみよう。ちょっと長いソースとなるが、ポイントは限られている。onStartメソッドの中でbindServiceを起動してサービスにアプリケーションを登録している。二番目の引数にはServiceConnectionインターフェースを継承したService2自身を渡しており、サービスとアプリケーションが接続された時にonServiceConnectedが呼ばれ、切断された際にはonServiceDisconnectedが呼ばれるように設定している。そして、サービスとアプリケーションが接続された際の処理(onServiceConnected)ではMusicServiceInterfaceインターフェースを取得している。他の箇所はサービスを利用しているだけとなる。

package com.suddenAngerSystem;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class Service2 extends Activity implements ServiceConnection {
	private boolean isConnected_;
	private MusicServiceInterface playerInterface_;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        isConnected_ = false;
        playerInterface_ = null;

        final Button start_button = (Button)findViewById(R.id.start_button);
        start_button.setOnClickListener(
        	new OnClickListener() {
        		@Override
        		public void onClick(View v) {
        			if(playerInterface_ == null) {
        				return;
        			}
        			final EditText edit = (EditText)findViewById(R.id.music_path);
        			final String path = edit.getText().toString();
        			try {
        				playerInterface_.start(path);
        			}
        			catch(Exception e) {
        				//例のためエラーハンドリングしない
        			}
        		}
        	}
        );

        final Button stop_button = (Button)findViewById(R.id.stop_button);
        stop_button.setOnClickListener(
        	new OnClickListener() {
        		@Override
        		public void onClick(View v) {
        			//サービスの接続を解除する
        			unbindService(Service2.this);
          		//引数のIntentを渡してサービスをストップする
          		stopService(new Intent(Service2.this, MusicService2.class));
        		}
        	}
        );

        final Button pause_button = (Button)findViewById(R.id.pause_button);
        pause_button.setOnClickListener(
        	new OnClickListener() {
        		private boolean isPause_ = false;
        		@Override
        		public void onClick(View v) {
        			if(playerInterface_ == null) {
        				return;
        			}
        			try {
        				Button pause_button = (Button)v;
        				if(isPause_ == false) {
        					playerInterface_.pause();
        					pause_button.setText("再開");
        					isPause_ = true;
        				}
        				else {
        					playerInterface_.unPause();
        					pause_button.setText("一時停止");
        					isPause_ = false;
        				}
        			}
        			catch(Exception e) {
        				//例のためエラーハンドリングしない
        			}
        		}
        	}
        );
        
        //引数のIntentを渡してサービスをスタートする(これをやらないと、バインドを解除したタイミングでサービスが死ぬ)
				startService(new Intent(Service2.this, MusicService2.class));
    }

    @Override
    protected void onStart() {
    	super.onStart();
    	//接続されていない場合は接続する
    	if(isConnected_ == false) {
    		bindService(new Intent(this, MusicService2.class),  this, Context.BIND_AUTO_CREATE);
    		isConnected_ = true;
    	}
    }

    @Override
	public void onServiceConnected(ComponentName name, IBinder service) {
    	//決まり切ったインターフェースの取得処理
    	playerInterface_ = MusicServiceInterface.Stub.asInterface(service);
	}
	@Override
	public void onServiceDisconnected(ComponentName name) {
		//インターフェースを無効にする
		playerInterface_ = null;
	}

}
	
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

	<Button android:text="指定パスのファイルを再生" android:id="@+id/start_button" android:layout_width="fill_parent" android:layout_height="wrap_content"></Button>
	<EditText android:text="path" android:id="@+id/music_path" android:layout_width="fill_parent" android:layout_height="wrap_content"></EditText>
	<Button android:text="一時停止" android:id="@+id/pause_button" android:layout_width="fill_parent" android:layout_height="wrap_content"></Button>
	<Button android:text="再生終了" android:id="@+id/stop_button" android:layout_width="fill_parent" android:layout_height="wrap_content"></Button>
</LinearLayout>
	

高度なサービスその2

サービスを使用する際にポーリングベースではなく、何らかのトリガーでサービスがイベント通知してくれる形式の方が便利な場合がある。そういった用途に対してはコールバックを使用すればよい。コールバックの仕組みは簡単で以下の様になる。

  1. サービス側が提供しているインターフェースを取得(前の例と同じ)
  2. サービス側が提供しているインターフェースを使用してコールバックを登録
  3. サービス側が登録されたコールバックを起動

具体的にはコールバックのためのインターフェースをサービスの提供側で定義し(要はaidlファイルをサービスの提供側で提供する)、サービスの使用側はその定義に従った実装をする事となる。また、コールバックが起動されるスレッドはアプリケーションのメインスレッドとは異なっており、Androidはビューを作成したスレッド以外からビューの更新ができない事に注意が必要となる。例えば、コールバックで起動されたメソッド内でfindViewByIdでビューを取得し、ビューの値の変更はできない。Androidではそれの解決のため、Handlerクラスを提供している。

Handlerクラスは子クラスにてデフォルトのメッセージ受信処理を拡張するクラスで、以下の様にキューの様な働きをする。

  1. HandlerクラスのインスタンスにsendMessageでメッセージをキューイング
  2. メインスレッドでHandlerクラスのhandleMessageが起動される。引数にはsendMessageで指定したメッセージが順番に渡される。
  3. オーバーライドしたhandleMessageで任意の処理を実施する。

また、メインスレッドから起動する様にする登録はコンストラクタで自動的にしてくれている。

例としてコールバック処理を利用して先ほどの例に機能追加をしてサービス提供側が再生時間を定期的に通知させる。まずはAIDLから見てみよう。コールバック用のインターフェースを以下の様に定義する。

package com.suddenAngerSystem;

interface ListenPlayTimeInterface {
	//残り時間を通知する
	void onPlayTimeNotify(in int time);
}
	

次にコールバックを登録するために元々のAIDLを変更する

package com.suddenAngerSystem;

//なぜか同じパッケージなのにimportしないと使えない。
import com.suddenAngerSystem.ListenPlayTimeInterface;

interface MusicServiceInterface {
	//指定されたパスのファイルを再生する
	void start(in String path);
	//一時停止する
	void pause();
	//再開する
	void unPause();
	//残り時間を定期的に受け取るリスナーを登録
	void setOnPlayTimeNotify(ListenPlayTimeInterface ptInterface);
}
	

サービス使用側におけるコールバックの実装は以下の様になる。

	private ListenPlayTimeInterface.Stub interfaceImpl_ = new ListenPlayTimeInterface.Stub() {
		@Override
		public void onPlayTimeNotify(int time) throws RemoteException {
			//ハンドラにメッセージを送信
			//メッセージはいちいち作成するとコストがかかるため、メッセージプールにあるインスタンスを使用する(obtainMessage)
			//送信内容はメッセージ種別と引数(引数に応じてオーバーロードされているobtainMessageのバリエーションが使える)
			handler_.sendMessage(handler_.obtainMessage(TIME_NOTIFIED, time, 0));
		}
	};

	private Handler handler_ = new Handler() {
		public void handleMessage(android.os.Message msg) {
			switch(msg.what) {
			case TIME_NOTIFIED:
				//通知された時間を表示する。
				final TextView text = (TextView)findViewById(R.id.play_time);
				final String minute = Integer.toString(msg.arg1/60000);
				final String second = Integer.toString(msg.arg1%60000/1000);
				text.setText(minute+":"+second);
				break;
			default:
				super.handleMessage(msg);
				break;
			}
		}
	};
	

また、サービス使用側でコールバックを登録する処理が必要となるため、startボタンが押された際のリスナーを以下の様に修正する。

        final Button start_button = (Button)findViewById(R.id.start_button);
        start_button.setOnClickListener(
        	new OnClickListener() {
        		@Override
        		public void onClick(View v) {
        			if(playerInterface_ == null) {
        				return;
        			}
        			final EditText edit = (EditText)findViewById(R.id.music_path);
        			final String path = edit.getText().toString();
        			try {
        				playerInterface_.start(path);
        				playerInterface_.setOnPlayTimeNotify(interfaceImpl_);
        			}
        			catch(Exception e) {
        				//例のためエラーハンドリングしない
        			}
        		}
        	}
        );
	

定期的に通知させるためにjavaの標準ライブラリのTimerTaskクラスとTimerクラスを使用している。処理としては単純に登録要求が来た際(setOnPlayTimeNotify)にコールバックを記憶し、定期的にプレイヤーから再生時間を取得してコールバックを起動しているだけである。

package com.suddenAngerSystem;

import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;

public class MusicService2 extends Service {
	private MediaPlayer player_;
	private ListenPlayTimeInterface listener_;
	private Timer timer_;
	@Override
	public void onCreate() {
		super.onCreate();
		player_ = null;
		listener_ = null;
		timer_ = new Timer();
	}

	@Override
	public void onDestroy() {
		//サービスが破棄される際には再生を停止する
		player_.stop();
		//サービスが破棄される際はリソースを解放する
		player_.release();
		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return interfaceImpl_;
	}

	private void start(String path) {
		//前の情報が残っている場合は削除(本当はsetDataSourceで切り替えれば良いのだが、なぜか動作が安定しない。)
		if(player_ != null && player_.isPlaying()) {
			player_.stop();
			player_.release();
			timer_.cancel();
		}
		//SDカード内のファイルを読んだプレイヤーを生成
		Uri.Builder builder = new Uri.Builder();
		builder.path(path);
		builder.scheme("file");
		player_ = MediaPlayer.create(this, builder.build());
		player_.start();
		//残り時間の通知を開始
		timer_.schedule(new NotifyPlayTime(this), 10, 1000);
	}
	private void pause() {
		player_.pause();
	}
	private void unPause() {
		player_.start();
	}
	public void onTime() {
		if(listener_ != null && player_ != null) {
			try {
				listener_.onPlayTimeNotify(player_.getCurrentPosition());
			}
			catch(Exception e) {
				//例のためエラーハンドリングしない
			}
		}
	}

	private MusicServiceInterface.Stub interfaceImpl_ = new MusicServiceInterface.Stub() {
		public void start(String path) throws android.os.RemoteException {
			MusicService2.this.start(path);
		}
		public void pause() throws android.os.RemoteException {
			MusicService2.this.pause();
		}
		public void unPause() throws android.os.RemoteException {
			MusicService2.this.unPause();
		}
		public void setOnPlayTimeNotify(ListenPlayTimeInterface ptInterface) throws RemoteException {
			MusicService2.this.listener_ = ptInterface;
		}
	};
}

//あんまり良くない設計だけど単純化のため
class NotifyPlayTime extends TimerTask {
	private MusicService2 musicService_;
	public NotifyPlayTime(MusicService2 musicService) {
		musicService_ = musicService;
	}
	@Override
	public void run() {
		try {
			musicService_.onTime();
		}
		catch(Exception e) {
			//例のためエラーハンドリングしない
		}
	}
}
	

参考資料