AndroidのRemote Serviceについて(+作り方)

ローカルサービスについては“AndroidのServiceについて”に書いたので、ここではリモートサービスについて書き留めておく。ここで取り上げるリモートサービスとはサービスとサービスを呼び出すクライアントが別々のアプリケーションとして動作しているケースを指す。(後で実際に動かしてみたサンプルを載せておくが、サービスとクライアントは別々のアプリケーション、別々のパッケージとして動かしている)。

基本的はローカルもリモートも同じサービスには変わりないのでstartServiceでサービスを起動、stopServiceでサービスを終了、bindServiceでサービスとの接続を確立、unbindServiceでサービスとの接続を切断、というのは全く同じ(詳しくは“AndroidのServiceについて”参照)。

ただ、リモートサービスの場合は、サービスのオブジェクトを直接参照したりメソッドを呼び出したりできないのでAIDL(Android Interface Definition Language)と言う仕組みを利用する。AIDLはRPCとデータのmarshall(セッション層とプレゼンテーション層に相当する模様、総称してIPC:interprocess communication)を実現するコードを自動生成してくれる便利ツールのようなもんだ。クライアントとサービス間で利用するデータやメソッドの“型”だけを定義しておくとAIDLが自動的にプロセス間でやりとりするためのプログラムを生成してくれる。(つまり、AIDLがなければユーザが自分でIPCのためのコードを書かなければならない。)なお、メソッドの本体はAIDLで自動生成するというわけには行かないのでサービス本体の中で定義しなければならない。

以下にサンプルを紹介しておく。(最初にEclipseを使ったサービスとクライアントの作成方法を紹介して、プログラム本体はその後にリストしておく。)

なお、このサンプルは次のような構成となっている。

  • サービス:クライアントからの依頼でステータスバーに“△”と“▽”のNotificationを交互に出す。サービス自身は専用のプロセス(アプリケーション)として実行する。Viewは持たない(デーモンの様な感じ)。パッケージ名は com.example.android.remote_service。
  • クライアント:Viewを持ち画面に1つだけボタンを配置する。ボタンを押すとサービスを呼出し、アイコンの向きを変えてもらう。パッケージ名は com.example.android.remote_service_client 。
  • サービスとクライアントのインタフェースはIChageIconというオブジェクトで実現する。これをAIDLで定義する。

この様に非常に単純でクライアントとサービスを合わせても100行ちょっと程度。あくまでもリモートサービスの実装の方法を知るためのもの。

サービスのアプリケーションを作成する

Eclipseを起動し、[File]ー[New]ー[Android Project]を選択する。

New Android Projectダイアログでは、

  • プロジェクト名(Remote_Service)
  • ビルドターゲット
  • アプリケーション名(RemoteService)
  • パッケージ名(com.example.android.remote_service)

を入力するところまではいつもの通りだが、サービス専用のアプリケーションにするので、Create Activityのチェックは外し、アクティビティ名も空白のままで[Finish]をクリックする。

プロジェクトを作成したら、パッケージエクスプローラでAndroidManifest.xmlを選択し、右側のエディタで[Application]タブを選択する。そして[Add...]ボタンをクリックする。

作成する要素を選択するダイアログが表示されるので“Service”を選択して、[OK]をクリックする。

エディタ画面に戻ったら、今作成したServiceを選択されていることを確認し、右側のNameラベルのリンクをクリックする。

New Java Classのダイアログが出たら、ソースフォルダ、パッケージ名を確認の上、クラス名を入力(RemoteService)し、“Inherited abstract methods”にチェックして[Finish]をクリックする。

するとサービスの雛形が作成され、自動的にエディタ画面へ移る。(マニフェストは“保存”しておくこと。)

次にAIDLファイルを作成する。左側のパッケージエクスプローラで、“src/com.example.android.remote_service”を選択し、マウスのボタンからコンテキストメニューを出す。コンテキストメニューの[New]ー[File]を選択する。

New Fileダイアログが出たらファイル名(IChangeIcon.aidl)を入力し[Finish]をクリックする。

IChangeIcon.aidlのエディタ画面へ移るので、AIDL定義を書き込み、保存する。

すると、Eclipseが自動的にAIDLファイルをJavaのプログラムに変換し、genフォルダのパッケージの下にIChangeIcon.javaが作られる。(内容は見ることができるが、genフォルダの下にあるファイルは基本的に修正しない。)

以上の準備ができたら、以下のプログラムをエディタで書き込む。

  • IChangeIcon.aidl(上で既に作成・保存)
  • RemoteService.java
  • AndroidManifest.xml(サービスのIntent Filterを追加する、のだがインテントフィルタ無しでもOKだった)

そして、

  • ステータスバーに表示するアイコンをres/drawableの下に作成する(私の場合は、drawable-hdpiとdrawable-mdpiに置いたが、1ヶ所でも良いだろう。)アイコンの画像はAndroid SDKの中のサービスのデモで使った三角のアイコン(/home/android/android-sdk/samples/android-8/ApiDemos/res/drawable-mdpi/stat_sample.png)とそれを画像エディタで上下逆さまにしたものを使った。

stat_up.png stat_down.png

  • 不要なファイルの削除。RemoteServiceはデーモン的に動くので画面の定義や、ランチャでのアイコンは不要となる。そこでicon.png、main.xml、string.xmlは削除してしまった。

以上の作業を終了するとプロジェクトの構成はこんな感じとなる。

後は Android Aplicationとして動かせばいい。

これでサービスをAVDにインストールできたが、サービスを呼び出すクライアントを起動しない限りはサービスも起動することはない。

サービスRemoteServiceのソースリスト

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.example.android.remote_service" android:versionCode="1"
	android:versionName="1.0">
	<application>
		<service android:name=".RemoteService">
			<intent-filter>
				<!-- バインドの対象となるサービスで提供するインターフェース -->
				<action android:name="com.example.android.remote_service.IChangeIcon" />
				<!-- 特定のクラスを指定せずにサービスを指定するアクションコード -->
				<action android:name="com.example.android.remote_service.REMOTE_SERVICE" />
			</intent-filter>
		</service>
	</application>
</manifest>

IChangeIcon.aidl

package com.example.android.remote_service;

interface IChangeIcon {
    void flipIcon();
}

RemoteService.java

package com.example.android.remote_service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;

public class RemoteService extends Service {

	private NotificationManager mNM = null;
	private Notification notification = null;
	private final int NOTIFICATION_ID = R.drawable.stat_up;
	PendingIntent pIntent = null;

	private boolean stat_up = true;

	@Override
	public void onCreate() {
		super.onCreate();
		pIntent = PendingIntent.getActivity(this, 0, new Intent(IChangeIcon.class.getName()), 0);
		notification = new Notification(R.drawable.stat_up, "Up and Down", System.currentTimeMillis());
		notification.setLatestEventInfo(this, "RemoteService", "Up and Down ICON ", pIntent);

		mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
		showNotification(stat_up);
	}
	
	@Override
	public void onDestroy() {
		mNM.cancel(NOTIFICATION_ID);
		super.onDestroy();
	}

	private final IChangeIcon.Stub mBinder = new IChangeIcon.Stub() {
		public void flipIcon() throws RemoteException {
			stat_up = !stat_up;
			showNotification(stat_up);
		}
	};
	
	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}

	private void showNotification(boolean isUp) {
		if (isUp)
			notification.icon = R.drawable.stat_up;
		else
			notification.icon = R.drawable.stat_down;
		mNM.notify(NOTIFICATION_ID , notification);
	}
}

クライアントのアプリケーションを作成する

クライアントはサービスとは別のアプリケーションとして作る。先ずはいつもの通りEclipseの[File]ー[New]ー[Android Project]を選択して新しいプロジェクト作る。

New Android Projectダイアログでは、

  • プロジェクト名(Remote_Service_Client)
  • ビルドターゲット
  • アプリケーション名(Client)
  • パッケージ名(com.example.android.remote_service_client)
  • アクティビティ名(Client)

を設定して[Finish]をクリックする。

