MBaaS徹底入門――Kii Cloudでスマホアプリ開発

第7回Kii Cloudを用いたチャットアプリケーションの開発 [その3] ―チャットルームの実装

第5回・第6回と、チャットアプリケーションの「サインアップ(サインイン⁠⁠友達追加」機能の解説・実装を進めてきました。

第7回は、チャットルームアプリの入り口となる「チャットルーム作成」機能について解説します。前回追加した友達リストからチャットしたい友達を選び、チャットルームを作成・開始するところまでをみていきましょう。

今回使用したソースコードについては、GitHubで公開しています。こちらもあわせてご覧ください。

チャットルーム作成機能の実装

実装に入る前に、第5回で説明したチャットルーム作成機能について簡単におさらいします。チャットルーム作成機能は、以下のフローにより成り立ちます。

チャットルーム作成時のデータフロー
チャットルーム作成時のデータフロー
  • チャットルーム作成・開始者の入室:チャットを開始する人は、まずグループを作成し、チャットに招待したい人をグループのメンバーとして追加する。その後、チャットメッセージを保存するためのバケットを購読し、他のユーザーからメッセージが送信された場合にプッシュ通知を受信できるようにする。
  • チャット招待通知の送信:チャットを開始する人は、invite_notifyトピックを通じてチャットに招待したい人へプッシュ通知を送信する。この通知には作成されたグループの情報を付加し、どのチャットに招待されたか判別できるようにする。
  • 招待者の入室:チャットに招待された人は、招待通知メッセージに含まれているグループ情報からチャットを行うバケットを特定し入室と購読を行う。これによって他のユーザーからメッセージが送信された場合にプッシュ通知を受信できるようにする。

この3つのフローについて、コードを交えてそれぞれ説明していきます。

チャットルームの作成・開始者の入室

まずはチャットを開始するために、チャットルームの作成を行いましょう。

チャットのメッセージを保存するために、バケットの作成を行います。今回は特定のユーザーで構成されたグループでチャットを行うため、バケットのスコープを「グループ」にします。⁠スコープについては第3回をご参照ください)

チャットルーム作成までのフローは「友人リストから友人を選択→チャット部屋を作成」とするため、下記のコードを FriendListFragment に実装します。

// チャットに参加するユーザー名からチャットルームの名前を生成する
String chatRoomName = ChatRoom.getChatRoomName(KiiUser.getCurrentUser(), this.chatFriend);
// チャットに参加するユーザーのIDからユニークなキーを生成する
String uniqueKey = ChatRoom.getUniqueKey(KiiUser.getCurrentUser(), this.chatFriend);

// 現在メンバーとして参加しているグループを取得する
List<KiiGroup> existingGroup = KiiUser.getCurrentUser().memberOfGroups();

// 既に同じメンバーのグループが存在するかユーザーIDを用いてチェックする
for (KiiGroup kiiGroup : existingGroup) {
    if (TextUtils.equals(uniqueKey, ChatRoom.getUniqueKey(kiiGroup))) {
        return kiiGroup;
    }
}

// チャット用のグループをユーザー名を用いて作成
KiiGroup kiiGroup = Kii.group(chatRoomName);
KiiUser target = KiiUser.createByUri(Uri.parse(this.chatFriend.getUri()));
target.refresh();
kiiGroup.addUser(target);
kiiGroup.save();
// Chat用バケツを購読してメッセージをプッシュ通知してもらう状態にする
KiiBucket chatBucket = ChatRoom.getBucket(kiiGroup);
KiiUser.getCurrentUser().pushSubscription().subscribeBucket(chatBucket);

まずは、チャットグループがすでに作成済みかどうかを確認します。チャットに参加するユーザー全員の識別子(ここではユーザーのURI)を利用し、グループに一意なIDを作成します。そして自分がメンバーとして属しているグループのリストを KiiUser#memberOfGroups() で取得し、同じIDを持つグループが無いかどうかを確認します。すでに作成済みの場合はそのグループを利用し、存在しない場合は新規に作成します。

上記のチェックを経てグループが存在しない場合は、チャットに参加するユーザーの名前を利用し Kii.group(String name) でグループを作成します。グループが作成されたタイミングではグループに所属しているメンバーは作成者のみとなるため、KiiGroup#addUser(KiiUser user) でグループにチャットメンバーを追加します。

最後に KiiGroup#save() を利用しKiiCloud上へ保存を行います。

これでチャット開始者とチャット招待者を含んだグループの作成が完了しました。

