この文書は本書「Androidアプリ開発逆引き大全500の極意」(秀和システム刊、ISBN978-4-7980-3734-9)で紹介しきれなかった内容をまとめたものです。
<>br 目次へ
清水美樹の本 トップページへ

アクティビティ「ApiDemos」のコード解説

AndroidManifest.xmlを見てください

登録されているアクティビティ、サービス

たくさんのアプリをリストから選んで実行するひとつのアプリ「ApiDemos」。実行するアプリのリストを記述する「ApiDemos.java」のコードの意味を知るには、まず「ApiDemos」の構造を知ることが大切です。
それは「AndroidManifest.xml」に書かれています。Eclipseの「Android XMLエディタ」で見ると便利です。(Tips057
「Android XMLエディタ」では、上の表示を折り畳んだり、横の窓を広げたりして、登録されているアクティビティの記述を見やすくすることができます。
図1「AndroidManifest.xml」に登録されているアクティビティを見る



アクションフィルタに注意

これから、本書のTips304「PackageManagerを用いて、特定のカテゴリのアクティビティを抽出する」に示した内容を、「ApiDemos」のソースコードに沿って詳しく説明していきます。

プロジェクト「ApiDemos」に登録されているアクティビティで、いわば本体にあたるアクティビティ「ApiDemos」以外のものは、全て以下のアクション名とカテゴリ名を持っています。
アクションandroid.intent.action.MAIN
カテゴリandroid.intent.category.SAMPLE_CODE

図2 アクションとカテゴリに注目



「ラベル」に注目

さらに各アクティビティについて、右側の設定欄を見てください。 まず、「Name」はクラス名ですが、「ApiDemos」クラスよりひとつ下のパッケージ階層にあるため、「.app.HelloWorld」のように相対的に下のパッケージとともに書かれます。
もっと大切なのが、「Label」の欄です。普段は設定する必要がありませんが、「カテゴリ」よりさらにアクティビティを分類する時など、いろいろに利用できます。
図3のように「strings.xml」に登録された値になっていますが、これは「APP/Activity/HelloWorld」を表します。「strings.xml」で確認してみてください。
図3 「ラベル」が登録されている

「ラベル」の意味

アプリ作成者の自由な発想で決められる「ラベル」は、「ApiDemos」では「リストの階層」を表します。
「App」「Activity」「Animation]とリストをクリックして行って起動するアプリの場合、「App/Activity/Animation」です。
「App」「Alarm」「Alarm Controller」で起動する場合「App/Alarm/Alarm Controller」です。
「Graphics」「OpenGL ES」「GLSurfaceView」で起動するなら「Graphics/OpenGL ES/GLSurfaceView」です(「strings.xml」に登録せず、直接「Label」欄に書かれているものも混在していますのでご注意を)。 このラベルが、アクティビティ「ApiDemos」のプログラミングで重要な意味を持ちますので、注目しておいてください。


ApiDemos.javaの内容

アクティビティは再帰的

「ApiDemos」クラスはListActivity(Tips365)のサブクラスです。その特徴は、自分自身を呼び出す再帰的なアクティビティということです。
onCreateメソッドに記述されているリスト1の部分をご覧ください。
リスト1 アクティビティ起動時に最初に読まれるコード
	Intent intent = getIntent();
	String path = intent.getStringExtra("com.example.android.apis.Path");
	
	if (path == null) {
		path = "";
	}

リスト1から読みとれることは、アクティビティが起動したとき、最初に自分を呼び出したインテントから情報を読み込んで、情報がなければ空の文字列にして済ます、という作業が行われていることです。これは、最初に起動したとき(Androidのシステムからインテントをもらっていると考えましょう)、もらうべきデータがなくてパニックにならない措置です。2度目からは自分でインテントにデータを与えて、自分自身を呼び出します。



getDataで得られるリストを表示している

onCreateメソッドでリスト1の次に書かれているのがリスト2です。
リスト2 SimpleAdapterでリストを記述
setListAdapter(new SimpleAdapter(this, getData(path),
		android.R.layout.simple_list_item_1, new String[] { "title" },
		new int[] { android.R.id.text1 }));

リスト2から読みとれるのは、ほぼ以下のようなことです。