Remote_Service_Clientのプロジェクトを作成した直後では、srcとgenの下にはパッケージ com.example.android.remote_service_client だけが存在している。

今回は、srcの下にパッケージcom.example.android.remote_serviceとその下にIChangeIcon.aidlを置く必要がある。次のような手順でプロジェクトRemote_Serverからリンクを張る。
まず、パッケージエクスプローラでsrcを選択し、マウスの右クリックからコンテキストメニューを表示する。[New]ー[Package]を選択する。

Java PackageダイアログでNameにcom.example.android.remote_serviceを入力し[Finish]をクリックする。

するとsrcの下にパッケージcom.example.android.remote_serviceが出来る。(自動的にgenの下にもcom.example.android.remote_serviceが作成される。)次に今作成したcom.example.android.remote_serviceを選択してマウスの右クリックからコンテキストメニューを表示する。[New]ー[File]を選択する。

Fileダイアログが出たら[Advanced>>]をクリックし、“Link to file ...”をチェックする。次にリンク先のIChangeIcon.aidlへのパスを直接入力するか、[Browse...]を使って入力する。(~/workspace/Remote_Server/src/com/example/android/remote_server/IChangeIcon.aidl)

するとサービスのところで作成したIChangeIcon.aidlへのリンクが張られ、クライアントのsrc/com.example.android.remote_serviceの下にもIChangeIcon.aidlが現れる。(自動的にgenの下にもIChangeIcon.aidlを展開したIChangeIcon.javaが作られる。)

あとは、通常のアクティビティを作る要領で作業を進める。main.xmlでボタン等のViewの定義をしてからClient.javaのプログラムを作成し、出来上がったら Android Applicationとして起動する。

なお、サービスのところで作成したIChangeIcon.aidlをリンクを使いクライアントにもコピーして来たが、クライアントを作成するのにサービスのファイルが必要なるというのが今ひとつ解せない。他人が作ったサービスを使うアプリケーションを作成するので、そのサービスの一部のソースがないと構築できないことになってしまう。クライアントからはIChangeIconクラスの情報があれば良いはずなのでIChangeIcon.aidlをコピーするのではなく、サービスのbinをオブジェクトライブラリをしてビルドパスに入れてみた。コンパイルは出来き、AVDにインストールもできるのだが、クライアントを起動すると“java.lang.NoClassDefFoundError: com.example.android.remote_service.IChangeIcon”という例外が発生しクライアントが止まってしまう。この辺りはもう少し調べて見たい。

クライアントClientのソースリスト

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.example.android.remote_service_client"
	android:versionCode="1" android:versionName="1.0">
	<application android:icon="@drawable/icon" android:label="@string/app_name">
		<activity android:name=".Client" android:label="@string/app_name">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
	</application>
</manifest> 

res/layout/main.xml

<?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:layout_width="fill_parent" 
		android:layout_height="wrap_content" 
		android:id="@+id/button_flip"
		android:text="flip !"
		android:onClick="onClick"
		/>
</LinearLayout>

res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Client</string>
</resources>

Client.java

package com.example.android.remote_service_client;

import com.example.android.remote_service.IChangeIcon;
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.os.RemoteException;
import android.view.View;

public class Client extends Activity {
	
	IChangeIcon mService = null;
	
	private ServiceConnection mConnection = new ServiceConnection() {
		
		public void onServiceConnected(ComponentName name, IBinder service) {
			mService = IChangeIcon.Stub.asInterface(service);
		}
		
		public void onServiceDisconnected(ComponentName name) {
			mService = null;
			
		}
	};
	
	@Override
	public void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		// startService(new Intent(IChangeIcon.class.getName()));
		bindService(
			new Intent(IChangeIcon.class.getName()), 
			mConnection, 
			Context.BIND_AUTO_CREATE
			);
	}
	
	@Override
	protected void onDestroy() {
		unbindService(mConnection);
		// stopService(new Intent(IChangeIcon.class.getName()));
		super.onDestroy();
	}
	
	public void onClick(View view) {
		try {
			if (mService != null)
				mService.flipIcon();
		} catch (RemoteException e) {
			// Nothing TODO: handle exception
		}
	}
}

出来上がったクライアントを起動すると以下のような画面になる。

ここで[Flip !]というボタンをクリックすると、ステータスバーの三角アイコンの向きが変わる。

なお、ここでメモったプログラムは Remote Service の“基本動作”を理解するためのスケルトン(骨組み)に過ぎない。実際のアプリケーションではAIDLはもっと複雑になってくる。この辺については、もしまた機会があったらメモっておきたいと思うが、“AndroidのBinderによるプロセス間のメソッド呼び出し”で、より実践的な使い方が紹介されている。(しかし、京都マイクロコンピュータって懐かしい響きだなぁ。確か京大の学生が作った会社じゃなかったかなぁ。)

AndroidのServiceについて

Androidのサービスについて、ちょっと調べてみた。
サービス自身を、それを使うActivityと一緒のアプリケーションとして使うローカルサービスと別のアプリケーションとして動かすリモートサービスがあるようだが、今回はローカルサービスについて。

【補足:2010.09.17】
Remote Serviceについては“AndroidのRemote Serviceについて(+作り方)”にメモしておいた。(ただし、Service全般とLocal Serviceについてはここにメモしている。)

Androidのページ:http://developer.android.com/intl/ja/reference/android/app/Service.htmlに詳しく書いてある。要点だけを私なりにまとめてみた。(といっても和訳らしきものになってしまったが、和訳するつもりではないので、内容の正確さは保証しない。あくまでも私が理解した内容である。あくまでも個人的に後から読み直せるように備忘録としてメモっている。)

Serviceとは何か?

まず、Serviceが以下のことでは“ない”ということに気をつけよう:

  • Serviceは個別のプロセスではない。Serviceオブジェクトは独自のプロセスで実行すること意味しているのではなく、アプリケーションの一部分として同じプロセスで動く。(コメント:リモートサービスとして別プロセスとして動かすこともできるのでないか?)
  • Serviceはスレッドではない。つまり(Application Not Respondingエラーを回避するために)メインスレッドの仕事を肩代わりするわけではない。(コメント:と言ってもサービスの中でスレッドを作ることは出来るのではないか?)

Serviceそれ自身は大変シンプルで、次の2つの仕組みを提供する:

  • アプリケーションが何かをバックグラウンドで実行することをシステムに対して伝える仕組み(ユーザがアプリケーションと直接対話していない場合でも)。これは Context.startService() を呼び出すことで実現し、Service自身または何か他のものが明示的にServiceを中断するまで動き続けるようにシステムに依頼する。
  • アプリケーションがその機能の一部を他のアプリケーションから見えるようにする仕組み。これは Context.bindService() を呼び出すことで実現し、Serviceと情報をやりとりするように長期間のコネクションを作ることができる。

Service自身は大変シンプルであり、ServiceをローカルなJavaオブジェクトとして直接メソッドを呼び出す単純な方法から、AIDLを利用したリモート・インタフェースを提供する方法まで、必要に応じて単純な、もしくは複雑な方法でServiceと情報をやりとりできる。

Serviceのライフサイクル

システムは2つの方法でServiceを実行することができる。一つは誰かが Context.startService() を呼び出す。するとシステムがServiceを探し(Serviceを生成し必要に応じて onCreate() メソッドを呼び出し)、クライアントが渡した引数でServiceの onStartCommand(Intent, int, int) メソッドを呼び出す。Serviceは Context.stopService() または stopSelf() が呼び出されるまで走り続ける。複数の Context.startService() 呼び出し(つまり対応する複数の onStartCommand()呼び出し)はネストされないので、何回スタートされても1回のContext.stopService() または stopSelf()で停止する。ただし、ServiceがstopSelf(int)を使っても、開始されたintentが処理されるまでは停止しない。

起動されたServiceには実行方法を決定する更に2つのモードがあり、onStartCommand()の返り値で決まる:START_STICKY は明示的に起動され、明示的に停止されるサービスが使う。一方、START_NOT_STICKY または START_REDELIVER_INTENT は送られてきたコマンドを処理する間だけ実行するServiceが使う。

