001/**
002 * Copyright 2015, Digium, Inc.
003 * All rights reserved.
004 *
005 * This source code is licensed under The MIT License found in the
006 * LICENSE file in the root directory of this source tree.
007 *
008 * For all details and documentation:  https://www.respoke.io
009 */
010
011package com.digium.respokesdk;
012
013import android.os.Handler;
014import android.os.Looper;
015
016import org.json.JSONArray;
017import org.json.JSONException;
018import org.json.JSONObject;
019
020import java.lang.ref.WeakReference;
021import java.util.ArrayList;
022import java.util.Date;
023
024
025/**
026 *  A group, representing a collection of connections and the method by which to communicate with them.
027 */
028public class RespokeGroup {
029
030    private WeakReference<Listener> listenerReference;
031    private String groupID;  ///< The ID of this group
032    private WeakReference<RespokeClient> clientReference;  ///< The client managing this group
033    private RespokeSignalingChannel signalingChannel;  ///< The signaling channel to use
034    private ArrayList<RespokeConnection> members;  ///< An array of the members of this group
035    private boolean joined;  ///< Indicates if the client is a member of this group
036
037
038    /**
039     *  A listener interface to notify the receiver of events occurring with the group
040     */
041    public interface Listener {
042
043
044        /**
045         *  Receive a notification that an connection has joined this group.
046         *
047         *  @param connection The RespokeConnection that joined the group
048         *  @param sender     The RespokeGroup that the connection has joined
049         */
050        void onJoin(RespokeConnection connection, RespokeGroup sender);
051
052
053        /**
054         *  Receive a notification that an connection has left this group.
055         *
056         *  @param connection The RespokeConnection that left the group
057         *  @param sender     The RespokeGroup that the connection has left
058         */
059        void onLeave(RespokeConnection connection, RespokeGroup sender);
060
061
062        /**
063         *  Receive a notification that a group message has been received
064         *
065         *  @param message  The body of the message
066         *  @param endpoint The endpoint that sent the message
067         *  @param sender   The group that received the message
068         *  @param timestamp The timestamp of when the message was sent.
069         */
070        void onGroupMessage(String message, RespokeEndpoint endpoint, RespokeGroup sender, Date timestamp);
071
072
073    }
074
075
076    /**
077     *  A listener interface to receive a notification that the task to get the list of group members has completed
078     */
079    public interface GetGroupMembersCompletionListener {
080
081        /**
082        *  Receive an array of the group members asynchronously
083        *
084        *  @param memberArray An array of the connections that are a member of this group
085        */
086        void onSuccess(ArrayList<RespokeConnection> memberArray);
087
088
089        /**
090         *  Receive a notification that the asynchronous operation failed
091         *
092         *  @param errorMessage  A human-readable description of the error that was encountered
093         */
094        void onError(String errorMessage);
095    }
096
097    /**
098     *  The constructor for this class
099     *
100     *  @param newGroupID  The ID for this group
101     *  @param channel     The signaling channel managing communications with this group
102     *  @param newClient   The client to which this group instance belongs
103     *  @param isJoined    Whether the group has already been joined
104     */
105    public RespokeGroup(String newGroupID, RespokeSignalingChannel channel, RespokeClient newClient,
106                        Boolean isJoined) {
107        groupID = newGroupID;
108        signalingChannel = channel;
109        clientReference = new WeakReference<RespokeClient>(newClient);
110        members = new ArrayList<RespokeConnection>();
111        joined = isJoined;
112    }
113
114    public RespokeGroup(String newGroupID, RespokeSignalingChannel channel, RespokeClient newClient) {
115        this(newGroupID, channel, newClient, true);
116    }
117
118    /**
119     *  Set a receiver for the Listener interface
120     *
121     *  @param listener  The new receiver for events from the Listener interface for this group instance
122     */
123    public void setListener(Listener listener) {
124        listenerReference = new WeakReference<Listener>(listener);
125    }
126
127
128    /**
129     *  Get an array containing the members of the group.
130     *
131     *  @param completionListener  A listener to receive a notification on the success of the asynchronous operation
132     **/
133    public void getMembers(final GetGroupMembersCompletionListener completionListener) {
134        if (isJoined()) {
135            if ((null != groupID) && (groupID.length() > 0)) {
136                String urlEndpoint = "/v1/channels/" + groupID + "/subscribers/";
137
138                signalingChannel.sendRESTMessage("get", urlEndpoint, null, new RespokeSignalingChannel.RESTListener() {
139                    @Override
140                    public void onSuccess(Object response) {
141                        JSONArray responseArray = null;
142
143                        if (response != null) {
144                            if (response instanceof JSONArray) {
145                                responseArray = (JSONArray) response;
146                            } else if (response instanceof String) {
147                                try {
148                                    responseArray = new JSONArray((String) response);
149                                } catch (JSONException e) {
150                                    // An exception will trigger the error handler
151                                }
152                            }
153                        }
154
155                        if (null != responseArray) {
156                            final ArrayList<RespokeConnection> nameList = new ArrayList<RespokeConnection>();
157                            RespokeClient client = clientReference.get();
158                            if (null != client) {
159                                for (int ii = 0; ii < responseArray.length(); ii++) {
160                                    try {
161                                        JSONObject eachEntry = (JSONObject) responseArray.get(ii);
162                                        String newEndpointID = eachEntry.getString("endpointId");
163                                        String newConnectionID = eachEntry.getString("connectionId");
164
165                                        // Do not include ourselves in this list
166                                        if (!newEndpointID.equals(client.getEndpointID())) {
167                                            // Get the existing instance for this connection, or create a new one if necessary
168                                            RespokeConnection connection = client.getConnection(newConnectionID, newEndpointID, false);
169
170                                            if (null != connection) {
171                                                nameList.add(connection);
172                                            }
173                                        }
174                                    } catch (JSONException e) {
175                                        // Skip unintelligible records
176                                    }
177                                }
178                            }
179
180                            // If certain connections present in the members array prior to this method are somehow no longer in the list received from the server, it's assumed a pending onLeave message will handle flushing it out of the client cache after this method completes
181                            members.clear();
182                            members.addAll(nameList);
183
184                            new Handler(Looper.getMainLooper()).post(new Runnable() {
185                                @Override
186                                public void run() {
187                                    if (null != completionListener) {
188                                        completionListener.onSuccess(nameList);
189                                    }
190                                }
191                            });
192                        } else {
193                            postGetGroupMembersError(completionListener, "Invalid response from server");
194                        }
195                    }
196
197                    @Override
198                    public void onError(final String errorMessage) {
199                        postGetGroupMembersError(completionListener, errorMessage);
200                    }
201                });
202            } else {
203                postGetGroupMembersError(completionListener, "Group name must be specified");
204            }
205        } else {
206            postGetGroupMembersError(completionListener, "Not a member of this group anymore.");
207        }
208    }
209
210
211    /**
212     * Join this group
213     *
214     * @param completionListener A listener to receive a notification upon completion of this
215     *                           async operation
216     */
217    public void join(final Respoke.TaskCompletionListener completionListener) {
218        if (!isConnected()) {
219            Respoke.postTaskError(completionListener, "Can't complete request when not connected. " +
220                    "Please reconnect!");
221            return;
222        }
223
224        if ((groupID == null) || (groupID.length() == 0)) {
225            Respoke.postTaskError(completionListener, "Group name must be specified");
226            return;
227        }
228
229        String urlEndpoint = String.format("/v1/groups/%s", groupID);
230        signalingChannel.sendRESTMessage("post", urlEndpoint, null, new RespokeSignalingChannel.RESTListener() {
231            @Override
232            public void onSuccess(Object response) {
233                joined = true;
234                Respoke.postTaskSuccess(completionListener);
235            }
236
237            @Override
238            public void onError(final String errorMessage) {
239                Respoke.postTaskError(completionListener, errorMessage);
240            }
241        });
242    }
243
244
245    /**
246     *  Leave this group
247     *
248     *  @param completionListener  A listener to receive a notification on the success of the asynchronous operation
249     **/
250    public void leave(final Respoke.TaskCompletionListener completionListener) {
251        if (isJoined()) {
252            if ((null != groupID) && (groupID.length() > 0)) {
253                String urlEndpoint = "/v1/groups";
254
255                JSONArray groupList = new JSONArray();
256                groupList.put(groupID);
257
258                JSONObject data = new JSONObject();
259                try {
260                    data.put("groups", groupList);
261
262                    signalingChannel.sendRESTMessage("delete", urlEndpoint, data, new RespokeSignalingChannel.RESTListener() {
263                        @Override
264                        public void onSuccess(Object response) {
265                            joined = false;
266
267                            Respoke.postTaskSuccess(completionListener);
268                        }
269
270                        @Override
271                        public void onError(final String errorMessage) {
272                            Respoke.postTaskError(completionListener, errorMessage);
273                        }
274                    });
275                } catch (JSONException e) {
276                    Respoke.postTaskError(completionListener, "Error encoding group list to json");
277                }
278            } else {
279                Respoke.postTaskError(completionListener, "Group name must be specified");
280            }
281        } else {
282            Respoke.postTaskError(completionListener, "Not a member of this group anymore.");
283        }
284    }
285
286
287    /**
288     *  Return true if the local client is a member of this group and false if not.
289     *
290     *  @return The membership status
291     */
292    public boolean isJoined() {
293        return joined && isConnected();
294    }
295
296    public boolean isConnected() {
297        return (null != signalingChannel) && signalingChannel.connected;
298    }
299
300    /**
301     *  Get the ID for this group
302     *
303     *  @return The group's ID
304     */
305    public String getGroupID() {
306        return groupID;
307    }
308
309
310    /**
311     *  Send a message to the entire group.
312     *
313     *  @param message             The message to send
314     *  @param push                A flag indicating if a push notification should be sent for this message
315     *  @param persist             A flag indicating if history should be maintained for this message.
316     *  @param completionListener  A listener to receive a notification on the success of the asynchronous operation
317     **/
318    public void sendMessage(String message, boolean push, boolean persist,
319                            final Respoke.TaskCompletionListener completionListener) {
320        if (isJoined()) {
321            if ((null != groupID) && (groupID.length() > 0)) {
322                RespokeClient client = clientReference.get();
323                if (null != client) {
324
325                    JSONObject data = new JSONObject();
326
327                    try {
328                        data.put("endpointId", client.getEndpointID());
329                        data.put("message", message);
330                        data.put("push", push);
331                        data.put("persist", persist);
332                    } catch (JSONException e) {
333                        Respoke.postTaskError(completionListener, "Unable to encode message");
334                        return;
335                    }
336
337                    String urlEndpoint = "/v1/channels/" + groupID + "/publish/";
338                    signalingChannel.sendRESTMessage("post", urlEndpoint, data, new RespokeSignalingChannel.RESTListener() {
339                        @Override
340                        public void onSuccess(Object response) {
341                            Respoke.postTaskSuccess(completionListener);
342                        }
343
344                        @Override
345                        public void onError(final String errorMessage) {
346                            Respoke.postTaskError(completionListener, errorMessage);
347                        }
348                    });
349                } else {
350                    Respoke.postTaskError(completionListener, "There was an internal error processing this request.");
351                }
352            } else {
353                Respoke.postTaskError(completionListener, "Group name must be specified");
354            }
355        } else {
356            Respoke.postTaskError(completionListener, "Not a member of this group anymore.");
357        }
358    }
359
360    public void sendMessage(String message, boolean push, final Respoke.TaskCompletionListener completionListener) {
361        sendMessage(message, push, false, completionListener);
362    }
363
364
365    /**
366     *  Notify the group that a connection has joined. This is used internally to the SDK and should not be called directly by your client application.
367     *
368     *  @param connection The connection that has joined the group
369     */
370    public void connectionDidJoin(final RespokeConnection connection) {
371        members.add(connection);
372
373        new Handler(Looper.getMainLooper()).post(new Runnable() {
374            @Override
375            public void run() {
376                Listener listener = listenerReference.get();
377                if (null != listener) {
378                    listener.onJoin(connection, RespokeGroup.this);
379                }
380            }
381        });
382    }
383
384
385    /**
386     *  Notify the group that a connection has left. This is used internally to the SDK and should not be called directly by your client application.
387     *
388     *  @param connection The connection that has left the group
389     */
390    public void connectionDidLeave(final RespokeConnection connection) {
391        members.remove(connection);
392
393        new Handler(Looper.getMainLooper()).post(new Runnable() {
394            @Override
395            public void run() {
396                Listener listener = listenerReference.get();
397                if (null != listener) {
398                    listener.onLeave(connection, RespokeGroup.this);
399                }
400            }
401        });
402    }
403
404
405    /**
406     *  Notify the group that a group message was received. This is used internally to the SDK and should not be called directly by your client application.
407     *
408     *  @param message      The body of the message
409     *  @param endpoint     The endpoint that sent the message
410     *  @param timestamp    The message timestamp
411     */
412    public void didReceiveMessage(final String message, final RespokeEndpoint endpoint, final Date timestamp) {
413        new Handler(Looper.getMainLooper()).post(new Runnable() {
414            @Override
415            public void run() {
416                Listener listener = listenerReference.get();
417                if (null != listener) {
418                    listener.onGroupMessage(message, endpoint, RespokeGroup.this, timestamp);
419                }
420            }
421        });
422    }
423
424
425    //** Private methods
426
427
428    /**
429     *  A convenience method for posting errors to a GetGroupMembersCompletionListener
430     *
431     *  @param completionListener  The listener to notify
432     *  @param errorMessage        The human-readable error message that occurred
433     */
434    private void postGetGroupMembersError(final GetGroupMembersCompletionListener completionListener, final String errorMessage) {
435        new Handler(Looper.getMainLooper()).post(new Runnable() {
436            @Override
437            public void run() {
438                if (null != completionListener) {
439                    completionListener.onError(errorMessage);
440                }
441            }
442        });
443    }
444
445
446}