前回は、
今回使用したソースコードについては、
メッセージ送受信機能の実装
実装に入る前に、

- 新規スタンプのアップロード:ユーザは好きな画像を
“chat_ stamps” バケットにアップロードすることでスタンプとして利用できます。アップロードされたスタンプはチャットを利用するすべてのユーザが使用することができます。 - スタンプ一覧の取得:スタンプを送信する場合は、
まず、 “chat_ stamps” バケットを検索して利用可能なスタンプの一覧を取得します。その後、 1件ずつスタンプの画像をダウンロードして画面に表示します。 - メッセージの送信:メッセージの送信は
“chat_ room” バケットにKiiObjectを保存することで実現できます。チャットの相手は対象の” chat_ room” バケットを購読しているはずなので、 新しいKiiObjectを保存するだけで、 自動的にプッシュ通知が相手に届きます。 - メッセージの受信:メッセージの受信はプッシュ通知を契機におこないます。プッシュ通知を受信したら、
対象の “chat_ room” バケットを検索しメッセージを取得して画面に表示します。受信したメッセージがスタンプを含んでいる場合は、 スタンプの画像をダウンロードします。同じ画像を何回もダウンロードする必要がないように、 画像は端末にキャッシュとして保存しておきます。
この4つのフローの実装について、
①新規スタンプのアップロード
スタンプはアプリケーションスコープの”