リストに表示するのは、メソッドgetDataの内容である。
メソッドgetDataに、自分自身でインテントに入れた情報pathの値を渡す。
実際に表示するのは、getDataの内容の中から、キー"title"で取り出した値である。

すなわち、getDataの内容は、「ハッシュマップのリスト(Tips367)」で、getDataの中のハッシュマップのひとつは「title」というキーを持つことがわかります。
また、リストの内容は、「App」「Activity」など、アプリ上でメニューに表示する「ラベルの一部」であると想像できます。アクティビティが再帰的に呼ばれるたびに、画面のリストが変わっていく記述はここでなされています。

メソッドgetDataの内容

戻り値はハッシュマップのリスト

メソッドgetDataの宣言はリスト3のような構造をとります。戻り値はハッシュマップのリストです。Mapですから、キーに対する値はなんらかのオブジェクトですが、実はインテントです。「自分自身をもう一度読み込むか、リストのアクティビティを起動するか」の選択を与えるものです。
リスト3 メソッドgetDataの構造
protected List> getData(String prefix) {
	List> myData = new ArrayList>();
	
	.....
	
	
	
	return myData;
}



ApiDemosで起動させたいアクティビティを収集する

getDataで作りたいリストは、アプリで起動させたいアクティビティの名前のリストです。そこで、PackageManagerを用いて、アクションが「MAIN」カテゴリが「SAMPLE_CODE」であるアクティビティの情報を集めます。そのために、AndroidManifest.xmlにおいて、各アクティビティのアクションフィルタを設定しているのです。
「自分のアプリにあるアクティビティの情報を集める」という方法はなく、パッケージマネージャはひたすら「インテント」を頼りに、アクティビティの情報を集めます。
リスト4 カテゴリ名が「CATEGORY_SAMPLE_CODE」であるアクティビティのリストを作る
	Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
	mainIntent.addCategory(Intent.CATEGORY_SAMPLE_CODE); //このインテントを頼りにアクティビティの情報を集める

	PackageManager pm = getPackageManager();
	List list = pm.queryIntentActivities(mainIntent, 0);


リスト4で集められるのはアクティビティのリストではなく、各アクティビティの情報をまとめてオブジェクトにしたものです。これは「ResolveInfo」というクラスのオブジェクトです。



アクティビティ(の情報)のリストのひとつずつについてラベル名を得る

メソッドgetDataの後半は繰り返し処理の大きなブロックです。これは、リスト4で得たResolveInfoオブジェクトのリストについての繰り返し処理です。
主な処理はResolveInfoオブジェクトから得た、該当するアクティビティのラベルの処理です。
リスト5 ResolveInfoのリストから要素をひとつとって、対応するアクティビティのラベルを得る
for (int i = 0; i < len; i++) {
	ResolveInfo info = list.get(i);
	CharSequence labelSeq = info.loadLabel(pm);
	....
}



ラベルが得られなければ、アクティビティ名で代用する

リスト6は小さな処理ですが、2つ意味があります。リスト5のlabelSeqはまだCharSequenceオブジェクトなので、toStringメソッドで文字列にします。また、もしアクティビティにラベルがなかった場合、「info」に対して「activityInfo.name」でアクティビティ名を文字列として得ます。
リスト6 labelという変数に、ラベル名もしくはアクティビティ名を割り当てる
String label = labelSeq != null
		? labelSeq.toString()
		: info.activityInfo.name;



ラベルを使ってやること

これで変数labelに、たとえば「App/Activity/Animation」のような文字列が渡されました。
この文字列に対して、以下のような処理をします。

(1)「/」記号で分解し「App」「Activity」「Animation」にします。それぞれ、リスト表示に使います。
(2)(1)の最後の要素「Animation」の場合は、クリックしたらアクティビティAnimationが起動するようにします。
(3)(2)以前の要素は、クリックしたらアクティビティApiDemosが自身を読み込み直すようにします。そのとき、インテントのデータに表示させる内容を渡します。



この方法の大変な点

この方法が難しいのは、アクティビティのリストアップに「パッケージマネージャ」を使っている点です。パッケージマネージャには、細かい検索メソッドは備わっていません。リストをクリックして次のページを読み込む(getDataメソッドが呼ばれる)たびに、パッケージマネージャは、同じカテゴリ名SAMPLE_CODEを持つアクティビティを全部探し出してきます。それらを分類して必要な項目だけを表示する作業は、getDataメソッドの中に自分で書かなければなりません。


