Service - Bindあり - ServiceからのCallback

さて前回に引き続きServiceの実装を試してみます。
今回はServiceからのCallbackを利用出来る様にしてみたいと思います。
BackgroundでServiceを走らせておいて「音楽の再生が終わったよ」というようなServiceで行っていた処理に何らかのEventが発生したらActivityに通知したいというような時に使用できます。
今回、実現する機能はServiceからのCallbackをもとにTextViewに表示するStringをupdateするというものです。

実装の要点をまとめます

Activity側
  1. Activity用のAIDLにCallback interfaceを定義
  2. ActivityにServiceからのCallbackを受け取るためのCallback interfaceを実装
  3. ActivityにServiceとBindするContext#bindService() methodを記述
  4. ActivityにServiceとUnbindするContext#unbindService() methodを記述
  5. ServiceConnection interfaceを実装したclassを作成
  6. 上記ServiceConnection#onServiceConnected()の引数として渡されるIBinder objectを使用しService用のAIDLで定義されたinterfaceを取得
  7. 上記で取得したinterfaceを利用しAIDLで定義されたmethodにてObserver登録/解除
Service側
  1. manifest xmlにservice tagを追加
  2. Service用のAIDLにCallback interfaceをObserver登録/解除するinterfaceを定義
  3. ServiceでAIDL fileに定義したinterfaceの実装
    • RemoteCallbackListというClassを用意しCallback Interfaceを保持する
    • Observer登録method内でRemoteCallbackList#register() methodで引数に渡されたCallback interfaceを登録する
    • Observer解除method内でRemoteCallbackList#register() methodで引数に渡されたCallback interfaceを削除する
  4. Service#onBind()の戻り値としてNo.3で実装したclassのobjectを返す
  5. Callback intefaceのCallback methodを呼び出す為にRemoteCallbackList#beginBroadcat()でCallbackの準備
  6. RemoteCallbackList#getBroadcastItem()でCallback interfaceを取り出しCallback methodを呼び出す
  7. RemoteCallbackList#finishBroadcast()でbroadcast stateをclear

気づいた点をまとめます

  • AIDLで定義されたmethodはBinder thread上で呼ばれます。ですので、それらのmethod内で直接はUIの操作は出来ません。Activityで実装したCallback interfaceだろうが、Serviceで実装したObserver登録/解除のmethodだろうが同じです(ここではUI threadで生成したHandlerを使用してUIを更新してます)
  • AIDL内ではimport文は自動生成されないので自前で記述する

んで、実装です

Callback interfaceを定義したAIDL(Activity用)

package com.android.practice;

//Activityで実装する用のCallback interfaceを定義
interface MyServiceObserver{
    void onDataReady(String message);
}

Activity