スタンプに関係する処理は基本的にChatStampクラスに実装されています。さっそく実際のコードを見ていきましょう。
public class ChatStamp extends KiiObjectWrapper {
private File imageFile;
private String uri;
public ChatStamp(File imageFile) {
super(getBucket().object());
this.imageFile = imageFile;
}
public ChatStamp(KiiObject kiiObject) {
super(kiiObject);
this.uri = kiiObject.toUri().toString();
}
public ChatStamp(ChatMessage message) {
super(KiiObject.createByUri(Uri.parse(message.getStampUri())));
this.uri = message.getStampUri();
}
public void save() throws Exception {
this.kiiObject.save();
if (this.imageFile != null) {
this.uri = this.kiiObject.toUri().toString();
KiiUploader uploader = this.kiiObject.uploader(KiiChatApplication.getContext(), this.imageFile);
uploader.transfer(null);
// アップロードしたファイルを、KiiObjectのURIに応じた名前にリネームする
File cacheFile = StampCacheUtils.getCacheFile(this.kiiObject.toUri().toString());
this.imageFile.renameTo(cacheFile);
}
}
...
}
ChatStampクラスは大きく2種類のコンストラクタを提供しています。新たにスタンプを追加する場合は、
ChatStamp#save()メソッドでKiiObjectの保存と、
まずKiiObject#save() でKiiObjectを保存します。このタイミングでこのスタンプのURIが割り当てられます。次にKiiObject#uploader(Context, File) でKiiUploaderのインスタンスを生成し、
また、
最後に、
②スタンプ一覧の取得
スタンプ一覧の取得は、
- “chat_
stamps” バケット内のすべてのオブジェクトを検索する (画像は含まない) - 取得したKiiObject全てに対して画像を取得する
“chat_
実際のソースコードは、
まず、
スタンプ一覧の取得には第6回でも登場したKiiQueryを使用します。今回は、
これはスタンプの表示順が毎回変わってしまうのを防止するためです。
“_created”
これらのプロパティは一覧を時系列でソートしたい場合にとても役に立ちます。
public static List<ChatStamp> list() {
List<ChatStamp> stamps = new ArrayList<ChatStamp>();
try {
// 作成日時でソートして、クエリ結果の順序を保証する
KiiQuery query = new KiiQuery();
query.sortByAsc(FIELD_CREATED);
List<KiiObject> objects = getBucket().query(query).getResult();
for (KiiObject object : objects) {
stamps.add(new ChatStamp(object));
}
return stamps;
} catch (Exception e) {
Logger.e("Unable to list stamps", e);
return stamps;
}
}
つづいて画像をダウンロードする処理を見ていきましょう。アップロード処理のコードと非常に似ていて簡単に理解できるかと思います。
KiiObject#downloader(Context, File) KiiDownloaderのインスタンスを生成しKiiDownloader#transfer(KiiRTransferProgressCallback)で画像ファイルをダウンロードしています。
public Bitmap getImage() {
try {
byte[] image = null;
if (this.imageFile != null) {
// ファイルを指定してChatStampのインスタンスが生成された場合 (新規スタンプの追加時)
image = readImageFromLocal(this.imageFile);
} else if (this.uri != null) {
// イメージがキャッシュされていれば、キャッシュから読み込む
File cacheFile = StampCacheUtils.getCacheFile(this.uri);
if (cacheFile.exists()) {
image = readImageFromLocal(cacheFile);
} else {
// キャッシュに存在しない場合は、KiiCloudからダウンロードする
Logger.i("downloads stamp image from KiiCloud");
KiiDownloader downloader = kiiObject.downloader(KiiChatApplication.getContext(), cacheFile);
downloader.transfer(null);
image = readImageFromLocal(cacheFile);
}
}
if (image != null) {
return BitmapFactory.decodeByteArray(image, 0, image.length);
}
Logger.w("failed to download stamp");
return null;
} catch (Exception e) {
Logger.e("failed to download stamp", e);
return null;
}
}
③メッセージの送信
メッセージの送信処理はChatActivityクラスに実装されています。メッセージはテキストとスタンプの2種類があり、
通常のテキストとスタンプの違いは、
コードは下記のようにChatMessageのインスタンスを生成して、
// 送信ボタンが押された場合のテキストのメッセージを送信する処理
@Override
public void onClick(View v) {
// 入力されたメッセージをバックグラウンドでKiiCloudに保存する
btnSend.setEnabled(false);
final ChatMessage message = new ChatMessage(kiiGroup);
message.setMessage(editMessage.getText().toString());
message.setSenderUri(KiiUser.getCurrentUser().toUri().toString());
new SendMessageTask(message).execute();
}
// スタンプが選択された場合のスタンプを送信する処理
@Override
public void onSelectStamp(ChatStamp stamp) {
// 選択されたスタンプをメッセージとしてバックグラウンドでKiiCloudに保存する
new SendMessageTask(ChatMessage.createStampChatMessage(this.kiiGroup, stamp)).execute();
}
// バックグラウンドでKiiObject#save() メソッドを実行するインナークラス
private class SendMessageTask extends AsyncTask<Void, Void, Boolean> {
private final ChatMessage message;
private SendMessageTask(ChatMessage message) {
this.message = message;
}
@Override
protected Boolean doInBackground(Void... params) {
try {
// メッセージを保存する
this.message.getKiiObject().save();
return true;
} catch (Exception e) {
Logger.e("failed to send messsage", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
editMessage.setText("");
} else {
ToastUtils.showShort(ChatActivity.this, "Unable to send messsage");
}
}
}
//スタンプを表現するChatMessageを生成するファクトリメソッド
public static ChatMessage createStampChatMessage(KiiGroup kiiGroup, ChatStamp stamp) {
ChatMessage message = new ChatMessage(kiiGroup);
message.setMessage("$STAMP:" + stamp.getUri());
message.setSenderUri(KiiUser.getCurrentUser().toUri().toString());
return message;
}
④メッセージの受信
メッセージの受信処理は、

前回のチャットルームの実装で説明したのと同様にGCMPushReceiver#onReceiveにプッシュ通知を受け取った処理を記述します。今回はバケット内のオブジェクト作成を契機に、
受信したメッセージを表すReceivedMessageインスタンスは、
今回はメッセージタイプがPUSH_
受信したメッセージが自身が所属するグループ(ChatRoom)宛のメッセージか確認した後にBroadcast Intentを使ってChatActivityに新規メッセージを受信したことを伝えます。
case PUSH_TO_APP:
Logger.i("received PUSH_TO_APP");
try {
// 参加中のChatに新規メッセージが投稿された場合
Logger.i("received PUSH_TO_USER");
new Thread(new Runnable() {
@Override
public void run() {
try {
KiiObject obj = ((PushToAppMessage)message).getKiiObject();
obj.refresh();
ChatMessage chatMessage = new ChatMessage(obj);
KiiGroup kiiGroup = KiiGroup.createByUri(Uri.parse(chatMessage.getGroupUri()));
// 自分がメンバーでないChatRoomは無視する
if (isMember(kiiGroup)) {
sendBroadcast(context, ApplicationConst.ACTION_MESSAGE_RECEIVED, chatMessage.getKiiObject().toJSON().toString());
}
} catch (Exception e) {
Logger.e("Unable to subscribe group bucket", e);
}
}
}).start();
} catch (Exception e) {
Logger.e("Unable to get the KiiObject", e);
}
break;
// BroadcastでActivityに新規メッセージを受信したことを伝える
private void sendBroadcast(Context context, String action, String message) {
Intent intent = new Intent(action);
intent.putExtra(ApplicationConst.EXTRA_MESSAGE, message);
context.sendBroadcast(intent);
}
ChatActivityクラスでは、
また、
private final BroadcastReceiver handleMessageReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// GCMPushReceiverからBroadcast Intentを受信した場合
updateMessage(false);
}
};
private void updateMessage(boolean showProgress) {
new GetMessageTask(showProgress).execute();
}
private class GetMessageTask extends AsyncTask<Void, Void, List<ChatMessage>> {
private final boolean showProgress;
private GetMessageTask(boolean showProgress) {
this.showProgress = showProgress;
}
@Override
protected void onPreExecute() {
if (this.showProgress) {
SimpleProgressDialogFragment.show(getSupportFragmentManager(), "Chat", "Loading...");
}
}
@Override
protected List<ChatMessage> doInBackground(Void... params) {
try {
ChatRoom chatRoom = new ChatRoom(kiiGroup);
List<ChatMessage> messages = null;
if (lastGotTime == null) {
messages = chatRoom.getMessageList();
} else {
// 前にメッセージを取得済みの場合は、最後に取得したメッセージより新しいメッセージのみを取得する
messages = chatRoom.getMessageList(lastGotTime);
}
if (messages.size() > 0) {
// messagesは_createdで昇順にソート済みなので、リストの最後の要素が最新のメッセージとなる
lastGotTime = messages.get(messages.size() - 1).getKiiObject().getCreatedTime();
}
return messages;
} catch (Exception e) {
return null;
}
}
@Override
protected void onPostExecute(List<ChatMessage> messages) {
if (messages != null) {
adapter.addAll(messages);
adapter.notifyDataSetChanged();
} else {
ToastUtils.showShort(ChatActivity.this, "Unable to get message");
}
if (this.showProgress) {
SimpleProgressDialogFragment.hide(getSupportFragmentManager());
} else {
vibrator.vibrate(500);
}
listView.setSelection(listView.getCount());
}
}
// chat_roomを検索するクエリの生成 (ChatMessageクラス内)
public static KiiQuery createQuery(Long modifiedSinceTime) {
KiiQuery query = null;
if (modifiedSinceTime != null) {
// 最新のメッセージのみ取得
query = new KiiQuery(KiiClause.greaterThan(FIELD_CREATED, modifiedSinceTime));
} else {
// 全件取得
query = new KiiQuery();
}
query.sortByAsc(FIELD_CREATED);
return query;
}
メッセージをListViewに表示する処理はChatActivityクラスのインナークラスであるMessageListAdapterに実装されています。
具体的にはメッセージが自分自身が送信したメッセージかどうかによって表示レイアウトを変更して、
private class MessageListAdapter extends AbstractArrayAdapter<ChatMessage> {
private static final int ROW_SELF_MESSAGE = 0;
private static final int ROW_FRIEND_MESSAGE = 1;
private static final int ROW_SELF_STAMP = 2;
private static final int ROW_FRIEND_STAMP = 3;
private final LayoutInflater inflater;
private final String userUri;
public MessageListAdapter(Context context, KiiUser kiiUser) {
super(context, R.layout.chat_message_me);
this.inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.userUri = kiiUser.toUri().toString();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
ChatMessage chatMessage = this.getItem(position);
if (convertView == null) {
switch (getRowType(chatMessage)) {
case ROW_SELF_MESSAGE:
// ログイン中のユーザが送信したメッセージの場合
convertView = this.inflater.inflate(R.layout.chat_message_me, parent, false);
break;
case ROW_SELF_STAMP:
// ログイン中のユーザが送信したスタンプの場合
convertView = this.inflater.inflate(R.layout.chat_stamp_me, parent, false);
break;
case ROW_FRIEND_MESSAGE:
// 他のユーザが送信したメッセージの場合
convertView = this.inflater.inflate(R.layout.chat_message_friend, parent, false);
break;
case ROW_FRIEND_STAMP:
// 他のユーザが送信したスタンプの場合
convertView = this.inflater.inflate(R.layout.chat_stamp_friend, parent, false);
break;
}
holder = new ViewHolder();
if (chatMessage.isStamp()) {
holder.message = null;
holder.stamp = (ImageView)convertView.findViewById(R.id.row_stamp);
} else {
holder.message = (TextView)convertView.findViewById(R.id.row_message);
holder.stamp = null;
}
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
if (chatMessage.isStamp()) {
ChatStamp stamp = new ChatStamp(chatMessage);
imageFetcher.fetchStamp(stamp, holder.stamp);
} else {
String message = chatMessage.getMessage() == null ? "" : chatMessage.getMessage();
holder.message.setText(message);
}
return convertView;
}
@Override
public int getViewTypeCount() {
// ListViewに表示する行の種類は「自分のメッセージ、スタンプ」「友達のメッセージ、スタンプ」の4種類あるので4を返す。
return 4;
}
@Override
public int getItemViewType(int position) {
// 与えられた位置の行が、「自分のメッセージ」か「友達のメッセージ」かを判定する
return getRowType(getItem(position));
}
private int getRowType(ChatMessage chatMessage) {
if (TextUtils.equals(this.userUri, chatMessage.getSenderUri())) {
if (chatMessage.isStamp()) {
return ROW_SELF_STAMP;
} else {
return ROW_SELF_MESSAGE;
}
} else {
if (chatMessage.isStamp()) {
return ROW_FRIEND_STAMP;
} else {
return ROW_FRIEND_MESSAGE;
}
}
}
}
以上で、
今回の実装で簡単ではありますがメッセージのやりとりを行えるアプリケーションが完成しました。チャットアプリケーションのような通信アプリを作成する場合、
MBaaSの登場により、
次回の第9回はチャットアプリケーションを拡張して、