もう一つはクライアントがContext.bindService()を使って、Serviceとの永続的な接続を取得する。もし、Serviceが既に実行されていなければ、まずServiceを生成する(その過程でonCreate()を呼び出す)が、 onStartCommand()は呼び出さない。Service側の onBind(Intent) の返り値として、クライアントは IBinderオブジェクトを受け取り、Serviceに対してコールバックできる。(たとえクライアントがServiceのIBinderを保持していなくても)コネクションが確立している限りServiceは実行を続ける。一般的にはIBinderはAIDLを使ったより複雑なインタフェースで使われる。

このようにServiceは起動され、Serviceにバインドされたコネクションを持つことができる。Serviceが明示的に起動されるか、 Context.BIND_AUTO_CREATEフラグを持った1つもしくは複数のコネクションがある限りシステムはServiceを実行し続けるように保つ。どちらの状態も保てなくなると(明示的な中断があるか、全てのコネクションがなくなるか)、ServiceのonDestroy()メソッドが呼び出され、Serviceは事実上終了する。onDestroy()からリターンするまでに全ての後片付け(スレッドの停止、レシーバの登録解除)が完了していなければならない。

プロセスのライフサイクル

Serviceが明示的に起動されてから、もしくはクライアントによりバインドされてから、AndroidシステムはServiceを実行しているプロセスを出来る限り長期間存続させようとする。メモリが不足して既存のプロセスをkillする必要がでると、Serviceを実行しているプロセスの優先順位は次のように扱われる可能性が高い:

  • もしServiceが現在 onCreate()、onStartCommand()、onDestroy()のコードを実行中である場合は、実行中のコードをkillすることのないように、Serviceを実行しているプロセスはフォアグラウンド・プロセスになる。
  • もしServiceが起動されているが、Serviceを実行しているプロセスが、ユーザが画面で見ているプロセスに比べ重要ではない見なされる場合、ただし、画面に現れてない他のどのプロセスよりも重要と見なされる場合。一般にはごく少数のプロセスだけが画面上でユーザに見えているので、この場合は、余程メモリが不足しない限りはServiceはkillされない。
  • もしクライアントがServiceにバインドしている場合には、バインドしているなかの最も重要なクライアントよりも重要でなくなることはない。つまり、クライアントの一つが画面に表示されていれば、サービス自身も画面に表示されているのと同等に扱われる。
  • 起動されているServiceは startForeground(int, Notification) API を使い、Serviceをフォアグランド状態に置くことができ、そうすることでシステムはServiceをユーザが認識中であると見なし、メモリ不足でkillする候補には入れない。(それでも、フォアグランドのアプリケーションによる極端なメモリ不足でServiceがkillされる論理的な可能性はあるが、実際的には懸念しなくても良い。)

つまり、まとめると、

  • サービスはリモートだけではなく、ローカルなものもあり、従来のコンピュータで言う“サービス”とはちょっと違うよ。
  • サービスの起動にはstartServiceによる方法とbindServceによる方法の2種類があるよ。
  • startServiceによる起動は、stopServiceなどが呼び出されるまでサービスが動き続けるよ。
  • bindServieはサービスの起動とサービスへのコネクッションの確立を一気に行うよ。だけど、コネクションが切れたところでサービスも終了するよ。
  • bindServiceではサービス側からクライアント側にIBindオブジェクトが伝えられるよ。IBindオブジェクトにはサービスのオブジェクトが含まれ、これを使ってサービスにコールバックできるよ(サービスのメソッドを呼び出せる)。(ローカル・サービスの場合)

ローカルサービスに関しては大体、こんな内容だと思う。

なお、ローカルサービスに関しては ApiDemoの中の App/Service/Local Service Binding と App/Service/LocalServiceConroller にサンプルがある。この2つのクライアントは実際には1つのjavaプログラムとして書かれていて com.example.android.apis.app/LocalServiceActivities.java として提供されている。実質的に2つのプログラムを1にまとめた形で、1つはbindServiceによるサービスの起動と呼び出し、もう1つはstartServiceによるサービスの起動(とその維持)をデモしている。ちなみにサービス本体のプログラムは別ファイルの com.example.android.apis.app/LocalService.java に書かれている。

ApiDemoを起動してリストのAppからServiceを選択する。中ほどに“Local Service Binding”と“Local Service Controller”がある。先ず、Bindingを起動すると[Bind Service]と[Unbind Service]のボタンが現れ、Bind Serviceをクリックすると通知領域に△アイコンが表示されサービスが起動したことがわかる(下のスクリーンショットの赤い丸の部分)。Unbindボタンをクリックするとバインドが切られサービスも終了するので△アイコンが消える。またUnbindボタンをクリックしなくてもでDパッドの[戻る]ボタンでクライアントのActivityを中断しても、バインドが切られるのでサービスも終了することがわかる。

次に“Local Service Controller”を起動すると[Start Service]と[Stop Service]のボタンが現れ、[Start Service]をクリックすると通知領域に△アイコンが表示されサービスが起動したことが分かる。Stopをクリックすればサービスは終了するが、Stopをクリックせずに[戻る]ボタンでAppメニューに戻っても△アイコンが表示されているのでサービスは動作を続けていることが分かる。


その状態で再び“Local Service Binding”を起動する。Bindをクリックすれば、サービスに接続するのはさっきと同じだが、今度はUnbindをクリックしてバインドを切ってもサービスは動き続けている。startServiceでサービスを起動したので、バインドが切れても(stopServiceを呼び出すまでは)サービスは動き続けていることがわかる。

IBinderによるコールバックのサンプル

ローカルサービスではバインドにより接続が確立すると、サービスよりIBinderが渡され、これを使ってクライアントからサービスに対してコールバックできる。その実験をしてみた。SDKのサンプル、LocalService.java と LocalServiceActivities.javaをチョットだけ変更して実験する。

まず、サーバ(LocalService.java)の“LocalService”というクラスの最後に次のフィールドとメソッドを追加する。

private int counter = 0;
public int returnInt() {
	return counter++;
}

クライアントから呼び出されるとカウンタの値を返すという単純なメソッドである。

