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.content.Context; 014import android.content.SharedPreferences; 015import android.net.Uri; 016import android.os.Handler; 017import android.os.Looper; 018import android.util.Log; 019 020import com.digium.respokesdk.RestAPI.APIDoOpen; 021import com.digium.respokesdk.RestAPI.APIGetToken; 022import com.digium.respokesdk.RestAPI.APITransaction; 023 024import org.json.JSONArray; 025import org.json.JSONException; 026import org.json.JSONObject; 027 028import java.lang.ref.WeakReference; 029import java.util.ArrayList; 030import java.util.Date; 031import java.util.HashMap; 032import java.util.Iterator; 033import java.util.List; 034import java.util.Map; 035 036import static android.R.attr.data; 037import static android.R.attr.id; 038 039/** 040 * This is a top-level interface to the API. It handles authenticating the app to the 041 * API server, receiving server-side app-specific information, keeping track of connection status and presence, 042 * accepting callbacks and listeners, and interacting with information the library keeps 043 * track of, like groups and endpoints. The client also keeps track of default settings for calls and direct 044 * connections as well as automatically reconnecting to the service when network activity is lost. 045 */ 046public class RespokeClient implements RespokeSignalingChannel.Listener { 047 048 private static final String TAG = "RespokeClient"; 049 private static final int RECONNECT_INTERVAL = 500; ///< The exponential step interval between automatic reconnect attempts, in milliseconds 050 051 public static final String PROPERTY_LAST_VALID_PUSH_TOKEN = "pushToken"; 052 public static final String PROPERTY_LAST_VALID_PUSH_TOKEN_ID = "pushTokenServiceID"; 053 054 private WeakReference<Listener> listenerReference; 055 private WeakReference<ResolvePresenceListener> resolveListenerReference; 056 private String localEndpointID; ///< The local endpoint ID 057 private String localConnectionID; ///< The local connection ID 058 private RespokeSignalingChannel signalingChannel; ///< The signaling channel to use 059 private ArrayList<RespokeCall> calls; ///< An array of the active calls 060 private HashMap<String, RespokeGroup> groups; ///< An array of the groups this client is a member of 061 private ArrayList<RespokeEndpoint> knownEndpoints; ///< An array of the known endpoints 062 private Object presence; ///< The current presence of this client 063 private String applicationID; ///< The application ID to use when connecting in development mode 064 private boolean reconnect; ///< Indicates if the client should automatically reconnect if the web socket disconnects 065 private int reconnectCount; ///< A count of how many times reconnection has been attempted 066 private boolean connectionInProgress; ///< Indicates if the client is in the middle of attempting to connect 067 private Context appContext; ///< The application context 068 private String pushServiceID; ///< The push service ID 069 private ArrayList<String> presenceRegistrationQueue; ///< An array of endpoints that need to be registered for presence updates 070 private HashMap<String, Boolean> presenceRegistered; ///< A Hash of all the endpoint IDs that have already been registered for presence updates 071 private boolean registrationTaskWaiting; ///< A flag to indicate that a task is scheduled to begin presence registration 072 073 public String baseURL = APITransaction.RESPOKE_BASE_URL; ///< The base url of the Respoke service to use 074 075 /** 076 * A listener interface to notify the receiver of events occurring with the client 077 */ 078 public interface Listener { 079 /** 080 * Receive a notification Respoke has successfully connected to the cloud. 081 * 082 * @param sender The RespokeClient that has connected 083 */ 084 void onConnect(RespokeClient sender); 085 086 087 /** 088 * Receive a notification Respoke has successfully disconnected from the cloud. 089 * 090 * @param sender The RespokeClient that has disconnected 091 * @param reconnecting Indicates if the Respoke SDK is attempting to automatically reconnect 092 */ 093 void onDisconnect(RespokeClient sender, boolean reconnecting); 094 095 /** 096 * Handle an error that resulted from a method call. 097 * 098 * @param sender The RespokeClient that is reporting the error 099 * @param errorMessage The error that has occurred 100 */ 101 void onError(RespokeClient sender, String errorMessage); 102 103 /** 104 * Receive a notification that the client is receiving a call from a remote party. 105 * 106 * @param sender The RespokeClient that is receiving the call 107 * @param call A reference to the incoming RespokeCall object 108 */ 109 void onCall(RespokeClient sender, RespokeCall call); 110 111 /** 112 * This event is fired when the logged-in endpoint is receiving a request to open a direct connection 113 * to another endpoint. If the user wishes to allow the direct connection, calling 'accept' on the 114 * direct connection will allow the connection to be set up. 115 * 116 * @param directConnection The incoming direct connection object 117 * @param endpoint The remote endpoint initiating the direct connection 118 */ 119 void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint); 120 121 /** 122 * Receive a notification that a message addressed to this group has been received 123 * 124 * @param message The message 125 * @param endpoint The remote endpoint the message is related to 126 * @param group If this was a group message, the group to which this group message was posted. 127 * @param timestamp The timestamp of the message 128 * @param didSend True if the specified endpoint sent the message, False if it received the message. Null for group messages. 129 */ 130 void onMessage(String message, RespokeEndpoint endpoint, RespokeGroup group, Date timestamp, Boolean didSend); 131 } 132 133 /** 134 * A listener interface to receive a notification that the task to join the groups has completed 135 */ 136 public interface JoinGroupCompletionListener { 137 138 /** 139 * Received notification that the groups were successfully joined 140 * 141 * @param groupList An array of RespokeGroup instances representing the groups that were successfully joined 142 */ 143 void onSuccess(ArrayList<RespokeGroup> groupList); 144 145 /** 146 * Receive a notification that the asynchronous operation failed 147 * 148 * @param errorMessage A human-readable description of the error that was encountered 149 */ 150 void onError(String errorMessage); 151 152 } 153 154 /** 155 * A listener interface to receive a notification that the connect action has failed 156 */ 157 public interface ConnectCompletionListener { 158 159 /** 160 * Receive a notification that the asynchronous operation failed 161 * 162 * @param errorMessage A human-readable description of the error that was encountered 163 */ 164 void onError(String errorMessage); 165 166 } 167 168 /** 169 * A listener interface to ask the receiver to resolve a list of presence values for an endpoint 170 */ 171 public interface ResolvePresenceListener { 172 173 /** 174 * Resolve the presence among multiple connections belonging to this endpoint. Note that this callback will NOT be called in the UI thread. 175 * 176 * @param presenceArray An array of presence values 177 * 178 * @return The resolved presence value to use 179 */ 180 Object resolvePresence(ArrayList<Object> presenceArray); 181 182 } 183 184 /** 185 * A listener interface to receive a notification when the request to retrieve the history 186 * of messages for a list of groups has completed 187 */ 188 public interface GroupHistoriesCompletionListener { 189 190 void onSuccess(Map<String, List<RespokeGroupMessage>> groupsToMessages); 191 192 void onError(String errorMessage); 193 } 194 195 /** 196 * A listener interface to receive a notification when the request to retrieve the 197 * history of messages for a specific group has completed 198 */ 199 public interface GroupHistoryCompletionListener { 200 201 void onSuccess(List<RespokeGroupMessage> messageList); 202 203 void onError(String errorMessage); 204 } 205 206 /** 207 * Encapsulates the info record for an Endpoint conversation. 208 */ 209 public static class EndpointConversationInfo { 210 public String groupId; 211 public Date timestamp; 212 public RespokeGroupMessage latestMessage; 213 public String sourceId; 214 public int unreadCount; 215 216 public EndpointConversationInfo() { 217 } 218 219 public EndpointConversationInfo( 220 String groupId, 221 Date timestamp, 222 RespokeGroupMessage message, 223 String sourceId, 224 int unreadCount) { 225 this.groupId = groupId; 226 this.timestamp = timestamp; 227 this.latestMessage = message; 228 this.sourceId = sourceId; 229 this.unreadCount = unreadCount; 230 } 231 } 232 233 /** 234 * A listener interface to receive a notification when the request to retrieve 235 * the list of conversations for an endpoint has completed. 236 */ 237 public interface EndpointConversationsCompletionListener { 238 239 void onSuccess(List<EndpointConversationInfo> conversations); 240 241 void onError(String errorMessage); 242 } 243 244 /** 245 * The constructor for this class 246 */ 247 public RespokeClient() { 248 calls = new ArrayList<RespokeCall>(); 249 groups = new HashMap<String, RespokeGroup>(); 250 knownEndpoints = new ArrayList<RespokeEndpoint>(); 251 presenceRegistrationQueue = new ArrayList<String>(); 252 presenceRegistered = new HashMap<String, Boolean>(); 253 } 254 255 /** 256 * Set a receiver for the Listener interface 257 * 258 * @param listener The new receiver for events from the Listener interface for this client instance 259 */ 260 public void setListener(Listener listener) { 261 listenerReference = new WeakReference<Listener>(listener); 262 } 263 264 /** 265 * Set a receiver for the ResolvePresenceListener interface 266 * 267 * @param listener The new receiver for events from the ResolvePresenceListener interface for this client instance 268 */ 269 public void setResolvePresenceListener(ResolvePresenceListener listener) { 270 resolveListenerReference = new WeakReference<ResolvePresenceListener>(listener); 271 } 272 273 /** 274 * Get the current receiver for the ResolvePresenceListener interface 275 * 276 * @return The current receiver for the ResolvePresenceListener interface 277 */ 278 public ResolvePresenceListener getResolvePresenceListener() { 279 if (null != resolveListenerReference) { 280 return resolveListenerReference.get(); 281 } else { 282 return null; 283 } 284 } 285 286 /** 287 * Connect to the Respoke infrastructure and authenticate in development mode using the specified endpoint ID and app ID. 288 * Attempt to obtain an authentication token automatically from the Respoke infrastructure. 289 * 290 * @param endpointID The endpoint ID to use when connecting 291 * @param appID Your Application ID 292 * @param shouldReconnect Whether or not to automatically reconnect to the Respoke service when a disconnect occurs. 293 * @param initialPresence The optional initial presence value to set for this client 294 * @param context An application context with which to access system resources 295 * @param completionListener A listener to be called when an error occurs, passing a string describing the error 296 */ 297 public void connect(String endpointID, String appID, boolean shouldReconnect, final Object initialPresence, Context context, final ConnectCompletionListener completionListener) { 298 if ((endpointID != null) && (appID != null) && (endpointID.length() > 0) && (appID.length() > 0)) { 299 connectionInProgress = true; 300 reconnect = shouldReconnect; 301 applicationID = appID; 302 appContext = context; 303 304 APIGetToken request = new APIGetToken(context, baseURL) { 305 @Override 306 public void transactionComplete() { 307 super.transactionComplete(); 308 309 if (success) { 310 connect(this.token, initialPresence, appContext, new ConnectCompletionListener() { 311 @Override 312 public void onError(final String errorMessage) { 313 connectionInProgress = false; 314 315 postConnectError(completionListener, errorMessage); 316 } 317 }); 318 } else { 319 connectionInProgress = false; 320 321 postConnectError(completionListener, this.errorMessage); 322 } 323 } 324 }; 325 326 request.appID = appID; 327 request.endpointID = endpointID; 328 request.go(); 329 } else { 330 postConnectError(completionListener, "AppID and endpointID must be specified"); 331 } 332 } 333 334 /** 335 * Connect to the Respoke infrastructure and authenticate with the specified brokered auth token ID. 336 * 337 * @param tokenID The token ID to use when connecting 338 * @param initialPresence The optional initial presence value to set for this client 339 * @param context An application context with which to access system resources 340 * @param completionListener A listener to be called when an error occurs, passing a string describing the error 341 */ 342 public void connect(String tokenID, final Object initialPresence, Context context, final ConnectCompletionListener completionListener) { 343 if ((tokenID != null) && (tokenID.length() > 0)) { 344 connectionInProgress = true; 345 appContext = context; 346 347 APIDoOpen request = new APIDoOpen(context, baseURL) { 348 @Override 349 public void transactionComplete() { 350 super.transactionComplete(); 351 352 if (success) { 353 // Remember the presence value to set once connected 354 presence = initialPresence; 355 356 signalingChannel = new RespokeSignalingChannel(appToken, RespokeClient.this, baseURL, appContext); 357 signalingChannel.authenticate(); 358 } else { 359 connectionInProgress = false; 360 361 postConnectError(completionListener, this.errorMessage); 362 } 363 } 364 }; 365 366 request.tokenID = tokenID; 367 request.go(); 368 } else { 369 postConnectError(completionListener, "TokenID must be specified"); 370 } 371 } 372 373 /** 374 * Disconnect from the Respoke infrastructure, leave all groups, invalidate the token, and disconnect the websocket. 375 */ 376 public void disconnect() { 377 reconnect = false; 378 379 if (null != signalingChannel) { 380 signalingChannel.disconnect(); 381 } 382 } 383 384 /** 385 * Check whether this client is connected to the backend infrastructure. 386 * 387 * @return True if connected 388 */ 389 public boolean isConnected() { 390 return ((signalingChannel != null) && (signalingChannel.connected)); 391 } 392 393 /** 394 * Join a list of Groups and begin keeping track of them. 395 * 396 * @param groupIDList An array of IDs of the groups to join 397 * @param completionListener A listener to receive a notification of the success or failure of the asynchronous operation 398 */ 399 public void joinGroups(final ArrayList<String> groupIDList, final JoinGroupCompletionListener completionListener) { 400 if (isConnected()) { 401 if ((groupIDList != null) && (groupIDList.size() > 0)) { 402 String urlEndpoint = "/v1/groups"; 403 404 JSONArray groupList = new JSONArray(groupIDList); 405 JSONObject data = new JSONObject(); 406 try { 407 data.put("groups", groupList); 408 409 signalingChannel.sendRESTMessage("post", urlEndpoint, data, new RespokeSignalingChannel.RESTListener() { 410 @Override 411 public void onSuccess(Object response) { 412 final ArrayList<RespokeGroup> newGroupList = new ArrayList<RespokeGroup>(); 413 for (String eachGroupID : groupIDList) { 414 RespokeGroup newGroup = new RespokeGroup(eachGroupID, signalingChannel, RespokeClient.this); 415 groups.put(eachGroupID, newGroup); 416 newGroupList.add(newGroup); 417 } 418 419 new Handler(Looper.getMainLooper()).post(new Runnable() { 420 @Override 421 public void run() { 422 if (null != completionListener) { 423 completionListener.onSuccess(newGroupList); 424 } 425 } 426 }); 427 } 428 429 @Override 430 public void onError(final String errorMessage) { 431 postJoinGroupMembersError(completionListener, errorMessage); 432 } 433 }); 434 } catch (JSONException e) { 435 postJoinGroupMembersError(completionListener, "Error encoding group list to json"); 436 } 437 } else { 438 postJoinGroupMembersError(completionListener, "At least one group must be specified"); 439 } 440 } else { 441 postJoinGroupMembersError(completionListener, "Can't complete request when not connected. Please reconnect!"); 442 } 443 } 444 445 /** 446 * Find a Connection by id and return it. In most cases, if we don't find it we will create it. This is useful 447 * in the case of dynamic endpoints where groups are not in use. Set skipCreate=true to return null 448 * if the Connection is not already known. 449 * 450 * @param connectionID The ID of the connection to return 451 * @param endpointID The ID of the endpoint to which this connection belongs 452 * @param skipCreate If true, return null if the connection is not already known 453 * 454 * @return The connection whose ID was specified 455 */ 456 public RespokeConnection getConnection(String connectionID, String endpointID, boolean skipCreate) { 457 RespokeConnection connection = null; 458 459 if (null != connectionID) { 460 RespokeEndpoint endpoint = getEndpoint(endpointID, skipCreate); 461 462 if (null != endpoint) { 463 for (RespokeConnection eachConnection : endpoint.connections) { 464 if (eachConnection.connectionID.equals(connectionID)) { 465 connection = eachConnection; 466 break; 467 } 468 } 469 470 if ((null == connection) && (!skipCreate)) { 471 connection = new RespokeConnection(connectionID, endpoint); 472 endpoint.connections.add(connection); 473 } 474 } 475 } 476 477 return connection; 478 } 479 480 /** 481 * Initiate a call to a conference. 482 * 483 * @param callListener A listener to receive notifications about the new call 484 * @param context An application context with which to access system resources 485 * @param conferenceID The ID of the conference to call 486 * 487 * @return A reference to the new RespokeCall object representing this call 488 */ 489 public RespokeCall joinConference(RespokeCall.Listener callListener, Context context, String conferenceID) { 490 RespokeCall call = null; 491 492 if ((null != signalingChannel) && (signalingChannel.connected)) { 493 call = new RespokeCall(signalingChannel, conferenceID, "conference"); 494 call.setListener(callListener); 495 496 call.startCall(context, null, true); 497 } 498 499 return call; 500 } 501 502 /** 503 * Find an endpoint by id and return it. In most cases, if we don't find it we will create it. This is useful 504 * in the case of dynamic endpoints where groups are not in use. Set skipCreate=true to return null 505 * if the Endpoint is not already known. 506 * 507 * @param endpointIDToFind The ID of the endpoint to return 508 * @param skipCreate If true, return null if the connection is not already known 509 * 510 * @return The endpoint whose ID was specified 511 */ 512 public RespokeEndpoint getEndpoint(String endpointIDToFind, boolean skipCreate) { 513 RespokeEndpoint endpoint = null; 514 515 if (null != endpointIDToFind) { 516 for (RespokeEndpoint eachEndpoint : knownEndpoints) { 517 if (eachEndpoint.getEndpointID().equals(endpointIDToFind)) { 518 endpoint = eachEndpoint; 519 break; 520 } 521 } 522 523 if ((null == endpoint) && (!skipCreate)) { 524 endpoint = new RespokeEndpoint(signalingChannel, endpointIDToFind, this); 525 knownEndpoints.add(endpoint); 526 } 527 528 if (null != endpoint) { 529 queuePresenceRegistration(endpoint.getEndpointID()); 530 } 531 } 532 533 return endpoint; 534 } 535 536 /** 537 * Returns the group with the specified ID 538 * 539 * @param groupIDToFind The ID of the group to find 540 * 541 * @return The group with specified ID, or null if it was not found 542 */ 543 public RespokeGroup getGroup(String groupIDToFind) { 544 RespokeGroup group = null; 545 546 if (null != groupIDToFind) { 547 group = groups.get(groupIDToFind); 548 } 549 550 return group; 551 } 552 553 /** 554 * Retrieve the history of messages that have been persisted for 1 or more groups. Only those 555 * messages that have been marked to be persisted when sent will show up in the history. Only 556 * the most recent message in each group will be retrieved - to pull more messages, use the 557 * other method signature that allows `maxMessages` to be specified. 558 * 559 * @param groupIds The groups to pull history for 560 * @param completionListener The callback called when this async operation has completed 561 */ 562 public void getGroupHistories(final List<String> groupIds, 563 final GroupHistoriesCompletionListener completionListener) { 564 getGroupHistories(groupIds, 1, completionListener); 565 } 566 567 /** 568 * Retrieve the history of messages that have been persisted for 1 or more groups. Only those 569 * messages that have been marked to be persisted when sent will show up in the history. 570 * 571 * Note: This operation returns history for a set of specified groups. If you simply want the 572 * set of conversations that the connected endpoint has stored on the server (without needing to 573 * specify the groups), use the getConversations() method. 574 * 575 * @param groupIds The groups to pull history for 576 * @param maxMessages The maximum number of messages per group to pull. Must be >= 1 577 * @param completionListener The callback called when this async operation has completed 578 */ 579 public void getGroupHistories(final List<String> groupIds, final Integer maxMessages, 580 final GroupHistoriesCompletionListener completionListener) { 581 if (!isConnected()) { 582 getGroupHistoriesError(completionListener, "Can't complete request when not connected, " + 583 "Please reconnect!"); 584 return; 585 } 586 587 if ((maxMessages == null) || (maxMessages < 1)) { 588 getGroupHistoriesError(completionListener, "maxMessages must be at least 1"); 589 return; 590 } 591 592 if ((groupIds == null) || (groupIds.size() == 0)) { 593 getGroupHistoriesError(completionListener, "At least 1 group must be specified"); 594 return; 595 } 596 597 JSONObject body = new JSONObject(); 598 try { 599 body.put("limit", maxMessages.toString()); 600 JSONArray groupIdParams = new JSONArray(groupIds); 601 body.put("groupIds", groupIdParams); 602 } catch(JSONException e) { 603 getGroupHistoriesError(completionListener, "Error forming JSON body to send."); 604 return; 605 } 606 607 // This has been modifed to use the newer group-history-search route over the 608 // deprecated group-histories route. 609 String urlEndpoint = "/v1/group-history-search"; 610 signalingChannel.sendRESTMessage("post", urlEndpoint, body, 611 new RespokeSignalingChannel.RESTListener() { 612 @Override 613 public void onSuccess(Object response) { 614 if (!(response instanceof JSONObject)) { 615 getGroupHistoriesError(completionListener, "Invalid response from server"); 616 return; 617 } 618 619 final JSONObject json = (JSONObject) response; 620 final HashMap<String, List<RespokeGroupMessage>> results = new HashMap<>(); 621 622 for (Iterator<String> keys = json.keys(); keys.hasNext();) { 623 final String key = keys.next(); 624 625 try { 626 final JSONArray jsonMessages = json.getJSONArray(key); 627 628 final ArrayList<RespokeGroupMessage> messageList = 629 new ArrayList<>(jsonMessages.length()); 630 631 for (int i = 0; i < jsonMessages.length(); i++) { 632 final JSONObject jsonMessage = jsonMessages.getJSONObject(i); 633 final RespokeGroupMessage message = buildGroupMessage(jsonMessage); 634 messageList.add(message); 635 } 636 637 results.put(key, messageList); 638 } catch (JSONException e) { 639 getGroupHistoriesError(completionListener, "Error parsing JSON response"); 640 return; 641 } 642 } 643 644 new Handler(Looper.getMainLooper()).post(new Runnable() { 645 @Override 646 public void run() { 647 if (completionListener != null) { 648 completionListener.onSuccess(results); 649 } 650 } 651 }); 652 } 653 654 @Override 655 public void onError(final String errorMessage) { 656 getGroupHistoriesError(completionListener, errorMessage); 657 } 658 }); 659 } 660 661 /** 662 * Retrieve a list of conversations (groupIds) that this endpoint has 663 * message history with. Only group messages that have been marked to be 664 * persisted will show up in history (and thus create a conversation). 665 * 666 * The success handler is passed a list of EndpointConversationInfo records. 667 * 668 * @param completionListener The callback called when this async operation has completed 669 */ 670 public void getConversations(final EndpointConversationsCompletionListener completionListener) { 671 if (!isConnected()) { 672 getEndpointConversationsError(completionListener, "Can't complete request when not connected, " + 673 "Please reconnect!"); 674 return; 675 } 676 677 String urlEndpoint = "/v1/endpoints/" + localEndpointID + "/conversations"; 678 signalingChannel.sendRESTMessage("get", urlEndpoint, null, 679 new RespokeSignalingChannel.RESTListener() { 680 @Override 681 public void onSuccess(Object response) { 682 if (!(response instanceof JSONArray)) { 683 getEndpointConversationsError(completionListener, "Invalid response from server"); 684 return; 685 } 686 687 final JSONArray json = (JSONArray) response; 688 final List<EndpointConversationInfo> results = new ArrayList<EndpointConversationInfo>(); 689 690 try { 691 for (int i = 0; i < json.length(); i++) { 692 final JSONObject jsonConversationInfo = json.getJSONObject(i); 693 694 final EndpointConversationInfo info = new EndpointConversationInfo(); 695 696 final JSONObject jsonMessage = jsonConversationInfo.getJSONObject("latestMsg"); 697 698 info.latestMessage = buildGroupMessage(jsonMessage); 699 info.groupId = jsonConversationInfo.getString("groupId"); 700 info.sourceId = jsonConversationInfo.getString("sourceId"); 701 info.unreadCount = jsonConversationInfo.getInt("unreadCount"); 702 info.timestamp = new Date(jsonConversationInfo.getLong("timestamp")); 703 704 results.add(info); 705 } 706 } catch (JSONException e) { 707 getEndpointConversationsError(completionListener, "Error parsing JSON response"); 708 return; 709 } 710 711 new Handler(Looper.getMainLooper()).post(new Runnable() { 712 @Override 713 public void run() { 714 if (completionListener != null) { 715 completionListener.onSuccess(results); 716 } 717 } 718 }); 719 } 720 721 @Override 722 public void onError(final String errorMessage) { 723 getEndpointConversationsError(completionListener, errorMessage); 724 } 725 }); 726 } 727 728 /** 729 * Mark messages in a conversation as having been read, up to the given timestamp 730 * 731 * @param updates An array of records, each of which indicates a groupId and timestamp of the 732 * the most recent message the client has "read". 733 * @param completionListener The callback called when this async operation has completed. 734 */ 735 public void setConversationsRead(final List<RespokeConversationReadStatus> updates, final Respoke.TaskCompletionListener completionListener) { 736 if (!isConnected()) { 737 Respoke.postTaskError(completionListener, "Can't complete request when not connected, " + 738 "Please reconnect!"); 739 return; 740 } 741 742 if ((updates == null) || (updates.size() == 0)) { 743 Respoke.postTaskError(completionListener, "At least 1 conversation must be specified"); 744 return; 745 } 746 747 JSONObject body = new JSONObject(); 748 JSONArray groupsJsonArray = new JSONArray(); 749 750 try { 751 // Add each status object to the array. 752 for (RespokeConversationReadStatus status : updates) { 753 JSONObject jsonStatus = new JSONObject(); 754 jsonStatus.put("groupId", status.groupId); 755 jsonStatus.put("timestamp", status.timestamp.toString()); 756 757 groupsJsonArray.put(jsonStatus); 758 } 759 760 // Set the array to the 'groups' property. 761 body.put("groups", groupsJsonArray); 762 } catch(JSONException e) { 763 Respoke.postTaskError(completionListener, "Error forming JSON body to send."); 764 return; 765 } 766 767 String urlEndpoint = "/v1/endpoints/" + localEndpointID + "/conversations"; 768 signalingChannel.sendRESTMessage("put", urlEndpoint, body, 769 new RespokeSignalingChannel.RESTListener() { 770 @Override 771 public void onSuccess(Object response) { 772 new Handler(Looper.getMainLooper()).post(new Runnable() { 773 @Override 774 public void run() { 775 if (completionListener != null) { 776 completionListener.onSuccess(); 777 } 778 } 779 }); 780 } 781 782 @Override 783 public void onError(final String errorMessage) { 784 Respoke.postTaskError(completionListener, errorMessage); 785 } 786 }); 787 } 788 789 /** 790 * Retrieve the history of messages that have been persisted for a specific group. Only those 791 * messages that have been marked to be persisted when sent will show up in the history. Only 792 * the 50 most recent messages in each group will be retrieved - to change the maximum number of 793 * messages pulled, use the other method signature that allows `maxMessages` to be specified. To 794 * retrieve messages further back in the history than right now, use the other method signature 795 * that allows `before` to be specified. 796 * 797 * @param groupId The groups to pull history for 798 * @param completionListener The callback called when this async operation has completed 799 */ 800 public void getGroupHistory(final String groupId, 801 final GroupHistoryCompletionListener completionListener) { 802 getGroupHistory(groupId, 50, null, completionListener); 803 } 804 805 /** 806 * Retrieve the history of messages that have been persisted for a specific group. Only those 807 * messages that have been marked to be persisted when sent will show up in the history. To 808 * retrieve messages further back in the history than right now, use the other method signature 809 * that allows `before` to be specified. 810 * 811 * @param groupId The groups to pull history for 812 * @param maxMessages The maximum number of messages per group to pull. Must be >= 1 813 * @param completionListener The callback called when this async operation has completed 814 */ 815 public void getGroupHistory(final String groupId, final Integer maxMessages, 816 final GroupHistoryCompletionListener completionListener) { 817 getGroupHistory(groupId, maxMessages, null, completionListener); 818 } 819 820 /** 821 * Retrieve the history of messages that have been persisted for a specific group. Only those 822 * messages that have been marked to be persisted when sent will show up in the history. 823 * 824 * @param groupId The groups to pull history for 825 * @param maxMessages The maximum number of messages per group to pull. Must be >= 1 826 * @param before Limit messages to those with a timestamp before this value 827 * @param completionListener The callback called when this async operation has completed 828 */ 829 public void getGroupHistory(final String groupId, final Integer maxMessages, final Date before, 830 final GroupHistoryCompletionListener completionListener) { 831 if (!isConnected()) { 832 getGroupHistoryError(completionListener, "Can't complete request when not connected, " + 833 "Please reconnect!"); 834 return; 835 } 836 837 if ((maxMessages == null) || (maxMessages < 1)) { 838 getGroupHistoryError(completionListener, "maxMessages must be at least 1"); 839 return; 840 } 841 842 if ((groupId == null) || groupId.length() == 0) { 843 getGroupHistoryError(completionListener, "groupId cannot be blank"); 844 return; 845 } 846 847 Uri.Builder builder = new Uri.Builder(); 848 builder.appendQueryParameter("limit", maxMessages.toString()); 849 850 if (before != null) { 851 builder.appendQueryParameter("before", Long.toString(before.getTime())); 852 } 853 854 String urlEndpoint = String.format("/v1/groups/%s/history%s", groupId, builder.build().toString()); 855 signalingChannel.sendRESTMessage("get", urlEndpoint, null, 856 new RespokeSignalingChannel.RESTListener() { 857 @Override 858 public void onSuccess(Object response) { 859 if (!(response instanceof JSONArray)) { 860 getGroupHistoryError(completionListener, "Invalid response from server"); 861 return; 862 } 863 864 final JSONArray json = (JSONArray) response; 865 final ArrayList<RespokeGroupMessage> results = new ArrayList<>(json.length()); 866 867 try { 868 for (int i = 0; i < json.length(); i++) { 869 final JSONObject jsonMessage = json.getJSONObject(i); 870 final RespokeGroupMessage message = buildGroupMessage(jsonMessage); 871 results.add(message); 872 } 873 } catch (JSONException e) { 874 getGroupHistoryError(completionListener, "Error parsing JSON response"); 875 return; 876 } 877 878 new Handler(Looper.getMainLooper()).post(new Runnable() { 879 @Override 880 public void run() { 881 if (completionListener != null) { 882 completionListener.onSuccess(results); 883 } 884 } 885 }); 886 } 887 888 @Override 889 public void onError(final String errorMessage) { 890 getGroupHistoryError(completionListener, errorMessage); 891 } 892 }); 893 } 894 895 /** 896 * Return the Endpoint ID of this client 897 * 898 * @return The Endpoint ID of this client 899 */ 900 public String getEndpointID() { 901 return localEndpointID; 902 } 903 904 /** 905 * Set the presence on the client session 906 * 907 * @param newPresence The new presence to use 908 * @param completionListener A listener to receive the notification on the success or failure of the asynchronous operation 909 */ 910 public void setPresence(Object newPresence, final Respoke.TaskCompletionListener completionListener) { 911 if (isConnected()) { 912 Object presenceToSet = newPresence; 913 914 if (null == presenceToSet) { 915 presenceToSet = "available"; 916 } 917 918 JSONObject typeData = new JSONObject(); 919 JSONObject data = new JSONObject(); 920 921 try { 922 typeData.put("type", presenceToSet); 923 data.put("presence", typeData); 924 925 final Object finalPresence = presenceToSet; 926 927 signalingChannel.sendRESTMessage("post", "/v1/presence", data, new RespokeSignalingChannel.RESTListener() { 928 @Override 929 public void onSuccess(Object response) { 930 presence = finalPresence; 931 932 Respoke.postTaskSuccess(completionListener); 933 } 934 935 @Override 936 public void onError(final String errorMessage) { 937 Respoke.postTaskError(completionListener, errorMessage); 938 } 939 }); 940 } catch (JSONException e) { 941 Respoke.postTaskError(completionListener, "Error encoding presence to json"); 942 } 943 } else { 944 Respoke.postTaskError(completionListener, "Can't complete request when not connected. Please reconnect!"); 945 } 946 } 947 948 /** 949 * Get the current presence of this client 950 * 951 * @return The current presence value 952 */ 953 public Object getPresence() { 954 return presence; 955 } 956 957 /** 958 * Register the client to receive push notifications when the socket is not active 959 * 960 * @param token The GCMS token to register 961 */ 962 public void registerPushServicesWithToken(final String token) { 963 String httpURI; 964 String httpMethod; 965 966 JSONObject data = new JSONObject(); 967 try { 968 data.put("token", token); 969 data.put("service", "google"); 970 971 SharedPreferences prefs = appContext.getSharedPreferences(appContext.getPackageName(), Context.MODE_PRIVATE); 972 973 if (null != prefs) { 974 String lastKnownPushToken = prefs.getString(PROPERTY_LAST_VALID_PUSH_TOKEN, "notAvailable"); 975 String lastKnownPushTokenID = prefs.getString(PROPERTY_LAST_VALID_PUSH_TOKEN_ID, "notAvailable"); 976 977 if ((null == lastKnownPushTokenID) || (lastKnownPushTokenID.equals("notAvailable"))) { 978 httpURI = String.format("/v1/connections/%s/push-token", localConnectionID); 979 httpMethod = "post"; 980 createOrUpdatePushServiceToken(token, httpURI, httpMethod, data, prefs); 981 } else if (!lastKnownPushToken.equals("notAvailable") && !lastKnownPushToken.equals(token)) { 982 httpURI = String.format("/v1/connections/%s/push-token/%s", localConnectionID, lastKnownPushTokenID); 983 httpMethod = "put"; 984 createOrUpdatePushServiceToken(token, httpURI, httpMethod, data, prefs); 985 } 986 } 987 } catch(JSONException e) { 988 Log.d("", "Invalid JSON format for token"); 989 } 990 } 991 992 993 /** 994 * Unregister this client from the push service so that no more notifications will be received for this endpoint ID 995 * 996 * @param completionListener A listener to receive the notification on the success or failure of the asynchronous operation 997 */ 998 public void unregisterFromPushServices(final Respoke.TaskCompletionListener completionListener) { 999 if (isConnected()) { 1000 SharedPreferences prefs = appContext.getSharedPreferences(appContext.getPackageName(), Context.MODE_PRIVATE); 1001 1002 if (null != prefs) { 1003 String lastKnownPushTokenID = prefs.getString(PROPERTY_LAST_VALID_PUSH_TOKEN_ID, "notAvailable"); 1004 1005 if ((null != lastKnownPushTokenID) && !lastKnownPushTokenID.equals("notAvailable")) { 1006 // A push token has previously been registered successfully 1007 String httpURI = String.format("/v1/connections/%s/push-token/%s", localConnectionID, lastKnownPushTokenID); 1008 signalingChannel.sendRESTMessage("delete", httpURI, null, new RespokeSignalingChannel.RESTListener() { 1009 @Override 1010 public void onSuccess(Object response) { 1011 // Remove the push token ID from shared memory so that push may be registered again in the future 1012 SharedPreferences prefs = appContext.getSharedPreferences(appContext.getPackageName(), Context.MODE_PRIVATE); 1013 SharedPreferences.Editor editor = prefs.edit(); 1014 editor.remove(PROPERTY_LAST_VALID_PUSH_TOKEN_ID); 1015 editor.apply(); 1016 1017 Respoke.postTaskSuccess(completionListener); 1018 } 1019 1020 @Override 1021 public void onError(String errorMessage) { 1022 Respoke.postTaskError(completionListener, "Error unregistering push service token: " + errorMessage); 1023 } 1024 }); 1025 } else { 1026 Respoke.postTaskSuccess(completionListener); 1027 } 1028 } else { 1029 Respoke.postTaskError(completionListener, "Unable to access shared preferences to look for push token"); 1030 } 1031 } else { 1032 Respoke.postTaskError(completionListener, "Can't complete request when not connected. Please reconnect!"); 1033 } 1034 } 1035 1036 //** Private methods 1037 1038 private void createOrUpdatePushServiceToken(final String token, String httpURI, String httpMethod, JSONObject data, final SharedPreferences prefs) { 1039 signalingChannel.sendRESTMessage(httpMethod, httpURI, data, new RespokeSignalingChannel.RESTListener() { 1040 @Override 1041 public void onSuccess(Object response) { 1042 if (response instanceof JSONObject) { 1043 try { 1044 JSONObject responseJSON = (JSONObject) response; 1045 pushServiceID = responseJSON.getString("id"); 1046 1047 SharedPreferences.Editor editor = prefs.edit(); 1048 editor.putString(PROPERTY_LAST_VALID_PUSH_TOKEN, token); 1049 editor.putString(PROPERTY_LAST_VALID_PUSH_TOKEN_ID, pushServiceID); 1050 editor.apply(); 1051 } catch (JSONException e) { 1052 Log.d(TAG, "Unexpected response from server while registering push service token"); 1053 } 1054 } else { 1055 Log.d(TAG, "Unexpected response from server while registering push service token"); 1056 } 1057 } 1058 1059 @Override 1060 public void onError(String errorMessage) { 1061 Log.d(TAG, "Error registering push service token: " + errorMessage); 1062 } 1063 }); 1064 } 1065 1066 /** 1067 * A convenience method for posting errors to a ConnectCompletionListener 1068 * 1069 * @param completionListener The listener to notify 1070 * @param errorMessage The human-readable error message that occurred 1071 */ 1072 private void postConnectError(final ConnectCompletionListener completionListener, final String errorMessage) { 1073 new Handler(Looper.getMainLooper()).post(new Runnable() { 1074 @Override 1075 public void run() { 1076 if (null != completionListener) { 1077 completionListener.onError(errorMessage); 1078 } 1079 } 1080 }); 1081 } 1082 1083 /** 1084 * A convenience method for posting errors to a JoinGroupCompletionListener 1085 * 1086 * @param completionListener The listener to notify 1087 * @param errorMessage The human-readable error message that occurred 1088 */ 1089 private void postJoinGroupMembersError(final JoinGroupCompletionListener completionListener, final String errorMessage) { 1090 new Handler(Looper.getMainLooper()).post(new Runnable() { 1091 @Override 1092 public void run() { 1093 if (null != completionListener) { 1094 completionListener.onError(errorMessage); 1095 } 1096 } 1097 }); 1098 } 1099 1100 private void getEndpointConversationsError(final EndpointConversationsCompletionListener completionListener, 1101 final String errorMessage) { 1102 new Handler(Looper.getMainLooper()).post(new Runnable() { 1103 @Override 1104 public void run() { 1105 if (completionListener != null) { 1106 completionListener.onError(errorMessage); 1107 } 1108 } 1109 }); 1110 } 1111 1112 private void getGroupHistoriesError(final GroupHistoriesCompletionListener completionListener, 1113 final String errorMessage) { 1114 new Handler(Looper.getMainLooper()).post(new Runnable() { 1115 @Override 1116 public void run() { 1117 if (completionListener != null) { 1118 completionListener.onError(errorMessage); 1119 } 1120 } 1121 }); 1122 } 1123 1124 private void getGroupHistoryError(final GroupHistoryCompletionListener completionListener, 1125 final String errorMessage) { 1126 new Handler(Looper.getMainLooper()).post(new Runnable() { 1127 @Override 1128 public void run() { 1129 if (completionListener != null) { 1130 completionListener.onError(errorMessage); 1131 } 1132 } 1133 }); 1134 } 1135 1136 /** 1137 * Attempt to reconnect the client after a small delay 1138 */ 1139 private void performReconnect() { 1140 if (null != applicationID) { 1141 reconnectCount++; 1142 1143 new java.util.Timer().schedule( 1144 new java.util.TimerTask() { 1145 @Override 1146 public void run() { 1147 actuallyReconnect(); 1148 } 1149 }, 1150 RECONNECT_INTERVAL * (reconnectCount - 1) 1151 ); 1152 } 1153 } 1154 1155 /** 1156 * Attempt to reconnect the client if it is not already trying in another thread 1157 */ 1158 private void actuallyReconnect() { 1159 if (((null == signalingChannel) || !signalingChannel.connected) && reconnect) { 1160 if (connectionInProgress) { 1161 // The client app must have initiated a connection manually during the timeout period. Try again later 1162 performReconnect(); 1163 } else { 1164 Log.d(TAG, "Trying to reconnect..."); 1165 connect(localEndpointID, applicationID, reconnect, presence, appContext, new ConnectCompletionListener() { 1166 @Override 1167 public void onError(final String errorMessage) { 1168 // A REST API call failed. Socket errors are handled in the onError callback 1169 new Handler(Looper.getMainLooper()).post(new Runnable() { 1170 @Override 1171 public void run() { 1172 Listener listener = listenerReference.get(); 1173 if (null != listener) { 1174 listener.onError(RespokeClient.this, errorMessage); 1175 } 1176 } 1177 }); 1178 1179 // Try again later 1180 performReconnect(); 1181 } 1182 }); 1183 } 1184 } 1185 } 1186 1187 /** 1188 * Register for presence updates for the specified endpoint ID. Registration will not occur immediately, 1189 * it will be queued and performed asynchronously. Queuing allows for large numbers of presence 1190 * registration requests to occur in batches, minimizing the number of network transactions (and overall 1191 * time required). 1192 * 1193 * @param endpointID The ID of the endpoint for which to register for presence updates 1194 */ 1195 private void queuePresenceRegistration(String endpointID) { 1196 if (null != endpointID) { 1197 Boolean shouldSpawnRegistrationTask = false; 1198 1199 synchronized (this) { 1200 Boolean alreadyRegistered = presenceRegistered.get(endpointID); 1201 if ((null == alreadyRegistered) || !alreadyRegistered) { 1202 presenceRegistrationQueue.add(endpointID); 1203 1204 // If a Runnable to register presence has not already been scheduled, note that one will be shortly 1205 if (!registrationTaskWaiting) { 1206 shouldSpawnRegistrationTask = true; 1207 registrationTaskWaiting = true; 1208 } 1209 } 1210 } 1211 1212 if (shouldSpawnRegistrationTask) { 1213 // Schedule a Runnable to register presence on the next context switch, which should allow multiple subsequent calls to queuePresenceRegistration to get batched into a single socket transaction for efficiency 1214 new Handler(Looper.getMainLooper()).post(new Runnable() { 1215 @Override 1216 public void run() { 1217 final HashMap<String, Boolean> endpointIDMap = new HashMap<String, Boolean>(); 1218 1219 synchronized (this) { 1220 // Build a list of the endpointIDs that have been scheduled for registration, and have not already been taken care of by a previous loop of this task 1221 while (presenceRegistrationQueue.size() > 0) { 1222 String nextEndpointID = presenceRegistrationQueue.remove(0); 1223 Boolean alreadyRegistered = presenceRegistered.get(nextEndpointID); 1224 if ((null == alreadyRegistered) || !alreadyRegistered) { 1225 endpointIDMap.put(nextEndpointID, true); 1226 } 1227 } 1228 1229 // Now that the batch of endpoint IDs to register has been determined, indicate to the client that any new registration calls should schedule a new Runnable 1230 registrationTaskWaiting = false; 1231 } 1232 1233 // Build an array from the map keySet to ensure there are no duplicates in the list 1234 final ArrayList<String> endpointIDsToRegister = new ArrayList<String>(endpointIDMap.keySet()); 1235 1236 if ((endpointIDsToRegister.size() > 0) && isConnected()) { 1237 signalingChannel.registerPresence(endpointIDsToRegister, new RespokeSignalingChannel.RegisterPresenceListener() { 1238 @Override 1239 public void onSuccess(JSONArray initialPresenceData) { 1240 // Indicate that registration was successful for each endpoint ID in the list 1241 synchronized (RespokeClient.this) { 1242 for (String eachID : endpointIDsToRegister) { 1243 presenceRegistered.put(eachID, true); 1244 } 1245 } 1246 1247 if (null != initialPresenceData) { 1248 for (int ii = 0; ii < initialPresenceData.length(); ii++) { 1249 try { 1250 JSONObject eachEndpointData = (JSONObject) initialPresenceData.get(ii); 1251 String dataEndpointID = eachEndpointData.getString("endpointId"); 1252 RespokeEndpoint endpoint = getEndpoint(dataEndpointID, true); 1253 1254 if (null != endpoint) { 1255 JSONObject connectionData = eachEndpointData.getJSONObject("connectionStates"); 1256 Iterator<?> keys = connectionData.keys(); 1257 1258 while (keys.hasNext()) { 1259 String eachConnectionID = (String) keys.next(); 1260 JSONObject presenceDict = connectionData.getJSONObject(eachConnectionID); 1261 Object newPresence = presenceDict.get("type"); 1262 RespokeConnection connection = endpoint.getConnection(eachConnectionID, false); 1263 1264 if ((null != connection) && (null != newPresence)) { 1265 connection.presence = newPresence; 1266 } 1267 } 1268 } 1269 } catch (JSONException e) { 1270 // Silently skip this problem 1271 } 1272 } 1273 } 1274 1275 for (String eachID : endpointIDsToRegister) { 1276 RespokeEndpoint endpoint = getEndpoint(eachID, true); 1277 1278 if (null != endpoint) { 1279 endpoint.resolvePresence(); 1280 } 1281 } 1282 } 1283 1284 @Override 1285 public void onError(final String errorMessage) { 1286 Log.d(TAG, "Error registering presence: " + errorMessage); 1287 } 1288 }); 1289 } 1290 } 1291 }); 1292 } 1293 } 1294 } 1295 1296 // RespokeSignalingChannelListener methods 1297 public void onConnect(RespokeSignalingChannel sender, String endpointID, String connectionID) { 1298 connectionInProgress = false; 1299 reconnectCount = 0; 1300 localEndpointID = endpointID; 1301 localConnectionID = connectionID; 1302 1303 Respoke.sharedInstance().clientConnected(this); 1304 1305 // Try to set the presence to the initial or last set state 1306 setPresence(presence, new Respoke.TaskCompletionListener() { 1307 @Override 1308 public void onSuccess() { 1309 // do nothing 1310 } 1311 1312 @Override 1313 public void onError(String errorMessage) { 1314 // do nothing 1315 } 1316 }); 1317 1318 new Handler(Looper.getMainLooper()).post(new Runnable() { 1319 @Override 1320 public void run() { 1321 Listener listener = listenerReference.get(); 1322 if (null != listener) { 1323 listener.onConnect(RespokeClient.this); 1324 } 1325 } 1326 }); 1327 } 1328 1329 public void onDisconnect(RespokeSignalingChannel sender) { 1330 // Can only reconnect in development mode, not brokered mode 1331 final boolean willReconnect = reconnect && (applicationID != null); 1332 1333 calls.clear(); 1334 groups.clear(); 1335 knownEndpoints.clear(); 1336 presenceRegistrationQueue.clear(); 1337 presenceRegistered.clear(); 1338 registrationTaskWaiting = false; 1339 1340 new Handler(Looper.getMainLooper()).post(new Runnable() { 1341 @Override 1342 public void run() { 1343 Listener listener = listenerReference.get(); 1344 if (null != listener) { 1345 listener.onDisconnect(RespokeClient.this, willReconnect); 1346 } 1347 } 1348 }); 1349 1350 signalingChannel = null; 1351 1352 if (willReconnect) { 1353 performReconnect(); 1354 } 1355 } 1356 1357 public void onIncomingCall(JSONObject sdp, String sessionID, String connectionID, String endpointID, String fromType, Date timestamp, RespokeSignalingChannel sender) { 1358 RespokeEndpoint endpoint = null; 1359 1360 if (fromType.equals("web")) { 1361 /* Only create endpoints for type web */ 1362 endpoint = getEndpoint(endpointID, false); 1363 1364 if (null == endpoint) { 1365 Log.d(TAG, "Error: Could not create Endpoint for incoming call"); 1366 return; 1367 } 1368 } 1369 1370 final RespokeCall call = new RespokeCall(signalingChannel, sdp, sessionID, connectionID, endpointID, fromType, endpoint, false, timestamp); 1371 1372 new Handler(Looper.getMainLooper()).post(new Runnable() { 1373 @Override 1374 public void run() { 1375 Listener listener = listenerReference.get(); 1376 if (null != listener) { 1377 listener.onCall(RespokeClient.this, call); 1378 } 1379 } 1380 }); 1381 } 1382 1383 public void onIncomingDirectConnection(JSONObject sdp, String sessionID, String connectionID, String endpointID, Date timestamp, RespokeSignalingChannel sender) { 1384 RespokeEndpoint endpoint = getEndpoint(endpointID, false); 1385 1386 if (null != endpoint) { 1387 final RespokeCall call = new RespokeCall(signalingChannel, sdp, sessionID, connectionID, endpointID, "web", endpoint, true, timestamp); 1388 } else { 1389 Log.d(TAG, "Error: Could not create Endpoint for incoming direct connection"); 1390 } 1391 } 1392 1393 public void onError(final String errorMessage, RespokeSignalingChannel sender) { 1394 new Handler(Looper.getMainLooper()).post(new Runnable() { 1395 @Override 1396 public void run() { 1397 Listener listener = listenerReference.get(); 1398 if (null != listener) { 1399 listener.onError(RespokeClient.this, errorMessage); 1400 } 1401 } 1402 }); 1403 1404 if ((null != signalingChannel) && (!signalingChannel.connected)) { 1405 connectionInProgress = false; 1406 1407 if (reconnect) { 1408 performReconnect(); 1409 } 1410 } 1411 } 1412 1413 public void onJoinGroup(String groupID, String endpointID, String connectionID, RespokeSignalingChannel sender) { 1414 // only pass on notifications about people other than ourselves 1415 if ((null != endpointID) && (!endpointID.equals(localEndpointID))) { 1416 RespokeGroup group = groups.get(groupID); 1417 1418 if (null != group) { 1419 // Get the existing instance for this connection, or create a new one if necessary 1420 RespokeConnection connection = getConnection(connectionID, endpointID, false); 1421 1422 if (null != connection) { 1423 group.connectionDidJoin(connection); 1424 } 1425 } 1426 } 1427 } 1428 1429 public void onLeaveGroup(String groupID, String endpointID, String connectionID, RespokeSignalingChannel sender) { 1430 // only pass on notifications about people other than ourselves 1431 if ((null != endpointID) && (!endpointID.equals(localEndpointID))) { 1432 RespokeGroup group = groups.get(groupID); 1433 1434 if (null != group) { 1435 // Get the existing instance for this connection. If we are not already aware of it, ignore it 1436 RespokeConnection connection = getConnection(connectionID, endpointID, true); 1437 1438 if (null != connection) { 1439 group.connectionDidLeave(connection); 1440 } 1441 } 1442 } 1443 } 1444 1445 private void didReceiveMessage(final RespokeEndpoint endpoint, final String message, final Date timestamp) { 1446 new Handler(Looper.getMainLooper()).post(new Runnable() { 1447 @Override 1448 public void run() { 1449 if (null != listenerReference) { 1450 Listener listener = listenerReference.get(); 1451 if (null != listener) { 1452 listener.onMessage(message, endpoint, null, timestamp, false); 1453 } 1454 } 1455 } 1456 }); 1457 } 1458 1459 private void didSendMessage(final RespokeEndpoint endpoint, final String message, final Date timestamp) { 1460 new Handler(Looper.getMainLooper()).post(new Runnable() { 1461 @Override 1462 public void run() { 1463 if (null != listenerReference) { 1464 Listener listener = listenerReference.get(); 1465 if (null != listener) { 1466 listener.onMessage(message, endpoint, null, timestamp, true); 1467 } 1468 } 1469 } 1470 }); 1471 } 1472 1473 public void onMessage(final String message, final Date timestamp, String fromEndpointID, String toEndpointID, RespokeSignalingChannel sender) { 1474 1475 if (localEndpointID.equals(fromEndpointID) && (null != toEndpointID)) { 1476 // The local endpoint sent this message to the remote endpoint from another device (ccSelf) 1477 final RespokeEndpoint endpoint = getEndpoint(toEndpointID, false); 1478 if (null != endpoint) { 1479 endpoint.didReceiveMessage(message, timestamp); 1480 1481 // Notify the client listener of the message 1482 didReceiveMessage(endpoint, message, timestamp); 1483 } 1484 return; 1485 } 1486 1487 // The local endpoint received this message from a remote endpoint 1488 final RespokeEndpoint endpoint = getEndpoint(fromEndpointID, false); 1489 if (null != endpoint) { 1490 endpoint.didSendMessage(message, timestamp); 1491 1492 // Notify the client listener of the message 1493 didSendMessage(endpoint, message, timestamp); 1494 } 1495 } 1496 1497 public void onGroupMessage(final String message, String groupID, String endpointID, RespokeSignalingChannel sender, final Date timestamp) { 1498 final RespokeGroup group = groups.get(groupID); 1499 1500 if (null != group) { 1501 final RespokeEndpoint endpoint = getEndpoint(endpointID, false); 1502 1503 // Notify the group of the new message 1504 group.didReceiveMessage(message, endpoint, timestamp); 1505 1506 // Notify the client listener of the group message 1507 new Handler(Looper.getMainLooper()).post(new Runnable() { 1508 @Override 1509 public void run() { 1510 if (null != listenerReference) { 1511 Listener listener = listenerReference.get(); 1512 if (null != listener) { 1513 listener.onMessage(message, endpoint, group, timestamp, null); 1514 } 1515 } 1516 } 1517 }); 1518 } 1519 } 1520 1521 public void onPresence(Object presence, String connectionID, String endpointID, RespokeSignalingChannel sender) { 1522 RespokeConnection connection = getConnection(connectionID, endpointID, false); 1523 1524 if (null != connection) { 1525 connection.presence = presence; 1526 1527 RespokeEndpoint endpoint = connection.getEndpoint(); 1528 endpoint.resolvePresence(); 1529 } 1530 } 1531 1532 public void callCreated(RespokeCall call) { 1533 calls.add(call); 1534 } 1535 1536 public void callTerminated(RespokeCall call) { 1537 calls.remove(call); 1538 } 1539 1540 public RespokeCall callWithID(String sessionID) { 1541 RespokeCall call = null; 1542 1543 for (RespokeCall eachCall : calls) { 1544 if (eachCall.getSessionID().equals(sessionID)) { 1545 call = eachCall; 1546 break; 1547 } 1548 } 1549 1550 return call; 1551 } 1552 1553 public void directConnectionAvailable(final RespokeDirectConnection directConnection, final RespokeEndpoint endpoint) { 1554 new Handler(Looper.getMainLooper()).post(new Runnable() { 1555 @Override 1556 public void run() { 1557 if (null != listenerReference) { 1558 Listener listener = listenerReference.get(); 1559 if (null != listener) { 1560 listener.onIncomingDirectConnection(directConnection, endpoint); 1561 } 1562 } 1563 } 1564 }); 1565 } 1566 1567 /** 1568 * Build a group message from a JSON object. The format of the JSON object would be the 1569 * format that comes over the wire from Respoke when receiving a pubsub message. This same 1570 * format is used when retrieving message history. 1571 * 1572 * @param source The source JSON object to build the RespokeGroupMessage from 1573 * @return The built RespokeGroupMessage 1574 * @throws JSONException 1575 */ 1576 private RespokeGroupMessage buildGroupMessage(JSONObject source) throws JSONException { 1577 if (source == null) { 1578 throw new IllegalArgumentException("source cannot be null"); 1579 } 1580 1581 final JSONObject header = source.getJSONObject("header"); 1582 final String endpointID = header.getString("from"); 1583 final RespokeEndpoint endpoint = getEndpoint(endpointID, false); 1584 final String groupID = header.getString("channel"); 1585 RespokeGroup group = getGroup(groupID); 1586 1587 if (group == null) { 1588 group = new RespokeGroup(groupID, signalingChannel, this, false); 1589 groups.put(groupID, group); 1590 } 1591 1592 final String message = source.getString("message"); 1593 1594 final Date timestamp; 1595 1596 if (!header.isNull("timestamp")) { 1597 timestamp = new Date(header.getLong("timestamp")); 1598 } else { 1599 // Just use the current time if no date is specified in the header data 1600 timestamp = new Date(); 1601 } 1602 1603 return new RespokeGroupMessage(message, group, endpoint, timestamp); 1604 } 1605}