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}