次にクライアント(LocalServiceActivities.java)を変更する。
Bindingクラスの中のmConnectionというフィールドの定義の中に、onServiceConnectedというメソッドがあり、その中でIBinderからサービスのオブジェクトを取得する“mBoundService = ....”という行(103行目)がある。その行の次にLogを使ってメッセージを出力する1行を加える。(次の例は“mBoundService = ....”の行も含んでいる。

mBoundService = ((LocalService.LocalBinder)service).getService();
Log.d("++++++", "counter=" + mBoundService.returnInt());

(この行を追加すると“import android.util.Log”が必要になる。)本来であれば、“Get Counter”のようなボタンを付ける方がカッコいいのではあるが、ここではあくまでコールバックの確認をするためだけなのでLogを使った。

以上の変更を加えてApiDemoをrunする。Bindでコネクションを確立する度に、サービスすから取得したカウンターの値をログに表示する。ただ、Bind ServiceボタンとUnbind Serviceボタンだけでコネクションの接続・切断を繰り返すと、常に帰って来る値は1になる。これは毎回サービスが中断されるためだ。Start Serviceボタンで一旦、サービスを起動しておけば、ind ServiceボタンとUnbind Serviceボタンをクリックする度にカウンターの値は上がってゆく。これはサービスが中断されずに走っているため。

android@android:~$ adb logcat
	:
D/++++++  (  492): counter=0
D/++++++  (  492): counter=1
D/++++++  (  492): counter=2
D/++++++  (  492): counter=3
	:

簡単だがローカルサービスへのコールバックの実験ができた。ローカルサービスの場合、クライアントもサービスも同じアプリケーションの中で実装される。なのでわざわざサービスを使わなくても、サービスの機能をActivityで実現して、そのオブジェクトを直接呼び出せば良いことになる。サービスを使うメリットとすれば、[戻る]ボタンや、他のアプリケーションが起動して画面を取られたとしても、サービスとして動作を続けられる、ということだろう、か。

EclipseによるAndroidのproviderの作り方とサンプルプログラム

入門 Android 2 プログラミング”を読んで結構参考になった。個人的にはプロバイダやサービスの仕組みに興味があったので、本書を参考に自前でゼロから組んでみた。先ずはプロバイダを作成してみたのだが、Eclipseでの簡単設定方法とサンプルプログラムについてメモしておく。

自分でゼロから作ってみて気が付いたのだけど、“入門 Android 2 プログラミング”にあるサンプルはもっと本格的なアプリケーションから不要なところを削除して書かれたようだ。そのために不要なコードが残っていたり、簡単なコードもわざわざメソッドにデレゲーションしているので、返って読み難い部分もあった。そういった部分もまとめて、ほぼ骨組みだけのコードで組んでみた。

Eclipseによるproviderの構築

前提として次のようなアプリケーションを作成する。

  • プロジェクト名: Provider_Skeleton_1
  • パッケージ名: com.example.android.provider_skeleton_1
  • アプリケーション名: ProductBrowser1
  • プロバイダ名: ProductDbProvider1

スマートフォン等の“製品”のデータベースから製品の各種情報を提供するプロバイダを想定している。ここでは製品の“製品名”、“重さ”、“備考(メーカー名等)”を検索できるようにする。

1:プロジェクトの作成
Eclipseのメニューから[File]−[New]−[Android Project]を選択して新規プロジェクト作成のダイアログを開き、次のようにプロジェクト名、アプリケーション名などを入力する。

[Finish]をクリックして終了。

2:マニフェストにプロジェクトを追加する
Eclipseの左側にある[Project]タブから、今作成したプロジェクトの中にある“AndroidManifest.xml”を選択する。マニフェストの設定画面(右側)の下の方にアクティビティなどを追加できる部分(Application Nodes)があるので、そこの[Add...]をクリックする。

すると何をエレメントを追加するのか聞かれるので“Provider”を選択して[OK]をクリックする。

マニフェストの設定画面で、今追加したプロバイダを選択すると、その右側にプロバイダに関する情報を入力する部分が表示される。その中で*印のついた必須項目の“Name”と“Authorities”を入力する(Authoritiesは下の方に隠れているのでスライドバーを下げれば表示される)。Nameには “ProductDbProvider1”、Autoritiesには“com.example.android.provider_skeleton_1.ProductDbProvider1”と入力する。アクティビティやプロバイダなどの名前はパッケージ名を省略する場合は頭にドットを付けて“.ProductDbProvider1”と表現するのが正式(qualified名)かと思うが、何故かここではドットを含んだ文字列を受け付けてくれない。qualified名でなくてもプログラムはちゃんと動くので問題ないが、気になるのであれば、次に説明するClass設定画面を終わらせた後、改めてNameをqualified名にすることができる。下の例ではドット付きのqualified名を入力しているので小さな赤い×印が出ている。(わざわざ警告を出す位なのでバグではないと思うのだが、何でこういった仕様なのかわからない。)

この2つを入力したら“Name”というタイトルをクリックする。(下線のついた“Name”というタイトルがリンクになっている。)すると、Class設定ダイアログで現れる。(上のマニフェスト画面でNameとAuthoritiesを入力せずに行きなりNameタイトルのリンクをクリックしてClass設定ダイアログで入力する方法もある。)

“Inherited abstract methods”をチェックして[Finish]をクリックする。すると新規に作成するプロバイダのソースコード(この場合、ProductDbProvider1.java)のスケルトンが作成され、自動的にエディタ画面へ移る。

あとは、必要なソースコードを追加して完成させる(ソースコードについては後述)。
なお、このプログラムにはres/layoutの下にrow.xmlというファイルが必要となる。row.xmlには“表”の“行”の定義が入っている。まず、次のようにパッケージエクスプローラで“layput”をマウスで右クリックしてコンテキストメニューを出し、[New]−[File]を選択する。

するとNew Fileダイアログが開くので、File Name欄に row.xml を入力し[Finish]をクリックする。

からのrow.xmlが作られエディタ画面が開く。
全てのファイルの設定が終わると、次のようなファイル群となる。

完成したらRunで“Android Application”を選択して実行する。


providerの超シンプル・サンプルプログラム

次の3つのファイルを変更する。

  • ProductDbProvider1.java
  • ProductBrowser1.java
  • main.xml

(AndroidManifest.xmlは上の作業ですでに変更しているので保存だけしておく。)
そして次の1つファイルを追加する。

以上、4つのファイルの設定だけ。

ProductDbProvider1.java

プロバイダの本体。先ずはソース全体を表示しておく:

package com.example.android.provider_skeleton_1;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
//import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;

public class ProductDbProvider1 extends ContentProvider {
	
	private static final String	AUTHORITY = 
		"com.example.android.provider_skeleton_1.ProductDbProvider1";
	private static final String	DB_NAME = "products.db";
	private static final int	DB_VERSION = 1;
	private static final String TABLE_NAME = "products";
	// ベースURIかインスタンスURIかの区別をするための定数。初期化子で使う。
	private static final int	BASE_URI = 1;
	private static final int	INSTANCE_URI = 2;
	private static UriMatcher	MATCHER;
	
	// コンテンツに関する定義(データベースの列名など)を宣言するクラス。
	// BaseColumnsをインプリしなくても"_id"カラムの定義をしておけば大丈夫。
	// SimpleCursorAdapterクラスで暗黙に"_id"カラムがあることを前提としているの"_id"の設定が必要。
	// public static final class Product implements BaseColumns {
	public static final class Product {
		public static final Uri	CONTENT_URI = 
			Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
		public static final String	_ID = "_id";
		public static final String	NAME = "name";
		public static final String	WEIGHT = "weight";
		public static final String	REMARKS = "remarks";
		public static final String	DEFAULT_SORT_ORDER = _ID + " ASC";
	}
	
	// MATCHER の初期化
	static {
		MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
		MATCHER.addURI(AUTHORITY, "products", BASE_URI);
		MATCHER.addURI(AUTHORITY, "products/#", INSTANCE_URI);
	}
	
	// SQLiteOpenHelperクラスを使ってデータベースを初期化&オープン
	private class ProductDbOpenHelper extends SQLiteOpenHelper {

		// コンストラクタ。 提供するコンテンツ(データベース)は決まっているので定数でビルトイン。
		public ProductDbOpenHelper(Context context) {
			super(context, DB_NAME, null, DB_VERSION);
		}
		
		// データベースの初期化。必要であればデータベースを作成し、ダミーデータを登録する。
		@Override
		public void onCreate(SQLiteDatabase db) {
			Cursor c = 
				db.rawQuery(
						"SELECT name FROM sqlite_master" +
							" WHERE type='table' AND name='"+ TABLE_NAME + "'", 
						null
						);
			
			if (c.getCount() == 0)
				try {
					// テーブルの作成
					db.execSQL("CREATE TABLE " + TABLE_NAME 
							+ " ( " + Product._ID + " INTEGER PRIMARY KEY AUTOINCREMENT"
							+ ", " + Product.NAME + " TEXT"
							+ ", " + Product.WEIGHT + " INTEGER"
							+ ", " + Product.REMARKS + " TEXT"
							+ " )"
							);
					
					// ダミーデータの定義
					final String[]	P_NAME = {
						"Xperia",	"Desire X06HT","IS01",	"IS02",		"LYNX SH-10B",	"Galaxy S",
						"HT-03A",	"SC-01B",	"X02T",		"X05HT",	"T-01B",	"Droid X",
						"Bold 9700","iPhine 4",	"aaaaa",	"bbbbb",	"ccccc",	"ddddd",
						"eeeee",	"fffff",	"ggggg",	"hhhhh",	"jjjjj"
					};
					final int[]	P_WEIGHT = {
						139,		135,		227,		123,		230,		119,
						123,		130,		129,		165,		160,		155,
						122,		137,		111,		222,		333,		444,
						555,		666,		777,		888,		999
					};
					final String[]	P_REMARKS = {
						"Sony Ericsson","HTC",	"Sharp",	"Toshiba",	"Sharp",	"SAMSUNG",
						"HTC",		"SAMSUNG",	"Toshiba",	"HTC",		"Toshiba",	"Motorola",
						"RIM",		"Apple",	"AAAAA",	"BBBBB",	"CCCCC",	"DDDDD",
						"EEEEE",	"FFFFF",	"GGGGG",	"HHHHH",	"JJJJJ"
					};
					
					// ダミーデータの設定
					ContentValues cv = new ContentValues();
					for (int i = 0; i < P_NAME.length; i++) {
						cv.put(Product.NAME, P_NAME[i]);
						cv.put(Product.WEIGHT, P_WEIGHT[i]);
						cv.put(Product.REMARKS, P_REMARKS[i]);
						// NullColumnHackにはNAMEを割り当てる。
						// 実際にはNAMEはNULLではないので今回は不要だが。
						db.insert(TABLE_NAME, Product.NAME, cv);
					}
				} finally {
					// Nothing to do
				}
			c.close();
		}
		
		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

			Log.w(
					this.getClass().getName().toString(), 
					"Upgrading Database, which will destroy all old data");
			db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
			onCreate(db);
		}
	}
	
	// プロバイダの本体
	private SQLiteDatabase db;

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public String getType(Uri uri) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean onCreate() {
		
		db = (new ProductDbOpenHelper(getContext())).getWritableDatabase();
		
		return (db == null) ? false : true;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		
		SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
		
		qb.setTables(TABLE_NAME);
		
		if (MATCHER.match(uri) == INSTANCE_URI)
			qb.appendWhere(Product._ID + " = " + uri.getPathSegments().get(1));
		
		String sort;
		if (TextUtils.isEmpty(sortOrder))
			sort = Product.DEFAULT_SORT_ORDER;
		else
			sort = sortOrder;
		
		Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sort);
		cursor.setNotificationUri(getContext().getContentResolver(), uri);
		
		return cursor;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

}