package com.android.practice;

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.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MyActivity extends Activity {
	
	private static final String TAG = "MyActivity";
	
	private static final int TEXT_UPDATE = 0;
	
	private Context mContext;
	private Handler mHandler;
	private MyServiceObserver mObserver;
	private ServiceConnection mConnection;
	private MyBindService mService;
	private TextView mTextView;
	private boolean mBind = false;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
    	
        super.onCreate(savedInstanceState);
        mContext = this;
        
        setContentView(R.layout.main);
        mTextView = (TextView)findViewById(R.id.status_text);
        Button bindButton = (Button)findViewById(R.id.bind_button);
        Button unbindButton = (Button)findViewById(R.id.unbind_button);
        Button startButton = (Button)findViewById(R.id.start_button);
        Button stopButton = (Button)findViewById(R.id.stop_button);
        Button stringButton = (Button)findViewById(R.id.get_string_button);
        
        mHandler = new Handler(){

			@Override
			public void handleMessage(Message msg) {
				switch(msg.what){
				case TEXT_UPDATE:
					mTextView.setText((String)msg.obj);
				}
			}
        };
        
        //ActivityにServiceからのCallbackを受け取るためのCallback interfaceを実装
        mObserver = new MyServiceObserver.Stub() {
			//Callback methodを実装
			public void onDataReady(String message) throws RemoteException {
				Log.d(TAG, "onDataReady called by " + Thread.currentThread().getName());
				//AIDLに定義したmethodはBinder thread上で実行されるのでUIの操作は出来ない
				//UIをupdateするためHandlerを使用
				Message msg = mHandler.obtainMessage(TEXT_UPDATE, message);
				mHandler.sendMessage(msg);
			}
		};
        
        //ServiceConnectionを拡張したclassを実装する
        mConnection = new ServiceConnection(){
        	
        	//ServiceConnection#onServiceConntected()の引数で渡される
        	//IBinder objectを利用しAIDLで定義したInterfaceを取得
    		public void onServiceConnected(ComponentName name, IBinder service) {
    			mService = MyBindService.Stub.asInterface(service);
    			Toast.makeText(mContext, "ServiceConnection onServiceConnected called by "
    			      + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
    			try{
    				//取得したinterfaceを利用しService用のAIDL fileで定義されたmethodでObserver登録/解除
    				mService.setObserver(mObserver);
    			}catch(RemoteException e){
    			}
				mBind = true;
    		}
    		
            //Serviceを動かしてるProcessがcrashするかkillされない限り呼ばれない
    		public void onServiceDisconnected(ComponentName name) {
    			mService = null;
    			Toast.makeText(mContext, "ServiceConnection onServiceDisConnected calle by "
    			      + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
    		}
    	};
    	
        bindButton.setOnClickListener(new OnClickListener(){
			public void onClick(View v) {
				//Intentを作成
				Intent intent = new Intent(mContext, MyService.class);
				//Context#bindService()でServiceとのConnectionを確率
				bindService(intent, mConnection, BIND_AUTO_CREATE);
			}
        });
        
        unbindButton.setOnClickListener(new OnClickListener(){
			public void onClick(View v) {
				if(mBind){
					try {
	    				//取得したinterfaceを利用しService用のAIDL fileで定義されたmethodでObserver登録/解除
						mService.removeObserver(mObserver);
					} catch (RemoteException e) {
					}
					//Context#UnbindService()でServiceとのConnectionを破棄
					unbindService(mConnection);
					mBind = false;
				}
			}
        });
        
        startButton.setOnClickListener(new OnClickListener(){
			public void onClick(View v) {
				Intent intent = new Intent(mContext, MyService.class);
				startService(intent);
			}
        });
        
        stopButton.setOnClickListener(new OnClickListener(){
			public void onClick(View v) {
				Intent intent = new Intent(mContext, MyService.class);
				stopService(intent);
			}
        });
        
        stringButton.setOnClickListener(new OnClickListener(){
			public void onClick(View v) {
				try{
					if(mBind){
						mTextView.setText((CharSequence)mService.getString());
					}
				}catch(RemoteException e){
				}
			}
        });
    }
}

Observerを登録/解除するmethodを定義するAIDL(Service用)

package com.android.practice;

//AIDL内ではimport文は自動生成されないので自前で記述する
import com.android.practice.MyServiceObserver;

//AIDL fileにServiceで使用するinterfaceを定義
//Eclipseで保存するとjava fileが生成されます
interface MyBindService{
	
	String getString();
	
	//AIDL fileで定義されるmethodの引数、戻り値に指定出来るのはprimitive型、String、
	//List、Map、CharSequence、その他のAIDLに定義されたinterface、Parcelable interfaceを実装したclass
	//MyServiceObserverはAIDLで定義されているのでOK
	void setObserver(MyServiceObserver observer);
	void removeObserver(MyServiceObserver observer);
}

Service

package com.android.practice;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;

public class MyService extends Service {
	
	private static final String TAG = "MyService";
	
	private static final int TIMER_TASK = 0;
	private static final long DELAY = 1000L;
	
	private Context mContext;
	private Handler mHandler;
	private int mCount = 1;
	
	//RemoteCallbackListというClassを用意しCallback Interfaceを保持する
	private RemoteCallbackList<MyServiceObserver> mObservers = new RemoteCallbackList<MyServiceObserver>();
	
	//ServiceでAIDL fileに定義したinterfaceの実装
	//ただし、生成されたjava fileのStub classを実装する
	private final MyBindService.Stub mMyBindService = new MyBindService.Stub() {
		
		public String getString() throws RemoteException {
			Log.d(TAG, "getString called by " + Thread.currentThread().getName());
			return "This string is provided by Service";
		}

		//Observer登録method内でRemoteCallbackList#register() methodで引数に渡されたCallback interfaceを登録する
		public void setObserver(MyServiceObserver observer)
				throws RemoteException {
			Log.d(TAG, "setObserver called by " + Thread.currentThread().getName());
			mObservers.register(observer);			
		}
		
		//Observer解除method内でRemoteCallbackList#register() methodで引数に渡されたCallback interfaceを削除する
		public void removeObserver(MyServiceObserver observer)
				throws RemoteException {
			Log.d(TAG, "removeObserver called by " + Thread.currentThread().getName());
			mObservers.unregister(observer);
		}
	}; 
	
	//生成したStub classをService#onBind()でreturnする
	@Override
	public IBinder onBind(Intent arg0) {
		Toast.makeText(mContext, "onBind called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		mHandler.sendEmptyMessage(TIMER_TASK);
		return mMyBindService;
	}

	@Override
	public boolean onUnbind(Intent intent) {
		Toast.makeText(mContext, "onUnbind called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		return true;
	}

	@Override
	public void onCreate() {
		mContext = this;
		
		mHandler = new Handler(){

			@Override
			public void handleMessage(Message msg) {
				switch(msg.what){
				case TIMER_TASK:
					//Callback intefaceのCallback methodを呼び出す為に
					//RemoteCallbackList#beginBroadcat()でCallbackの準備
					int observerNum = mObservers.beginBroadcast();
					for(int i = 0; i < observerNum; i++){
						try {
							//RemoteCallbackList#getBroadcastItem()でCallback interfaceを取り出し
							//Callback methodを呼び出す
							mObservers.getBroadcastItem(i).onDataReady(
									"This is the text provided by MyService. Count: " + mCount);
						} catch (RemoteException e) {
						}
					}
					//RemoteCallbackList#finishBroadcast()でbroadcast stateをclear
					mObservers.finishBroadcast();
					mCount++;
					sendEmptyMessageDelayed(TIMER_TASK, DELAY);
				}
			}
			
		};
		
		Toast.makeText(mContext, "onCreate called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		super.onCreate();
	}

	@Override
	public void onDestroy() {
		Toast.makeText(mContext, "onDestroy called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		super.onDestroy();
	}

	@Override
	public void onRebind(Intent intent) {
		Toast.makeText(mContext, "onRebind called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		super.onRebind(intent);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Toast.makeText(mContext, "onStartCommand called by "
			  + Thread.currentThread().getName(), Toast.LENGTH_LONG).show();
		return START_STICKY;
	}
}

というような感じです。もう一つMessengerというものを使用しActivityとService間でやりとりをする方法があるようです。
次回、それを実装してみるかどうかは乞うご期待!

HastTag #android, #java