階層的なようで微妙に違う

そこで、getDataでは以下のような方法をとっています。たとえば、「App/Activity/Animation」というラベルがあったとしますと、以下のような処理で、リストを表示させています。

最初のページでは、「App」を表示させる。
次のページでは、「App/」の次の語(Activity)を表示させる。
次のページでは、「App/Activity/」の次の語(Animation)を表示させる。
「App/Activity/Animation」の次の語、はないので、アクティビティAnimationを起動する。

この方法は一見階層的処理に見えますが、実際にプログラムを組んでみると微妙に違います。そこで、ApiDemosのソースコードでは、細かい条件分岐をして、なるべく階層的な処理に仕上げようとしています。しかし、結果として、大変読みにくいプログラムになっていると思います。


注意:listはいつも同じ

これから解説するメソッドgetDataで注意しておくことのひとつは、このメソッドに出てくる変数listの値は、getDataメソッドを何度呼び出しても、常に同じです。それは、同じカテゴリを指定したアクティビティのリストだからです。


注意:同じラベルについてlabelPathはいつも同じ

同様、アクティビティのラベルを表すlabelPathの値も、getDataメソッドを何度呼び出しても、各アクティビティについていつも同じです。


prefixの値を変えながらgetDataを繰り返す

では、何が変わるかというと、getDataの引数です。メソッドの定義ではprefix, onCreateメソッドではpathという変数名に入る値です。これが空、"App", "App/Activity", "App/Activity/Animation"と、getDataを呼ぶごとにだんだん「長くなっていく」ことに注意しましょう。

ApiDemosで用いる二つのインテント

インテントの目的だけ把握しておこう

ApiDemosで、リストをクリックしたときに「自分自身を呼び出し直す(そしてgetDataメソッドを呼ぶ)」動作と、「アクティビティを起動する」動作は、異なるインテントを戻す以下のメソッドで区別します。

browseIntentprefix(path)の値を渡して、自分自身を呼び出し直す。リストで「App」をクリックしたら「Activity」が乗ったリストが出てくる動作
activityIntent対応するアクティビティを起動する。リストで「Animation」をクリックしたら、アクティビティ「Animation」が起動する動作


これらのインテントを用いて実際にアクティビティを起動する作業は、サンプルのオリジナルのコードをご覧ください。
このページでは、「browseIntent」が出てきたら「クリックして次のリストへ行くんだな」「activityIntent」が出てきたら「アクティビティが起動するんだな」と御想像ください。

具体的なラベルを例に用いたgetDataメソッドの説明

途中まで同じ項目の二つのラベル

これからgetDataメソッドの流れを考えるにあたり、以下のラベルを持つ二つのアクティビティの情報があると考えましょう。これらは、パッケージマネージャで読み込まれ、getDataメソッド中の配列listに要素として渡されているとします。

App/Activity/Animation
App/Activity/ListActivity/MyList


パッケージマネージャはgetDataメソッドが呼ばれるたびにこの二つを読み込んでくることになります。
ゆえに、listの値はいつも同じで、変数lenの値は常に2です。そして、getData中のリスト7の繰り返しは常に「0回目」と「1回目」の2回になります。
リスト7 getData中の繰り返し処理の部分
	for (int i = 0; i < len; i++) {
		ResolveInfo info = list.get(i);
		//....大変長い処理
	}


ApiDemosを起動したとき:forループ0回目

最初ですから、prefixは空の文字列です。そこで、prefixPathもnullです。

リスト7に示したforループの0回目には、変数labelの値は"App/Activity/Animation"です。

prefixが空なので、prefixWithSlashも空です。
するとprefixWithSlashの長さは0です。
そこで、リスト8のif文が真となります。実は、これが真でなければ、getDataの処理は全くなされません。
リスト8 forループ中の最初のif文
if (prefixWithSlash.length() == 0 || label.startsWith(prefixWithSlash)) {
....
}


labelPathの内容は["App","Activity","Animation"]になります。