次にチャットメッセージを保存するバケットを購読し、最新のメッセージが保存された場合に通知が送信されるようにします。KiiUser#pushSubscription() と KiiPushSubscription#subscribeBucket(KiiBucket bucket) を利用して購読を行います。

これでチャットを行うためのグループとバケットの作成が完了しました。

チャットに参加していない人(グループに属していない人)からは、このグループで行った操作やデータは参照することができません。バケット・トピック・オブジェクトそれぞれについてデフォルトのスコープを上手に利用することで、アクセス権をとても簡単に設定できるのがKiiCloudの特徴です。

チャット招待通知の送信

次にチャットに招待された人へ招待通知を送信しましょう。招待通知の送信にはトピックを利用し、ユーザーを指定したプッシュ通知を送信します。

プッシュ通知を利用するためには、GCMの設定・開発者ポータルの設定など準備が予め必要です。もし設定されていない方は第4回の「Push通知の有効化」をご覧いただき、準備をお願いいたします。

さて、すでに第5回で招待通知を受けるためのトピックを作成しているため、このトピックを利用して招待を送信しましょう。下記のコードを FriendListFragment に実装します。

// 招待ユーザーのURIからKiiUserのインスタンスを作成
KiiUser target = KiiUser.createByUri(Uri.parse(this.chatFriend.getUri()));

// 招待ユーザー上で作成済みのinvite_notifyトピックを指定
KiiTopic topic = target.topicOfThisUser(ApplicationConst.TOPIC_INVITE_NOTIFICATION);

// プッシュ通知で送信するデータにグループのURIを付加
Data data = new Data();
data.put(ChatRoom.CAHT_GROUP_URI, kiiGroup.toUri().toString());

// プッシュ通知を送信
KiiPushMessage message = KiiPushMessage.buildWith(data).build();
topic.sendMessage(message);

チャットに招待する友人のURIを取得し、そこからKiiUserを作成します。そのユーザー上で invite_notify のトピック(ApplicationConst.TOPIC_INVITE_NOTIFICATION)は作成済みのため、そのトピックへプッシュ通知を送信します。その際、どのグループに招待されたか判別できるようにグループのURIを情報として付与し、メッセージを送信します。

これでチャットしたい相手に招待を送信することができました。プッシュ通知を利用するためリアルタイムで通知することができます。

招待者の入室(プッシュ通知契機)

チャットに招待されたユーザー上の端末では、⁠チャット招待通知の送信」による招待メッセージが届きます。この時点ではユーザーはまだチャットルームに入る権限はあるものの、まだチャットルームに入っていない状態です。

そのためチャットルームへ入室し、今後そのルームへメッセージがあった場合に新着通知が届くようにプッシュ通知の設定を行います。

上記のフローはプッシュ通知を契機とするため、GCMPushReceiver#onReceive に下記のコードを追加します。

public void onReceive(final Context context, Intent intent) {
    // GCM経由で送信されたメッセージかどうかを判断
    GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
    String messageType = gcm.getMessageType(intent);
    if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
        final Bundle extras = intent.getExtras();
        // MessageTypeを判断
        ReceivedMessage message = PushMessageBundleHelper.parse(extras);
        MessageType type = message.pushMessageType();
        switch (type) {
            // トピックを利用したプッシュ通知である場合
            case PUSH_TO_USER:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 受信したメッセージからグループのURIを取得
                            String groupUri =  extras.getString(ChatRoom.CAHT_GROUP_URI);
                            // 取得したURIを元にグループのインスタンスを作成
                            KiiGroup kiiGroup = KiiGroup.createByUri(Uri.parse(groupUri));
                            // 自分がメンバーでないChatRoomは無視する
                            if (isMember(kiiGroup)) {
                                // チャットルームのバケットを取得
                                KiiBucket chatBucket = ChatRoom.getBucket(kiiGroup);
                                // チャットルームのバケットを購読
                                KiiUser.getCurrentUser().pushSubscription().subscribeBucket(chatBucket);
                                sendBroadcast(context, ApplicationConst.ACTION_CHAT_STARTED, groupUri);
                            }
                        } catch (Exception e) {
                            Logger.e("Unable to subscribe group bucket", e);
                        }
                    }
                }).start();
                break;
        }
    }
}

private boolean isMember(KiiGroup kiiGroup) throws GroupOperationException {
    if (KiiUser.getCurrentUser() != null) {
        kiiGroup.refresh();
        List<KiiUser> members = kiiGroup.listMembers();
        for (KiiUser member : members) {
            if (KiiUser.getCurrentUser().toUri().equals(member.toUri())) {
                return true;
            }
        }
    }
    return false;
}

