この文書は本書「Androidアプリ開発逆引き大全500の極意」(秀和システム刊、ISBN978-4-7980-3734-9)で紹介しきれなかった内容をまとめたものです。

目次へ
清水美樹の本 トップページへ

サービスと通知のコード例

サービス、メッセンジャーとハンドラー、そして通知

一番ピンと来なさそうなところをまとめて

本書は「Tips集」であり、あまり長いコードをまとめて御紹介するものではありません。しかし、「サービス」「メッセンジャー」「通知」という、第5章に書いたアプリの考え方は、初めて目にした方は何が問題になっているのかピンとこないのではないかと懸念されます。そこで、これらの要素を一気にまとめたプログラムを紹介したいと思います。

数値を入力して送信すると積算の値を返すサービス

アプリの完成イメージは、図1のようなものです。画面の下のほうにあるテキスト入力欄に数値を入れて「数値を送信」と書かれたボタンをクリックすると、これまでに送信した値を積算した値が帰ってきます。
図1 サービスのテストプログラムが動作する様子
サービスが始まると、画面の左上に通知アイコンが表示されます。AVD(Tips020-025)だとこれをクリックして、マウスで下にひっぱって通知を表示させます(AVDは重いので、結構えいやっと引っ張る必要があります)
図2 AVDだとマウスでひっぱる



アプリの構成

サービスGBServiceとアクティビティGBServiceActivity