prefixPathはnullですから、リスト9の条件文は、リスト10と同じになります。
リスト9 prefixPathがnullかどうかで、nextLabelの値が異なる
String nextLabel = prefixPath == null ? labelPath[0] : labelPath[prefixPath.length];
リスト10 prefixPath==nullなので
String nextLabel =labelPath[0];


すなわち、nextLabelの値は"App" になります。

また、prefixPathはnullですから、リスト11のif判定は、リスト12の判定と同じになります。
リスト11 if文の判定内容が条件によって違ってくる
 if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length-1) 
リスト12 「prefixPath != null」でないので
 if (0 == labelPath.length-1) 


リスト12にはさらに、具体的な値を入れられます。すなわち、labelPath.length=3ですから、条件は「if(0==2)」となります。
これは、偽ですから、リスト13のelseに進みます。
リスト13 リスト11の判定とelseの関係
 if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length-1) {
			.......
			
		} else {
			if (entries.get(nextLabel) == null) {
				.....
			}
		}


リスト13では、elseでまた条件分岐があります。
if文に出てくる「entries」はハッシュマップですが、まだ「空」ですから、キー「nextLabel」の値はありません。そのため、判定は真になります。
そこで、リスト14の作業をします。 メソッドaddItemの最後の引数はメソッドbrowseIntentの戻り値ですが、browseIntent自身がとる引数が、また条件文になっています。
リスト14 elseの中身
} else {
			if (entries.get(nextLabel) == null) {
				addItem(myData, nextLabel, browseIntent(prefix.equals("") ? nextLabel : prefix + "/" + nextLabel));
				entries.put(nextLabel, true);
			}
		}
リスト15 メソッドbrowseIntentのとる引数が条件文
 browseIntent(prefix.equals("") ? nextLabel : prefix + "/" + nextLabel));


リスト15で prefixは空の文字列ですから、browseIntentの引数の値はnextLabelになります。すなわち"App"です。

ゆえに、リスト14 に示したaddItemメソッドにより、myDataに以下のように、「2つのキー・値の組み合わせを持つハッシュマップ」がひとつ入ります。

キー
マップその1"title""App"
マップその2"intent"browseIntent("App")


これは、「Appと表示し、クリックすると、メソッドgetData("App")を実行する欄」をひとつ表示する意味になります。


ApiDemosを最初に起動したとき:forループ1回目

forループの1回目では、labelの値は「App/Activity/Secure Surface/MyList」です。

prefixはforループの回数に寄らず同じですから、条件分岐は最後のひとつを除いて全てforループ0回目と同じになります。
ゆえに、nextLabelの値は"App"になります。

すると、先のリスト14に示した条件分岐が違ってきます。
リスト16 条件分岐だけ考えると、forループの0回目と、ここだけが違う
 if (entries.get(nextLabel) == null) 


なぜなら ハッシュマップ entriesには、キーが"App", 値がtrueの組み合わせが一つ入っていますから、nullであるという過程は偽になります。
そこで、このあとの作業(リスト15)はスキップされます。


ApiDemosを最初に起動したとき:起動完了時の状態

以上のように、ループが終わった時、残るのは

「Appと表示し、クリックすると、メソッドgetData("App")を実行する欄」をひとつ表示する欄を表示する

という情報のみということになります。

これをクリックすると、今度はprefix = "App"で getDataメソッドが呼ばれます。
図4 ApiDemos起動時、forループ終了時

「App」をクリックしたとき:forループ0回目

図4の状態で「App」をクリックし、"App"を引数にgetDataが呼ばれたらどうなるかを考えます。

まず、listの内容は変わりません。list.sizeは2です。
各ループについて、labelPathのも変わりません。

しかし、prefixが違います。prefix="App"ですから、prefixPathは要素が1個の["App"]になります。
そして、prefixWithSlash="App/"になります。

0回目のループで、リスト8は、prefixWithSlash.length()==0 が今度は偽になります。
しかし、ラベルは"App/"で始まりますので、条件全体はやはり真になります。そこで、リスト9以下の条件を判断していきます。

リスト9では、prefixPathがnullではありませんから、nextLabelの値はリスト17のように書かれます。
リスト17 「prefixPath == null」が偽なので
String nextLabel = labelPath[prefixPath.length];