このサンプルでは、データベースの生成、テーブルの生成とダミーデータの追加(onCreate)、そしてデータの検索(query)だけを作る。(追加(insert)、更新(update)、削除(delete)などについては実装しない。あくまでもプロバイダの実装方法についての骨組みだけ示すため。)

onCreateでは“SQLiteOpenHelper”をベースとした“ProductDbOpenHelper”クラスでデータベースの初期化をしている。このプログラムの殆どはProductDbOpenHelperはである。(逆に言えば、プロバイダの本質的な部分はそれ以外の僅かな部分。)

さて、上のコードの要点を説明しておく。先ずは定数などの定義から:

private static final String	AUTHORITY = 
	"com.example.android.provider_skeleton_1.ProductDbProvider1";
private static final String	DB_NAME = "products.db";
private static final int	DB_VERSION = 1;
private static final String TABLE_NAME = "products";
// ベースURIかインスタンスURIかの区別をするための定数。初期化子で使う。
private static final int	BASE_URI = 1;
private static final int	INSTANCE_URI = 2;
private static UriMatcher	MATCHER;

これらはこのプロバイダ内部だけで使うprivate final。“AUTHORITY”はマニファストで記述したものと同じ文字列であることを確認する。
“BASE_URI”と“INSTANCE_URI”は、他の例等を見ると“データ名”と“データ名_ID”となっている。しかし、これは与えられたURIがベースURI(つまりID無し)であるかインスタンスURI(つまりID付き)であるかを区別するための抽象表現なので、上記のような定数名にした。その方がプログラムも読みやすいと思う。
“MATCHER”はクラスなのでこの後、static初期化子で初期化している。
ここで、参考書などでは次のようにHashMapを使ったProjection Map(column Map)を宣言している。

private static HashMap<String, String>	PROJECTION_MAP;

しかし、Projection Mapは(Androidのホームページ(setProjectionMap)を見ると)複数のテーブルでJOINを実施する際に、呼び出したアクティビティが指定したカラム名に曖昧さがないように、より正確なカラム名マッピングするためとある。テーブルを一つしか持たない単純なサンプルでは必要ないので割愛した。

次はコンテンツ(データベース)の定義:

// コンテンツのプロバティ(データベースの列)に関する定義を宣言するクラス
// BaseColumnsをインプリしなくても"_id"カラムの定義をしておけば大丈夫。
// SimpleCursorAdapterクラスで暗黙に"_id"カラムがあることを前提としているの"_id"の設定が必要
// public static final class Product implements BaseColumns {
public static final class Product {
	public static final Uri	CONTENT_URI = 
		Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
	public static final String	_ID = "_id";
	public static final String	NAME = "name";
	public static final String	WEIGHT = "weight";
	public static final String	REMARKS = "remarks";
	public static final String	DEFAULT_SORT_ORDER = _ID + " ASC";
}

ここではデータベース(もしくはプロバイダが提供するデータ)の構造等をクラスとして定義している。(このサンプルでは“製品”の情報なので“Product”という名前にした。)このクラスについては2点ほど検討事項があった。一つは、このクラスをプロバイダ(つまりProductDbProvider1)の内部で定義するのか、もう一つは“BaseColumns”をインプリメントするのか、という点である。

プロバイダ内部で定義するかどうかは、このデータが他の多くのアプリケーションから利用されるかどうかによるだろう。ここではプロバイダにローカルな位置づけでプロバイダ内部で定義した。「初めてのGoogle Androidプログラミング」ではプロバイダとは別のクラスとして定義している。

次にBaseColumnsをインプリメントするかどうかだが。Android流のデータベースの利用を前提とすれば使った方が良いのだろう。Androidの内部で使われている数々のデータベース(連絡先等)はこのBaseColumnsをインプリしている。BaseColumnsをインプリするとデータベースのカラム(列)名として“_id”が有ることが前提となる。ここにはユニークな値が書き込まれ一般にPRIMARY KEYとして利用される。一方で、製品データベースの様なものではJANコードの様なものをPRIMARY KEYにしたいので不要になる。その場合はAndroid環境に依存した“_id”が存在することを前提とするのは頂けない。BaseColumnsをインプリしたくないところだ。
上の例ではBaseColumnsをインプリしていないケースでコンパイルしているが、こうしたこともできるよ、というサンプル。実際にはメインActivityで使っているSimpleCursorAdapterクラス等が暗黙にデータベースに"_id"カラムがあることを前提して実装されているらしく、BaseColumnsを使わなくても結局は“_id”を定義しなければならない。なんかちょっとナンダカナァって感じ。所詮は携帯の中のアプリケーションなので、郷に入れば郷に従え、なのかも知れない。

// MATCHER, PROJECTION_MAP の初期化
static {
	MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
	MATCHER.addURI(AUTHORITY, "products", BASE_URI);
	MATCHER.addURI(AUTHORITY, "products/#", INSTANCE_URI);
}

ここはお約束みたいなところ。URIが“products”で終わる場合はベースURI、“products スラッシュ 番号”で終わる場合はインスタンスURIと宣言している。

次に出てくるのはデータベースの初期化等で利用するSQLiteOpenHelperのサブクラスProductDbOpenHelperの定義。実際のアプリケーションでは、結構複雑な処理をしなければならないが、ここではデータベースの作成とダミーデータの設定といったスタブコードになっている(つまり手を抜いているということ)。なので、特にコメントは無い。

ProductDbOpenHelperの後がデータベース操作の本体部分。ここではプロバイダの構成方法を示すだけなので、“onCreate”と“query”しか実装していない。onCreateでは上で定義したProductDbOpenHelperを呼び出してデータベースをオープン(&初回にのみ初期化)しているだけ。queryは:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
		String[] selectionArgs, String sortOrder) {
	
	SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
	
	qb.setTables(TABLE_NAME);
	
	if (MATCHER.match(uri) == INSTANCE_URI)
		qb.appendWhere(Product._ID + " = " + uri.getPathSegments().get(1));
	
	String sort;
	if (TextUtils.isEmpty(sortOrder))
		sort = Product.DEFAULT_SORT_ORDER;
	else
		sort = sortOrder;
	
	Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sort);
	cursor.setNotificationUri(getContext().getContentResolver(), uri);
	
	return cursor;
}

