第 2 回 : 仕様・設計と実装 ( IM "improve" の機能仕様と J2ME/MIDP のつまずきどころ)
はじめに
実際に IM "improve" を作成していきます。
IM "improve" の機能仕様を決定し、J2ME/MIDP 仕様上の制限をまとめます。
- 第 1 回 : 携帯 Java と IM
- 第 2 回 : 仕様・設計と実装( IM "improve" の機能仕様と J2ME/MIDP のつまずきどころ)
- 第 3 回 : 仕様・設計と実装( IM "improve" の設計・実装とエミュレーター動作)
- 第 4 回 : IM "improve" を実機に載せよう( IM "improve" のダウンサイズ)
- 第 5 回 : インターフェイス構築のヒント
- 第 6 回 : IM "improve" の実機対応と携帯電話の将来展望
improveの機能
IM "improve" の機能仕様をまとめます。
目標 | シンプルだけれども実用的な IM |
---|---|
想定ユーザー数 | 登録:〜50アカウント、同時接続:〜25クライアント |
端末 | MIDP 1.0 (後に KDDI Phase 2.5 [1:ezplus], J-PHONE 開発ガイド1.1.9の仕様 [2,3:J-PHONE]に対応) |
サーバー | Servlet on Tomcat 4.0.6 |
データベース | PostgreSQL 7.3 |
入会・退会
初回起動時にユーザー登録します。2 回目以降は自動ログインします。退会も可能です。
ユーザーアカウント
アカウント名は自分で決めます。ニックネームも設けます。
IM の基本機能
友達管理機能
友達を追加、削除できます。友達をリスト表示できます。名前の横のアイコンが友達の状態です。(a)〜(e)が友達リスト画面です。なお赤で表示されているのは自分です。
(a) 友達リスト(本人) | (b) 本人メニュー | (c) ステータス変更 |
(d) 友達リスト(友達) | (e) 友達メニュー | (f) メッセージング |
グループ
improve にはグループ機能は搭載しません。
友達の検索
improve には友達の検索機能は搭載しません。
状態確認機能
状態は次のように分類できます。
プロフィール
生年月日・住所・電話番号・趣味などのかなり静的な状態情報。
プレゼンス
オンライン、オフラインなどです。
ステータス
どこで何をしているかなどの状態。
improve での状態の扱い
プロフィール
電話番号以外は省略します。
プレゼンス
オンラインとオフラインの 2 つの状態とします。オフラインの場合は灰色信号を表示します。
ステータス
「いつでも OK (緑)・少しなら(黄)・ NG (赤)」の 3 つから選ぶか、自由文でも設定できます。友達リストでフォーカスをあわせると自由文がティッカーに流れます。
コミュニケーション機能
improve では 2 者間メッセージングのみ実装します。図1-(f)がメッセージング画面です。図 1 で右向き三角アイコンが、現在会話中の人を示しています。
その他の機能について
ポーリング
常時コネクションを張れない携帯 Java ではポーリングが必須です。図1-(f)の右下の「攻撃」コマンドで手動ポーリングもできます。
ティッカー
ティッカーとは文字列を左右に流して表示するコンポーネントです。図 1 で日時が表示されている部分がティッカー領域です。
セキュリティとプライバシー
improve ではセキュリティとプライバシーの機能は搭載しません。
機能のまとめ
画面遷移一覧
複雑な友達リストとメッセージング画面のみ Canvas で作成し、その他の画面は Panel を用います。
図2 画面遷移図
機能一覧
表 3 は搭載機能一覧です。
基本 | 入会・退会 | 初回登録、メニューから退会 |
---|---|---|
アカウント切り替え | 英数字 10 字以内、重複なし | |
アカウント名 | 英数 10 字以内、重複なし | |
ニックネーム | 日本語を含む9字以内 | |
パスワード | 自動生成、ユーザー不可視 | |
友達管理 | 表示 | 状態アイコン付き文字列リスト |
グループ | なし | |
追加 | アカウント指定 | |
削除 | メニューから | |
検索 | なし | |
状態確認 | プロフィール | 電話番号のみ |
プレゼンス | オンライン・オフライン | |
ステータス | チャット許容度3段階、自由文15文字以内 | |
コミュニケーション | チャット | 特定 2 者間、同時複数 |
電話 | 拡張予定 | |
その他 | ポーリング | 自動 2 分間隔、チャット時手動併用 |
プレゼンス | オンライン・オフライン | |
ティッカー | 独自・優先度あり | |
セキュリティー | なし | |
プライバシー保護 | なし | |
遮断機能 | なし |
動作の確認
検証は以下のエミュレーターと実機で行いました。
- ezplus エミュレータ Version1.0
- J-SKY Application Emulator Version 1.3B
- WTK104 エミュレータ
- au 実機 C3003P(Panasonic)
タイマーのキャンセル
タイマーについて
MIDP では Java.util パッケージに Timer クラスと TimerTask クラスが用意されています。ティッカーやアイコンの点滅、ポーリング間隔調整に使います。
MIDP のタイマー
- 1.TimerTask クラスを継承した MyTimerTask クラスで、 run() メソッドを実装する
- 2.MyTimerTask クラスのインスタンスを Timer クラスの schedule() メソッドで登録する
- 3.タイマが発生すると MyTimerTask クラスの run() メソッドが呼ばれる
- 4.タスクが不要になったら MyTimerTask クラスの cancel() メソッドで破棄する
図3 MIDP のタイマ
DoJa のタイマー
DoJa ではタイマーとして com.nttdocomo.util パッケージにイベント-リスナ型の Timer クラスと TimerListener インターフェースが用意されています。
- 1.TimerListener インターフェース MyTimerListener を実装する
- 2.MyTimerListener クラスのインスタンスを Timer クラスの setListener() メソッドで登録する
- 3.Timer クラスの start() メソッドでタイマを起動する
- 4.タイマイベントが発生すると MyTimerListener クラスの timerExpired() メソッドが呼ばれる
- 5.タイマが不要になったら Timer クラスの stop() メソッドでタイマを停止する
図4 DoJa のタイマ
DoJa と MIDP の Timer クラス
ティッカーに新しい文字列を設定するたびにタイマーを起動し、文字列が既定回数流れたら停止させるとします。
このとき、DoJa で Timer クラスの start() と stop() を使うように、MIDP で Timer クラスの schedule() でタスクを登録し、MyTimerTask クラスの cancel() でタスクを停めるのを繰り返そうとすると … ?
cancel() は停止じゃない!
MIDP において MyTimerTask クラスの cancel() メソッドを呼んだ後、再度同じ MyTimerTask クラスのインスタンスを schedule() で登録しようとすると、例外 IllegalStateException が発生します。
TimerTask クラスの cancel() メソッドはタスクの破棄であり、停止ではありません。タイマの発生前に同じインスタンスを再登録しても同様な例外が発生します。
また、 Timer クラスの cancel() メソッドを呼んだ後に schedule() メソッドを呼ぶと、例外 IllegalStateException が発生します。
Timer クラスと TimerTask クラスは Java.util パッケージにあり、その仕様は Java2 SE と同じです。DoJa から Java の開発を始めた開発者にとってつまずきやすいポイントです。
タイマタスクを使い回すには?
キャンセルされた MyTimerTask インスタンスは破棄しなければなりません。正常にタスクを終了した MyTimerTask インスタンスを再び schedule() で登録することは構いません。(ただし ezplus の仕様で、Phase 2.5 からは同一の MyTimerTask インスタンスを一度しか登録できないので注意してください。[1:ezplus])
しかし、キャンセルするたびに MyTimerTask クラスのインスタンスを作り直していたのでは効率が落ちます。フラグを 1 つ用意し、フラグが立っていればタイマが発生しても何もしないように run() メソッドを実装すれば、同じ MyTimerTask のインスタンスが使いまわせます。
サンプルプログラム「TimerTest」
サンプルプログラム TimerTest を見ていきましょう。
図5 TimerTest のスクリーンショット
TimerTest サンプルでは 2 つのタイマタスクを用います。「登録」でそれぞれをワンショットで 3 秒後と 5 秒後にスケジュールし、「キャンセル」で両タスクをキャンセルします。特に、以下の点を実際に確認してみてください。
■ タイマの登録前・登録後・発生後を問わず、 キャンセルした後に登録を行うと例外 IllegalStateException が発生する
■ タイマの登録後、 タイマの発生前に再度登録を行うと例外 IllegalStateException が発生する
※ WTK104 エミュレータでは少し挙動が異なり、タイマ発生後の単なる再登録においても IllegalStateException が発生します。
TimerTestMIDlet クラス (TimerTestMIDlet.Java)
MIDlet を継承したアプリのメインクラスです。詳細は「第 1 回 : 携帯 Java と IM 」を参照してください。
TimerTestForm クラス (TimerTestForm.Java)
Form を継承したアプリのメイン画面です。
12〜24行
2つのタイマタスクを実装し、 インスタンス化しています。 それぞれ StringItem である itemState1, itemState2 に setText() し、 タイマが発生したことを画面に表示します。
41行 コンストラクタ
画面の作成を行い、 65 行目でタイマを作成しています。
73行 commandAction() メソッド
キーが押されたときの処理を行います。
74〜79行
「登録」キーが押されたら timer.schedule() を呼び、 task1, task2 をそれぞれをワンショットで 3 秒後と 5 秒後にスケジュールします。 また、 画面にタスクの状態を表示します。
80〜86行
「キャンセル」キーが押されたら task1.cancel() および task2.cancel() を呼び、 両者をキャンセルします。 また、 画面にタスクの状態を表示します。
ダイアログを作ろう
確認ダイアログ
友達リストから友達を削除するときや、アプリケーションの終了確認など、「Yes」か「No」の確認ダイアログをプログラム上で手軽に使えると便利です。
図6 確認ダイアログの例
DoJa の場合
com.nttdocomo.ui パッケージに Dialog クラスがあります。
Dialog dialog = new Dialog(Dialog.DIALOG_YESNO, "削除確認"); dialog.setText("削除してもよろしいですか?"); int result = dialog.show();
とすることで確認ダイアログが表示され、 ユーザがどのボタンを押したのかを show() メソッドの返り値として得られます。
MIDP の場合
MIDP には DoJa の Dialog に対応するクラスがありません。MIDP では、2 通りの確認ダイアログのプログラム実装方法があります。
- 1.確認ダイアログを 1 つの画面遷移としてとらえ、 他の画面遷移と同列に扱う
- 2.他の画面遷移とは区別し、ダイアログを表示するクラスやメソッドを用意する
他の画面遷移とは、例えばログイン画面でログインが完了したら友達リストに移るとか、「メニュー」キーが押されたらメニューを表示するなどです。
1.は、MIDP では標準的な方法です。2.は DoJa の Dialog に似たクラスを用意する方法です。
improve では、2.の方法で確認ダイアログの実装します。
Alert クラス
MIDP にも、Javax.microedition.lcdui パッケージの Alert クラスというダイアログがあります。Alert クラスはデータの表示やエラーの通知を行うためのものです。タイムアウトも設定できます。ただし、ユーザは入力できません。
図7 Alert クラスの表示サンプル
DialogTest1 の実装
「表示」を押すと「Yes」「No」のついた確認ダイアログが表示されます。いずれかのソフトキーを押すと、結果が「ダイアログの結果:」に表示されます。
図8 DialogTest1 のスクリーンショット
DialogTest1MIDlet クラス (DialogTest1MIDlet.Java)
MIDlet を継承したアプリのメインクラスです。
この MIDlet の Display を外部から参照するために、10行目で Display 型の変数 display を public で宣言し、18行目で代入しています。
DialogTest1Form クラス (DialogTest1Form.Java)
Form を継承したアプリのメイン画面です。
21行 コンストラクタ
画面の作成を行います。
41行 commandAction() メソッド
キーが押されたときの処理を行います。 特に、 「表示」が押されたら45行目で showDialog() メソッドを呼んでいます。 また、 ダイアログの結果を itemResult に設定します。
70行 showDialog() メソッド
確認ダイアログの呼び出しメソッドです。 確認ダイアログ(DialogForm クラス)をインスタンス化し、 これに Display を切り替えます。
78行
確認ダイアログの「Yes」か「No」が押されるまで待ちます。 この wait() は139行か144行の notify() で解除されます。 その後、 Display を元の画面に切り替えます。
93行 DialogForm クラス
確認ダイアログのクラスです。
107行 コンストラクタ
画面の作成を行います。
126行 getCommandStatus() メソッド
status のアクセッサです。
135行 commmandAction() メソッド
キーが押されたときの処理を行います。 特に、 押されたキーにしたがって status を更新し、 78行の wait() を解除します。
以下に DialogTest1 で意図したシーケンス図を示します。
図9 DialogTest1 の意図されたシーケンス図
DialogTest1 の実行
DialogTest1 をエミュレータで実行し、「表示」を押してみます。
図10 DialogTest1 で「表示」を押したところ
ソフトキーのラベルは「Yes」と「No」に更新されましたが、確認ダイアログは表示されず、ソフトキーの入力も受け付けなくなり、アプリが固まってしまいました…?
MIDP のイベント処理
MIDP のイベント処理は、MIDP1.0 の仕様書[4:JSR-37]の 9.3 Event Handling に定義されています。それによると、MIDP の実装系はイベントの発生をコールバックによってアプリに通知しますが、UI コールバックには 4 種類があります。
- 1.高水準 API で用いられる抽象コマンド(Command クラス)
- 2.キー入力およびポインタ入力の低水準なイベント(Canvas クラスの keyPressed() など)
- 3.Canvas クラスの paint() メソッド呼び出し
- 4.Display#callSerially() の呼び出しで要求された Runnable オブジェクトの run() の呼び出し
これらの UI コールバックは直列化され、並列に実行されることはありません。また、UI コールバックは直前の UI コールバックが終了した後、呼び出されます。
1.の「抽象コマンド」とは、DialogTest1Form.Java の41行と135行の commandAction() メソッドに渡される Command クラスのことです。Command クラスはソフトキー入力に対応し、これを処理したいクラスでは CommandListener インターフェースを実装して、commandAction() メソッドにキー入力に対する処理内容を記述します。
ちなみに、前節のタイマイベントは UI イベントとはみなされません。したがって、タイマコールバック(TimerTask#run() の呼び出し)と UI コールバックは同時に処理される可能性があります。ただし、同一 TimerTask オブジェクトへのタイマコールバックは直列化されます。
commandAction()
DialogTest1 を System.out.println() でトレースしてみると、
- 1.(41行) DialogTest1Form#commandAction()
- 2.(70行) DialogTest1Form#showDialog()
- 3.(78行) DialogForm#wait()
で停止していることがわかります。このときの状況は先程述べたように、以下のようでした。
- 1.画面が更新されない(確認ダイアログが表示されない)
- 2.ソフトキーの入力(「Yes」と「No」)が受け付けられない
2.の理由は、先ほどの Command クラスによる UI コールバック、すなわち CommandListener インターフェースの commandAction() メソッド呼び出しが直列化されているためだと考えられます。つまり、41行の DialogTest1Form#commandAction() をまだ終了していないので、たとえキー入力があっても、135行の DialogForm#commandAction() が実行されないのです。
1.の理由については実装依存の部分があるので推測の域を出ませんが、Canvas の paint() が直列化されるのと同様に、Screen クラス(Form の親クラス。第1回の記事を参照)の描画も UI コールバックとして直列化されているためだと思われます。(実際、MIDP Reference Implementation 1.0.3 では Displayable (Canvas と Screen の親クラス)に抽象メソッド paint() があり、Canvas と Screen の描画をポリモーフィズムで処理しています。)この仮定の下では、やはり41行の DialogTest1Form#commandAction() を終了していないことが画面が更新されない原因です。
図11 DialogTest1 の実際のシーケンス図(固まる)
結局、アプリが固まる原因は commandAction() の中で wait() して停まってしまい、同メソッドから返っていないことです。これを回避するために、コマンドの処理をスレッド化します。
コマンド処理をスレッド化「DialogTest2」
commandAction() をただちに返すようにするため、commandAction() 内で新たにスレッドを起動し、すべての処理をそのスレッドにまかせるように DialogTest1 を変更します。
DialogTest2MIDlet クラス (DialogTest2MIDlet.Java)
MIDlet を継承したアプリのメインクラスです。使用する Form クラス以外は DialogTest1MIDlet.Java と同じです。
DialogTest2Form クラス (DialogTest2Form.Java)
Form を継承したアプリのメイン画面です。DialogTest1Form クラスから変更された部分を特に説明します。
9行 コマンド処理用ロック
commandAction() をただちに返すようにすると、 連続したコマンド入力(キー入力)があった場合には、 あるコマンド処理が終了する前に次のコマンドが入力される可能性があります。 このような場合にコマンド処理を直列化するためのロックに使用される変数です。
44行 commandAction() メソッド
キーが押されたときの処理を行います。 コマンド処理のロックを取得できれば、 コマンド処理スレッドを新たに起動します。 このロックは116行で解除されます。 ロックを取得できなかった場合はそのコマンドを破棄します。
56行 lockCommandAction() メソッド
コマンド処理のロックを取得しようと試みます。 成功すれば true が返ります。
67行 unlockCommandAction() メソッド
コマンド処理のロックを解除します。
74行 ProcessCommand クラス
実際のコマンド処理を行うクラスです。
89行 run() メソッド
ここで実際のコマンド処理を行います。 「表示」が押されたら showDialog() メソッドを呼んで確認ダイアログを表示します。
116行
コマンド処理が終了したのでロックを解除します。
124行 showDialog() メソッド
確認ダイアログを表示します。 DialogTest1 と同じです。
以下に DialogTest2 のシーケンス図を示します。
図12 DialogTest2 のシーケンス図
DialogTest2 の実行
DialogTest2 をエミュレータで実行します。
図13 DialogTest2 の実行結果
(a) DialogTest2 で「表示」を押したところ (b) 確認ダイアログで「Yes」を押したところ
今度は期待通りに動作しました。このとき、エミュレータのメッセージ欄には DialogTest2Form クラスと DialogForm クラスの commandAction() を実行したスレッドが出力されています。
DialogTest2Form#commandAction() Thread[@487789072,5] DialogForm#commandAction() Thread[@487789072,5]
両者は同一スレッドから呼び出されています。UI イベントの処理が直列化されているため、単一スレッドで実装されているのです。実機においても同様に実装される可能性があります。commandAction() からすぐに返らなければならないときは、注意する必要があります。たとえば通信などの長時間処理を書いてしまうと、アプリが固まったように見えます。
まとめ
- IM "improve" の機能仕様を決定しました。
- MIDP のつまずきどころを DoJa と比較しながら説明しました。
エキストラ トラック!
各キャリアの仕様・拡張機能など
J-PHONE、KDDI の Java ( J2ME/MIDP ) の仕様をまとめます。詳細は J-PHONE、KDDI のドキュメントをご確認ください。
参考文献
- 1.ezplus プログラミングガイド (Phase1〜2.5) Version 1.0
- 2.J-PHONE Java アプリ開発ガイド Version 1.1.9
- 3.J-PHONE Java アプリプログラミングガイド Version 1.03.03
- 4.Mobile Information Device Profile (JSR-37)
- 5.MID Profile Javadoc
- 6.DoJa2.0 Standard API Javadoc