nextLabelは、labelPathの「prefixPath.length番目の要素」になります。
つまり、0から数えて「1」番目の要素ですから、nextLabel = "Activity"になります。

リスト11はif文の判定条件そのものが条件によって変わる形ですが、今度はprefixPath != nullが真になりますから、リスト18のようなiif文になります。
リスト18 「prefixPath != null」が真なので
if(prefixPath.length == labelPath.length-1)


リスト18の判定で、前者は1で後者は3ですから、偽になります。そこで、リスト14のelseに行きます。
0回目のループですから、entriesはnullです。そこで、brouseIntentが呼ばれます。
今度は、browseIntentの引数が、"App"+"/"+"Activity"となります。


「App」をクリックしたとき:forループ1回目

1回目のループでは、nextLabelの値が、ループ0回目と同じ"Activity"になりますから、entriesにすでに値が入っていることになり、処理はなされません。


「App」をクリックしたとき:ページ切り替え完了時の状態

以上、「App」をクリックしたとき切り替わる画面には、

「Activityと表示し、クリックすると、メソッドgetData("App/Activity")を実行する欄」をひとつ表示する

という処理がなされます。
図5 「App」をクリックして画面の切り替わりが完了したとき



「Activity」をクリックしたとき:forループ0回目

図5の状態でクリックすると、prefix = "App/Activity"でgetDataが呼ばれます。

今度は、prefixPath=["App","Activity"] と要素が2つです。

ラベルはprefixWithSlash="App/Activity/"で始まりますので、やはり条件全体が真になります。
labelPathは変わらず["App","Activity","Animation"]です。 prefixPath.length = 2になりますので、nextLabel =labelPath[2], すなわち"Animation"となります。 リスト13のif文は起動時以外は「if(prefixPath.length == labelPath.length-1)」になりますが、前者が2, 後者が3ですから、2==3-1で真になります。
これは、nextLabelがラベルの最後の項すなわち起動すべきアクティビティ名にまで来たことを示します。

そこで、今度は、リスト20のように、メソッドactivityIntentが呼ばれます。
リスト20 ifが真となり、activityIntentが呼ばれる

 if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length - 1) {
                    addItem(myData, nextLabel, activityIntent(
                            info.activityInfo.applicationInfo.packageName,
                            info.activityInfo.name));
                } else {
                ....//今まではbrowseIntentが呼ばれていた


これは、"Animation"という欄を表示し、それをクリックしたらアクティビティAnimationが起動する欄を表示するということです。
もうひとつ大事なのは、entriesがnullのままで置かれるということです。


「Activity」をクリックしたとき:forループ1回目

一方、1回目のループでは、まだリスト13のif文が真になりません。
なぜなら、prefixPathはループが0回目でも1回目でも同じですから、変わらず2ですが、labelPath.length=4です。
つまり、"App/Activity/ListActivity/Secure/Window"のほうはまだ、nextLabelとして"ListActivity"が得られます。 そこで、リスト14のelse以下を行います。0回目のループで「entries」がnullのままだったので、browseIntentが呼ばれる処理になります。


「Activity」をクリックしたとき: ページの切り替え完了後

この過程で初めて、リストに欄が二つ表示されます。すなわち、「クリックすると起動する"Animation"欄」の下に、「クリックするとgetDataが呼ばれる"ListActivity"」という欄が加わります。
図6 「Actiity」をクリックして画面の切り替わりが完了したとき



「ListActivity」をクリックしたとき

さて、図6で"ListActivity"をクリックしたらどうなるでしょうか?

実は、やっぱり、「App/Activity/Animation」「App/Activity/ListActivity/MyList」と二つともラベルが読み込まれます。
しかし、今度はprefixが"App/Activity/ListActivity"です。とすると、0回目のループではリスト8が偽になりますから、「何も処理せず終わり、1回目のループに処理が移ります。

1回目のループでは、nextLabelは"MyList"になります。
そして、とうとう「prefixPath.length == labelPath.length-1」が真になりますので、activityIntentメソッドが呼ばれて「MyListと表示され、クリックすると起動」する欄が追加されることになります。
図7 「ListActivity」をクリックしたとき
以上がApiDemosのリスト表示のしくみです。ずいぶんと面倒なことをやっていますね。
目次へ
清水美樹の本 トップページへ