ここでは、プロジェクト名を「GBMessenger」とします。サービス「GBService」とアクティビティ「GBServiceActivity」を作成し、「AndroidManifest.xml」に登録します。(ウィザードを用いれば登録も自動でなされます - Tips056, 335
アクティビティ「GBServiceActivity」のほうは、インテントフィルタのアクションを「MAIN」、カテゴリを「LAUNCHER」にします。アプリの起動とともにこのアクティビティが起動するようにするためです。(Tips297)
一方、「GBService」のほうは、とくにインテントフィルタを指定しません。サービスの起動には、クラス名を指定します。
図3 AndroidManifest.xmlへの登録



レイアウト

図1にはボタンが5つ、テキストフィールド(EditText)が1つ、テキストビューが2つあります。
idが必要なのは、そのうち数値を入力するテキストフィールドと、積算値を表示するテキストビューの2つだけです。
それぞれR.id.editCount, R.id.textCountというID(Java上での値)を与えておきます。(Tips172)
各ボタンに対するメソッドは、レイアウトエディタ上で「On Click」プロパティに割り当てます(Tips221)。
図4 IDが必要な部品は二つだけ


GBServiceの定義

サービスのonCreateメソッド

サービスのonCreateメソッドは、サービスが開始したときに呼ばれます。そこで、サービス開始の通知を行うshowNotificationというメソッドを呼びます。このメソッドは自分で作るもので、このあとに定義します。
showNotificationはNotificationManagerを必要とするので、これをオブジェクトとしてメンバー変数mNMに渡します(Tips351)。 また、「積算値」となるmCountを初期化しておきます。
リスト1 onCreateメソッド。showNotificationはこのあと定義
private NotificationManager mNM;
	int mCount;
	
	@Override
	public void onCreate() {
		mCount=0;
		
		mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
		showNotification();
	
   }




showNotificationメソッド

Notification.Builderクラスを用いた通知の方法です。R.string...およびR.drawable....で示されるリソースは、適宜準備してください。

なお、本書におけるNotification.Builderの解説に誤りがありました。(Tips352のリスト2および(Tips353)のリスト3です。
メソッドsetContentTitle, SetContentStringの引数に、R.string.text_shortなどリソースIDを直接入れた記述になっています。
そこは、下のリスト2のように、getString, getTextなどのメソッドでCharSequenceの形にしなければなりません(Tips078)。
「間違えやすいので気をつけましょう」と書いておりますがやはり間違えました。お詫びして訂正させていただきます。
リスト2 showNotificationメソッド
	private void showNotification() {
		
		PendingIntent contentIntent = PendingIntent.getActivity(
    			this, 0, 
    			new Intent(this, GBServiceActivity.class), 0);
		
		Notification notif= new Notification.Builder(this)
		.setContentTitle(getString(R.string.app_name))
		.setContentText(getText(R.string.service_started))
		.setSmallIcon(R.drawable.bickie_square_48)
		.setContentIntent(contentIntent)
		.build();
		
		mNM.notify(1, notif);

	}

メソッドonStartCommand

onStartCommandは、アクティビティから開始させられたときに呼ばれるメソッドです(Tips336)。ログに記録する(Tips112)などの処理が考えられますが、ここでは特に記述しません。
リスト3 メソッドonStartCommand
@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		
		//ログに記録するなどの処理
		
		return START_STICKY;
	}



メソッドonDestroy

サービスを停止するときは、出ていた通知を取り消します。サービスの停止は通知バーではなくToastで知らせるのが簡単です(Tips317)。
リスト3 メソッドonDelete
@Override
public void onDestroy() {
	//通知をキャンセル
	mNM.cancel(1);
	
	Toast.makeText(this, R.string.service_stopped, Toast.LENGTH_SHORT).show();
}



ハンドラーとメッセンジャー

サービスで行う作業は、別スレッドで行うのが普通ですが、ハンドラーを使うのが書きやすいと思います(Tips343-346)
サービス側のハンドラーとしてmCountHandlerを定義します。そして、このハンドラーにメッセージを送るmServiceMessengerも定義します。
リスト4 mCountHandler
Handler mCountHandler = new Handler(){
		
	@Override
	 public void handleMessage(Message msg){
		super.handleMessage(msg);

		mCount=mCount+msg.arg1;
		
		
		Message replyMessage = Message.obtain(null,0, mCount, 0);
	    try{
	    	(msg.replyTo).send(replyMessage);
	    }catch(RemoteException e){}
		
	}
 };
 
 final Messenger mServiceMessenger = new Messenger(mCountHandler);	
 


onBindメソッド

最後に、このサービスに接続したアクティビティに「IBinder」を返すメソッドonBindです(Tips336,344)。
リスト5 onBindメソッド
@Override
public IBinder onBind(Intent intent) {
	return mServiceMessenger.getBinder();
}



以上、GBServiceの内容でした。パッケージとインポートの宣言は、省略しています。Eclipseが自動で記入してくれます(Tips121)。


GBServiceActivityの内容

メンバー変数の宣言とonCreateメソッド

ここまでは、普通のアクティビティと同じようなものです。
リスト6 変数の宣言とonCreateメソッド
private boolean mIsBound;
private boolean mIsRunning;
private TextView mTextCount;
private EditText mEditCount;
private Messenger mServiceMessenger;

@Override
protected void onCreate(Bundle savedInstanceState) {
	
	super.onCreate(savedInstanceState);
	setContentView(R.layout.gbmessenger);
	mIsRunning=false;
	
	mTextCount=(TextView)findViewById(R.id.textCount);
	mEditCount=(EditText)findViewById(R.id.inputChange);
	
}
	


リスト6で、メンバー変数mIsRunningとmIsBoundはそれぞれ、サービスを起動させたか、サービスに接続の命令を出したかどうかを示すフラグです。本当にサービスの動作を検知するものではなく、ボタンを押したかどうかという備忘録的なフラグです。


サービスの開始・停止

サービスの開始、停止メソッドです(Tips337)。それぞれレイアウトエディタ上でボタンに割り当ててください。
リスト7 サービスの開始・停止メソッド
public void onBtStartClick(View v) {
	startService(new Intent(this, GBService.class));
	mIsRunning=true;
}

public void onBtStopClick(View v) {
	stopService(new Intent(this, GBService.class));
	if(mIsRunning==true){
		mIsRunning=false;
	}
}


ServiceConnectionオブジェクトの取得

サービスへの接続に使うServiceConnectionオブジェクトを作成し、メンバー変数mConnectionに割り当てます。(Tips340)
リスト9 ServiceConnectionオブジェクトを取得
	private ServiceConnection mConnection = new ServiceConnection(){
		public void onServiceConnected(ComponentName className, IBinder service){
			
			
			mServiceMessenger = new Messenger(service);
			
			Toast.makeText(GBServiceActivity.this, R.string.bind_service,
					Toast.LENGTH_SHORT).show();	
		}
		
		public void onServiceDisconnected(ComponentName className){
			Toast.makeText(GBServiceActivity.this, R.string.unbind_service, 
					Toast.LENGTH_SHORT).show();				
		}
	};
	


リスト9には重要な点が2つあります。

(1)mServiceMessengerも満たされた
クライアントがサービスへメッセージを送るには、サービスのメッセンジャーを取得する必要があります。それがリスト9においてmServiceMessengerに保持されました。

(2)Toast.makeTextのコンテキストは「GBServiceActivity.this」である

リスト9でToastが記述されているところは新しいServiceConnectionオブジェクトの定義なので、いつもの調子で「this」と書いてしまうと、ServiceConnectionオブジェクトのことになってしまいます。起動中のアクティビティのオブジェクトであることを示すには、「GBServiceActivity.this」を用います。

mConectionを用いたサービスへの接続/接続解除

リスト9で得られたmConnectionを用いて、サービスへの接続/接続解除のメソッドをリスト10のように記述します。(Tips338)
これらのメソッドは、レイアウトエディタ上で、適当なボタンに割り当ててください。
リスト10 サービスへの接続/接続解除
public void onBtBindClick(View v) {
	bindService(new Intent(this, GBService.class), mConnection, Context.BIND_AUTO_CREATE);
	mIsBound = true;
	if(mIsRunning==false){mIsRunning=true;}
}

public void onBtUnbindClick(View v) {
	if(mIsBound){
		unbindService(mConnection);
		mIsBound = false;
	}
}


クライアント側のメッセンジャーとハンドラ

サービス側がreplyToで参照するメッセンジャーと、そのメッセージを受け取るハンドラーを定義します。(Tips345)
リスト11 クライアントのメッセンジャーとハンドラー。サーバーの応答を受け取って処理する
Handler mClientHandler =new Handler(){
	
	@Override
	 public void handleMessage(Message msg){
		super.handleMessage(msg);
		
		mTextCount.setText(Integer.toString(msg.arg1));
		
	}
	
 };
 
 final Messenger mClientMessenger= new Messenger(mClientHandler);



リスト11でハンドラーが行う仕事は、メッセンジャーが受け取ってきたmsgの値をテキストフィールドに表示させることです。


数値を送信するメソッド

サービスに数値を送信するメソッドは、リスト12のようになります。このメソッドは、適当なボタンに割り当てます。
リスト12 サービスに数値を送信する
public void onBtCountClick(View v){

	//アクティビティがサービスに接続していない状態でメッセージを送るとエラーになる!
	if(mIsBound==false){
		Toast.makeText(this, R.string.please_bind, Toast.LENGTH_SHORT).show();
			//「サービスに接続してください」とメッセージを出す
			
		return; //処理を中断して強制終了を回避
	}

	int num = Integer.parseInt((mEditCount.getText()).toString());
	
	Message msg = Message.obtain(null,0,num, 0);
	msg.replyTo = mClientMessenger;
			
	try{
		
	    mServiceMessenger.send(msg);
	
	}catch(RemoteException e){}
				
}


リスト12のメソッドで、サービスの開始とメッセージの送信を両方書けばいいのではないかという考えもありますが、お勧めしません。
サービスは別スレッドなので、サービスの起動にメッセージの送信が間に合わないかも知れないからです。特に、処理の遅いエミュレーターでは危険です。別のボタンで接続を確立してから、メッセージを送ることをお勧めします。

動作確認

アクティビティがサービスに接続中は、サービスが停止しないことに注意

実行したら、以下のことを確かめてください。

サービスを開始するボタンをクリックすると、通知が現れる。
そのまま(接続せずに)サービスを終了させるボタンをクリックすると、通知が引っ込んで、「サービスが終了しました」というToastが現れる。
サービスを開始するボタンを押さなくても、サービスに接続するボタンを押せばサービスが開始する。通知が出て、かつ「サービスに接続しました」というToastが出る。
サービスが開始されない状態で数値を送信しようとすると、「サービスに接続してください」というToastが出る
サービスに接続した状態で「サービス終了」ボタンを押してもサービスは終了せず、続いてサービスからの接続解除したときにはじめて終了する(通知が引っ込む)
サービスを開始するボタンをクリックしてから接続すると、接続を解除してもサービスは終了しない
接続のみによって開始したサービスは接続解除とともに終了する。


目次へ