private void sendBroadcast(Context context, String action, String message) {
    Intent intent = new Intent(action);
    intent.putExtra(ApplicationConst.EXTRA_MESSAGE, message);
    context.sendBroadcast(intent);
}

まずは受信したメッセージについてGCM経由で送信されたメッセージかどうかを判断し、その後Kiiのプッシュメッセージのタイプを判定します。今回はトピック経由で送信されたため、メッセージタイプは PUSH_TO_USER となります。

そして、受信したメッセージからグループのURIを取得・パースした後、KiiGroup.createByUri(Uri groupUri) でグループを作成します。こうして、チャット開始者が作成したグループが招待者のアプリケーション上でも作成されます。

しかしながら、1デバイスで複数アカウントを利用している場合に、ログインしているユーザーとは別のユーザーに対する招待通知を受信する可能性があります。そのため、通知に含まれているグループに所属しているかの確認を isMember メソッドで行います。このisMemberメソッド中では、KiiGroup#refresh() によるグループ情報の取得・更新と、 KiiGroup#listMembers() で取得したメンバーリスト中に自分自身が含まれているかの確認を行っています。

次にチャットルームのバケットを取得し、バケットの購読を行います。バケット名は全てのユーザーで同じ名前を利用していますが、グループごとに別々のものとして扱われます。

最後に、チャットルームのバケットを KiiUser#pushSubscription() と KiiPushSubscription#subscribeBucket(KiiBucket bucket) を利用して購読を行います。こうすることでバケットにメッセージが送信された場合、プッシュ通知が送信されるようになります。

招待者の入室(ユーザーサインイン契機)

前項ではプッシュ通知を契機とした入室処理を行いましたが、これだけでは招待者が必ずしもチャットルームに入室できるとは限りません。

たとえば「長い間デバイスの電源が切れていてプッシュ通知の再送期限を越えたため、通知が届かなかった」⁠アプリケーションからログアウトしていた」などの要因によって、招待通知を取りこぼすことが考えられます。

そのためユーザーサインインを契機として招待者がチャットルームに入室できるよう、下記のメソッドをChatRoomクラスに追加します。

public static void ensureSubscribedBucket(final KiiUser kiiUser) {
    new AsyncTask<Void, Void, Boolean>() {
        @Override
        protected Boolean doInBackground(Void... params) {
            try {
                // ユーザが所属しているグループの一覧を取得
                List<KiiGroup> groups = kiiUser.memberOfGroups();
                for (KiiGroup group : groups) {
                    // 全てのグループでchat_roomバケツを購読済みであることを確認
                    KiiBucket chatBucket = ChatRoom.getBucket(group);
                    boolean isSubscribed = kiiUser.pushSubscription().isSubscribed(chatBucket);
                    if (!isSubscribed) {
                        // 購読されていない場合は、購読する
                        Logger.i("---------------- 取りこぼし対応 ----------------");
                        kiiUser.pushSubscription().subscribeBucket(chatBucket);
                    }
                }
                return true;
            } catch (Exception e) {
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (!result) {
                Logger.e("Unable to subscribe group bucket");
            }
        }
    }.execute();
}

まずは、ユーザーが所属しているグループのリストを KiiUser#memberOfGroups() で取得します。次にそれぞれのグループについてチャットルームのバケットを取得し、 KiiUser#pushSubscription() と KiiPushSubscription#isSubscribed(KiiBucket bucket) を利用して、バケットを購読しているかどうかの確認を行います。もし購読していないバケットが存在した場合には、 KiiUser#pushSubscription() と KiiPushSubscription#subscribeBucket(KiiBucket bucket) を利用して購読を行います。

これによって、プッシュを受信できなかった場合の購読処理に対応することができます。プッシュ通知を契機とした実装を行う際には、受信できなかった場合のフローも考慮するとより安定性のあるアプリケーションを作成することができます。

まとめ

チャットルームの実装を終えて、いかがでしたでしょうか。

今回はバケット・トピックに関する実装に加え、プッシュ通知に関する実装も行いました。とくにプッシュ通知は、サーバーを別途用意することなく実装できるうえ、Android - iOS間でのメッセージ送信にも対応しており、MBaaSならではの恩恵を大きく受けることができます。ぜひプッシュ通知を用いたAndroid・iOSアプリケーションを開発される場合は利用してみてください。

次回は主要機能の最後となる「メッセージ送受信」部分について実装を行っていきます。どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