これもシンプル。

  1. これから検索するテーブルをしていする。
  2. 与えられたUriインスタンスURIであれば、特定の行だけを検索するようにWHERE句を作っている。ベースURIであれば何もしない(=テーブル全体を検索対象とする)。getPathSegmentsはURIのパス部分(UriのAuthorityの右側)だけを抽出する。この例では第0要素が“products”で、もしインスタンスIDがあれば第1要素がその番号となる。なのでget(1)でインスタンス番号を取得できる。
  3. ソート条件の設定。引数が空であれば、標準のソート条件を設定し、空でなければ、そのまま引数を使う。
  4. 検索を実行する。結果はCursorクラスで返ってくる。
  5. データ変更のためのNotificationを発行する。(が、検索(query)に必要なのだろうか?)

以上でプロバイダProductDbProvider1の構築は終了。今回は使っていないのでgetTypeの実装も省略している。

ProductBrowser1.java

このアプリケーションのメイン・アクティビティ。上のプロバイダを使ってデータベースを検索して結果を表示するだけ。

package com.example.android.provider_skeleton_1;

import android.app.ListActivity;
import android.database.Cursor;
import android.os.Bundle;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;

public class ProductBrowser1 extends ListActivity {
	
	private static final String[] PROJECTION =
		new String[] {
			ProductDbProvider1.Product._ID,
			ProductDbProvider1.Product.NAME,
			ProductDbProvider1.Product.WEIGHT,
			ProductDbProvider1.Product.REMARKS
	};
	private Cursor cursor;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		cursor = 
			managedQuery(ProductDbProvider1.Product.CONTENT_URI, PROJECTION, null, null, null);
		
		ListAdapter adapter =
			new SimpleCursorAdapter(
					this, 
					R.layout.row, 
					cursor, 
					new String[] {
							ProductDbProvider1.Product._ID,
							ProductDbProvider1.Product.NAME,
							ProductDbProvider1.Product.WEIGHT,
							ProductDbProvider1.Product.REMARKS
					}, 
					new int[] {
							R.id.c_id,
							R.id.c_name,
							R.id.c_weight,
							R.id.c_remarks
					});
		setContentView(R.layout.main);
		setListAdapter(adapter);
	}
	
	@Override
	protected void onDestroy() {
		super.onDestroy();
		cursor.close();
	}

}

これも単純。ベースUriを指定してmanagedQueryでプロバイダを呼び出し、結果をCursorで受け取る。それをListAdapterに渡してAndroid画面に表示しているだけ。

このサンプルはプロバイダの構築が目的なので画面は凝っていない(表示データの見出し行もない)。mail.xmlは次のような内容にした:

<?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"
	>
	<ListView
		android:id="@android:id/list"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
		/>
</LinearLayout>

単にデータベースの検索結果を表示するListViewがあるだけ。

ListViewに表示するリストの各行はrow.xmlで定義している。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="horizontal"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	>
	<TextView
		android:id="@+id/c_id"
		android:layout_width="24sp" 
		android:layout_height="wrap_content"
		android:gravity="right"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_name"
		android:layout_width="100sp" 
		android:layout_height="wrap_content"
		android:singleLine="true"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_weight"
		android:layout_width="50sp" 
		android:layout_height="wrap_content" 
		android:gravity="right"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_remarks"
		android:layout_width="match_parent" 
		android:layout_height="wrap_content" 
		android:singleLine="true"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
</LinearLayout>

だらだらと長くなったが、見やすいように途中に罫線代わりのTextViewを挿入したため(罫線(各Viewの枠)の表示方法ってあったような気もするのだけど、思い出せなかった)。

FindAFriendを動かしてみた

Jerome DiMarzio氏の「初めてのGoogle Androidプログラミング」(邦訳版)のサンプルプログラムを動かしているのだけども、最後のFindAFriendが動かなかった。
初めてのGOOGLE ANDROIDプログラミング
プログラムが長いので入力ミスでもあったのかと思い、再度Walk Throughをしてみたが、間違いらしきところもない。仕方ないのでEclipseのデバッガで追いかけてみたら、次の箇所がおかしいことに気付いた。

友人の名前と緯度・経度をデータベースから読んで来てGoogole Map上のOverlayにプロットする“FriendMap”というクラスがある。その中の“loadFriends”というメソッドで、データベースから読み出した緯度・経度が正しいフォーマットか確認のため正規表現でマッチングしている部分がある。その正規表現がおかしい。原書もGoogle Booksで(たまたまプレビューできたので)確認してみたが、正規表現が次のようになっている。

final String geoPattern = "(geo:[\\-]?[0-9]{1,3}\\.[0-9]{1,6}\\,[\\-]?[0-9]{1,3}\\.[0-9]{1,6}\\#)";

つまり、緯度経度がデータベースに次のような形式で登録されていなければならない。

(geo:45.0,141.0#)

ところが邦訳版280ページの操作説明では単に“45.0,141.0#”と入力しているだけだし、データベースへもそのままの文字列が登録される。また、データベースから読み出してアプリケーションに渡されるのも元の文字列のままとなる。

本の通りにタイプするとデータベース上にあるすべてのお友達の座標が無効になってしまう。それだけだったら、地図上に誰も表示されないだけの話なのだが、さらに事態をややこしくしているのが、このアプリケーションは最低でも1人の友人が登録されて初めて地図にプロットできる、という前提で作られていることだった。同じloadFriendsというメソッドで“friendLocation”という変数があり、データベース上の最後にマッチした友人の座標がはいっている“ハズ”なのだが、これがnullのままsetCenterというMapControllerのメソッドに引き渡されるのでNullPointerExpectionを発生する。

このアプリケーションを正しく動かすためには、データの入力を

(geo:45.0,141.0#)

とするか、もしくは、正規表現を次のように変更する(座標入力を“45.0,141.0#”とする場合)。

final String geoPattern = "[\\-]?[0-9]{1,3}\\.[0-9]{1,6}\\,[\\-]?[0-9]{1,3}\\.[0-9]{1,6}\\#";

一応、どちらの方法でも正しく動作することを確認している。

これは推測だが、もっと本格的なアプリケーションをベースに必要のないところを削除してこのサンプルをつくったのではないだろうか。そのためこういった表現が残ってしまったのかも知れない。

関係ないが、色々調べていたら“geo”は“ゲオ”と発音するのではなく“ジーオ”(gee-oh)と発音するらしい、ということが分かった。GeeもOhも感嘆の表現。:-C

読書感想

最後のFindAFrind以外はちゃんと動いていたし、原著作者のコメント以外にも訳者のコメントがしっかりしていたので分かりやすかった(例えば、Google MapのAPI Keyの取得方法とか詳細に書いてあった)。ただ、内容が“概要”に留まっているので、詳細に理解しようとすると物足りなさがある。作者は前書きで「本書はプログラマーズガイドであり、ビギナーズガイドではない」と書いているが、私は逆だと思う。概要を知る入門書としては良く出来ているが、例えば、レイアウトとかIntent、Uriの構造や構成が分かりにくかったり、詳細については隔靴掻痒の感じがある。

この本のあとに「入門 Android 2 プログラミング」を読んだが、こっちの方がプログラマーズガイドに近いと思った。

とは言え、初めて読んだAndroid SDK関係の本としては、大変参考になった。

半仮想マシンのすゝめ

gPXEとiSCSIによるネットワークブートの環境を整えてからは、マシンの利用方法が随分変わって来た。特に“半仮想マシン”を使っている。半仮想マシンとは、ある時は実マシンで動かして、ある時は仮想マシンとして動かすという使い方。マシンは起動時に使うHDDにその全てが入っているから、HDDを変えれば同じマシンでも他のシステムとして利用できる。逆に言えば、あるHDDを色々なPCでブートして使えば、どのPCでも同じシステムが利用できる。つまり“あるHDD”を実マシンに付ければ実マシンでシステムが動くし、同じHDDを仮想マシンに繋げば仮想マシンでシステムが動く。“あるHDD”の実体が仮想ディスクで、そのファイルをiSCSIで任意のPCから使うようにしただけだ。

Ubuntu等のLinuxで標準的に使われるiSCSIターゲット(サーバ)は“IET”だが、IETではターゲットマシン上のあるファイルを仮想ディスクとしてiSCSIでサービスすることができる。そしてこの“仮想ディスク”はVMwareなどで使うモノシリック型の仮想ディスクと中身は同じである。

そこで、iSCSIでサービスするディスクファイルを作る際に、一旦、仮想マシンの仮想ディスクとしてファイルを作成する。VMwareであれば“Add Hardware”で仮想ディスクを1つ新しく作る。その際にPropertyのFile Optionsで“Allocate all disk space now”を選択する。つまり可変サイズの仮想ディスクではなく、予め全容量を割り当てるフラット型を選択する。例えば、nomad.vmdkという仮想ファイルを作成するとnomad-flat.vmdkという仮想ディスク“本体”が作成される。この“-flat”付きのファイルをiSCSIのターゲットとして/etc/ietd.confの中で指定する。例えば、こんな風に:

root@server# ls -l /usr/lib/Virtual_Machines/Nomad/
total 4198416
-rw------- 1 root root 4294967296 2010-08-21 10:25 Nomad-flat.vmdk
-rw------- 1 root root        399 2010-08-21 10:25 Nomad.vmdk
-rw------- 1 root root          0 2010-08-21 10:24 Nomad.vmsd
-rwxr-xr-x 1 root root       1223 2010-08-21 10:25 Nomad.vmx
-rw------- 1 root root        260 2010-08-21 10:25 Nomad.vmxf
root@server# 
root@server# cat /etc/ietd.conf
# iSCSI target configuration

Target iqn.2010-08.localnet:nomad
	Lun 0 Path=/usr/lib/Virtual_Machines/nomad/nomad-flat.vmdk,Type=fileio

root@server# 

そうすれば、iSCSIを通してネットワーク上の任意の実マシンからこの仮想ディスクをHDDとして利用できる。また、このファイルを接続したVMware仮想マシンとして立ち上げれば、仮想マシンとして利用できることになる。

実際にUbuntu 10.04をインストールして、実マシンでも仮想マシンでも利用できることを確認している。
実マシンで動かすと:

仮想マシンで動かすと:

まぁ、当たり前の事だがほとんど違いはない。(逆に実マシンと仮想マシンでブート時に画面サイズを変えることもできる。) 実マシンと仮想マシンで接続されているハードウェアが異なるので、それに関連するところは自動的に変更されている。 Linuxの場合、ブート時にハードウェア構成に合わせてモジュールをロードするのでブートの度に手動で設定を変えることは余りないだろう。

ただし、仮想マシンとして動かしつつ、同時に実マシンで利用することはNG。このディスク・ファイルの中のファイルシステムが壊れてしまう。排他制御の機構がないので、その点は注意が必要だ。

同様の方法でWindowsも半仮想マシンとして動かすことも確認している。ライセンス認証前のお試し版のWindows7で試してみた。ただし、ライセンス認証後にこう言ったこと(=別のPCでブートする)をやると、再認証が必要となる。同時に複数のマシンで動かしているわけではないのでライセンス条項的には問題ないだろうが、ブートの度にOSに“ハードウェア構成が変わっているからリブートが必要”とか“再認証しなさい”と言われ、何回も繰り返していると最後にはブートのたびにマイクロソフトに電話して認証しなおさなければならない。現実的ではない。その点、Linuxは同じHDDをどのマシンで起動しても文句を言わないので嬉しい。コンピュータの色々な要素が仮想化されている現在では、Windows型のライセンス形態(1つのマシンに1つのライセンスを対応させる形態)は時代遅れになりつつあるのかも知れない。

実はこれを始めたために1つの実マシンをWindowsで立ち上げたりLinuxで立ち上げたりすることになった。ところがWindowsLinuxではシステムクロックの扱いが異なるのでどちらかの時間が狂ってしまう。それでクロックの扱いを統一しなければならなかった。詳しくは“UTCにするかJSTにするか、それが問題だ”を参照。
この半仮想マシン、もう一歩進めて、仮想マシンiSCSIでブートする方がベターだ。仮想ディスクを仮想マシンに直接“接続”してもOKなのだが、実マシンでiSCSIブートする際には、仮想マシンから“切断”しておく方が良い。しかし、実マシンでブートする度に一々、仮想マシンの設定を変えるのも頂けない。いっそのこと、仮想マシン自身もiSCSIブートにすればすっきりする。いざという時のために仮想ディスクとして作成はしておくものの、普段はiSCSIのターゲットとして使うわけだ。(仮想マシンをgPXEでiSCSIでブートするのではあれば、仮想マシンBIOSを拡張した方が素早くブートできる。詳しくは“VMwareのBIOSを拡張する”を参照。)

Ubuntu 10.04にAndroid SDKをインストールしてみた

Android SDKを使おうと思ってWindows XPSDKをインストールしたのだけども“遅い”。Windows7のお試し版(ライセンス認証前)にインストールしてみたのだけどもやっぱり遅い(多分Vistaは論外だろう)。Windowsはオーバーヘッドが大きいのかな、と思いUbuntu 10.04にインストールしてみたら結構スイスイ動く。やっぱOSは軽くなければ、ということでUbuntu 10.04へのAndroid SDKのインストールについてメモしておく。

なお、Androidの手順書から外れて、私はJavaeclipseは指定のホームページからダウンロードしたものではなく、Ubuntuのパッケージを利用した。そのため、JavaはSun(オラクル)版ではなくOpenJavaを採用している。また、eclipseUbuntuのパッケージとして配られてるものなので、インストール(の場所とか)もお任せで簡単にできた。(どちらもPATHとか設定する必要もない。)今のところ快調に動いている。(もちろん、確実に動作する保証は無いので、心配ならば手順書の通り、個別にソフトウェアをダウンロードしてインストール&設定する方がいいだろう。)

余談だが、最近オラクルがGoogleJavaの特許侵害で訴えたらしい。Sunの時代はJavaを使ってくれる企業にはパートナーとして特許ウンウンは主張していなかったのに、オラクルになった途端にこれだ。なんかセコイ企業だなって感じ。GoogleもOpenJavaを使うようにすればいいのに。(もっとも今回の問題はJava仮想マシンのあたりのことだろうから、OpenJavaで解決できるようなことでもなさそだけど。)

あと、以下のメモにはUbuntuパッケージ以外に、Androidの手順書にある通り、各ソフトウェアをダウンロードしてインストールする手順も載せておく(けど、そっちの方が行数が多くなってしまった。)

ベースとなるUbuntuは32bit版を使う。64bit版だと32bit版であるSDKを動かすために余計なパッケージとかが必要だし、何かトラブルが出ないとも限らない。素直に32bit版を使う。(Ubuntu 10.04では32bit版でも標準でPAE版のカーネルがインストールされるらしく、4GB以上のメモリ空間も使える。)

もちろん、基本はAndroidのホームページの方法に沿って進める:http://developer.android.com/intl/ja/sdk/index.html

私なりに要約すると次の手順になる:

  1. Java (SE) JDKをインストールする
  2. Eclipse をインストールする
  3. Android SDK をインストールする
  4. EclipseにADT Pluginをインストールする
  5. SDKに“Android platforms and other components”を追加する
  6. Android SDK and AVD ManagerでADVを作成する

Java JDK をインストールする

Ubuntuパッケージを使う場合:
OpenJavaを使う

sudo apt-get install openjdk-6-jdk

以上で終了。

Sun Javaを使う場合:

sudo dd-apt-repository "deb http://archive.canonical.com/ lucid partner"
sudo apt-get update
sudo apt-get install sun-java6-jdk

もしくは、http://java.sun.com/javase/ja/6/download.html から“Java Platform, Standard Edition”の“JDK”(jdk-6u21-linux-i586.bin)をダウンロードする。ダウンロードしたインストーラーを実行する。

念のため、リブート後にコントロールパネルから更新チェックをしておく。

Eclipse をインストールする

Ubuntuパッケージを使う場合:

sudo apt-get install eclipse

以上で終了。“Galileo(Version 3.5)”がインストールされる。

ダウンロード版を使う場合:
http://www.eclipse.org/downloads/packages/eclipse-ide-java-developers/galileorから“Galileo Version 3.5 SR2”の“Eclipse IDE for Java Developers”(eclipse-java-galileo-linux-gtk.tar.gz)をダウンロードする。ダウンロードしたtarファイルを解凍すると“eclipse”というディレクトリが出来るので、適当な場所(/usr/local等)へmvして、.bashrcでそこへPATHをきっておく。

Android SDK をインストールする

以下の作業は一般ユーザで行う(SDKを使うユーザごとに設定する)。http://developer.android.com/sdk/からダウンロードする。現在の最新バージョンは“Android 2.2 Platform Relesase 6”(android-sdk_r06-linux_86.tgz)。ダウンロードしたtarファイルをホームディレクトリに展開する。

tar -C $HOME -xvf /path-to/android-sdk_r06-linux_86.tgz

ホームディレクトリに“android-sdk-linux_86”というフォルダが出来るので使いやすいように“android-sdk”に改名しておく(ここは好みの問題なので、改名しなくてもOK)。また、.bashrcに$HOME/android-sdk/toolsへのパスを切っておく。$HOME/.bashrcに次の行を追加:

export PATH=${PATH}:$HOME/android-sdk/tools

EclipseにADT Pluginをインストールする

概要:http://developer.android.com/sdk/eclipse-adt.html

  1. Eclipseを起動し、メニューから"Help" > "Install New Software"を選択する。
  2. Available Softwareダイアログで "Add...."をクリック
  3. Add Siteダイアログが現れたら、"Name"フィールドに任意の名前 (例えば"Android Plugin")を入力する。次に"Location"フィールドに次のURL、https://dl-ssl.google.com/android/eclipse/ を入力する。(もし、うまく行かなかったら"https"を "http"に変更してトライしてみる。)最後にOKをクリックする。
  4. 確認画面が出たら"Finish"をクリックする。
  5. Available Software画面に戻ると、リストに"Developer Tools"追加されている。Developer Toolsの先頭のチェックボックスを選択し、Nextをクリックする。
  6. Install Detailsダイアログが表示されるのでacceptをクリックした後、Finishをクリックする。
  7. インストールが終わったらEclipseを再起動する。

SDKに“Android platforms and other components”を追加する

概要:http://developer.android.com/intl/ja/sdk/installing.html#components

  1. eclipseのメニューから"Window" > "Android SDK and AVD Manager"を起動
  2. 左側のAvailable Packagesを選び、ダウンロード可能なコンポーネントを表示する。
  3. 必要なコンポーネントを選択する。(最新のAPIAndroid platforms、Samples、Documentation、SDK Add-Ons(Google APIs Add-On)の4つだけを選択すればいい。SDK ToolsはSDKのインストール時に既に設定されているはず。また、以前のバージョンのコンポーネントはとりあえずは必要ない。ただし、WindowsにおいてはUSB Driverも。)
  4. 必要に応じ"Accept all"を選択し、"Install Accepted"をクリックすう。
  5. インストールが終わったら、eclipseのメニューから"Help" > "Check for Updates"を選択して最新版がないか確認しおく。

Android SDK and AVD ManagerでADVを作成する

概要:http://developer.android.com/guide/developing/tools/avd.html
eclipseのメニューから"Window" > "Android SDK and AVD Manager"を起動し、AVD(アンドロイド仮想デバイス)を作成する。これが無いとサンプルプログラムやデモが動かない。手順書にはサンプルプログラムのHello Androidを作成するところでAVDを作るようになっているが、AVDの作成はインストールの一環としておいた方が良いような気がする。(入門レベルの話では。)

  1. eclipseのメニューから"Window" > "Android SDK and AVD Manager"を起動
  2. 左側のVirtual Devicesを選び、右側の"New"をクリック。
  3. Create new AVDダイアログのNameに適当な名前、例えば“my_avd_001”とか入力する。TargetにAVDのフレームワークを指定する。リストに出ているものであれば何でも良いが、例えば“Android 2.2 - API Level 8”など。“Create AVD”をクリック。
  4. Android SDK and AVD Managerを閉じて終了。

UTCにするかJSTにするか、それが問題だ

システムクロックをUTCにするかJSTにするか。ほとんどの場合は気にしない。特にWindowsオンリーユーザの場合は気にしたこともないと思う。
悩ましいのはLinuxと混在した環境の場合だ。私の場合はWindowsLinuxは別々で使っていたので余り気にしていなかったが、いつも使うWindowsマシンを“たまに”Linuxで使う場合に適宜補正していたくらいだ(“Linuxで日本標準時(JST)を協定世界時(UTC)として設定する”参照)。
ところが、iSCSIでブートできる環境を整えてしまったので、LANにつながったマシンであれば、任意のマシンをiSCSIに対応した任意のOSでブートできるようになった(“Windows 7をディスクレスで使う”参照)。そうなるとイチイチ時計の設定を変える訳にもいかず、UTCJSTかに統一する必要が出てきた。特に同じ“マシン”を物理的なPCで動かしたり仮想マシンで動かしたりするとどちらかに統一しないといけない。
で、調べて見るとWindowsLinuxも設定でシステムクロックをUTCと見るかJSTと見るか選択出来るようになっていた。もっともWindowsの場合は設定画面はなく、レジストリをいじらないと設定はできないが、それでも以前のWindowsに比べれば格段の違いだと思う(し、設計思想的には実はUTCを標準としている感じられる節がある。)
WindowsLinuxもどちらでも設定できるとなると、UTCJSTのどちらを“我が家の標準”として採用するのか、それが問題だ。結論から言えばどちらもで構わないわけで、あとは好みの問題だろう。
私は世界中で動いているPCのシステムクロックは本来、同じ“時刻”であるべきだと“思う”。マシンの気持ちとしては“どこで”動いているかは気にならない。気にするのはそのPCを使っている人間だけの問題だ。従って、システムクロックはUTCとして、その時刻を表示する時に、そのPCが現在動作している場所に合わせて表示すべきだと考える(もちろん、別の考え方の人もいっぱいいると思う)。
で、私の環境ではUTCに統一することにした。

WindowsUTCに設定する方法

参考:http://coreblog.atikoro.net/iyoda/128 (現在のところGoogle検索のトップ)
このページに書いてある通り。レジストリ
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation
に値 DWORD で“RealTimeIsUniversal”を作成する。値が“1”の場合にシステムクロックをUTCと見る。設定後、リブートすればUTCとして動作する。
今のところ1台のマシンで試しているが、特に問題は無さそうだ(OSは問題なくてもアプリケーションで何か弊害が出てこないか、暫く様子見の状態)。

Linuxの設定

Linuxでは以前からインストール時にUTCJST(ローカルタイム)かを選択できるようになっている。では、インストール後に変更するにはどうすればよいのか。
参考:http://blog.goo.ne.jp/tabitom2002/e/d3e109e62da68f7f29694174a8a03eeb
Google検索でトップというわけではないが、一番分かりやすかった。)
このページにある通り、/etc/default/rcS にある環境変数UTCを“yes”に変更する。(あと、/etc/localtimeがもしJSTでなければ、変更しなければならない。インストール時に地域は日本(=JST)であれば変更する必要はない。/etc/default/rcSUTC変数を変更するだけでOK。ただし、インストール時の地域が日本以外であれば、日本(もしくはPCを使っている場所)に変更する必要がある。このページではシンボリックリンクを張るように書いてあるが、私としてはシンボリックリンクを張るよりは/usr/share/zoneinfo/Japanを/etc/localtimeへコピーした方がベターだと思う。オリジナルの/etc/localtime自身がシンボリックファイルではなく独立したファイルだからだ。(Ubuntu 10.04の場合))
/etc/default/rcSを変更したら、

# ntpdate ntp.nict.jp
# hwclock -w

を実行してからリブートでOK。