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.opengl.GLSurfaceView; 015import android.os.Handler; 016import android.os.Looper; 017import android.util.Log; 018 019import org.json.JSONArray; 020import org.json.JSONException; 021import org.json.JSONObject; 022 023import java.lang.ref.WeakReference; 024import java.util.ArrayList; 025import java.util.Date; 026import java.util.Iterator; 027 028/** 029 * Represents remote Endpoints. Endpoints are users of this application that are not the one logged into this 030 * instance of the application. An Endpoint could be logged in from multiple other instances of this app, each of 031 * which is represented by a Connection. The client can interact with endpoints by calling them or 032 * sending them messages. An endpoint can be a person using an app from a browser or a script using the APIs on 033 * a server. 034 */ 035public class RespokeEndpoint { 036 037 private WeakReference<Listener> listenerReference; 038 private String endpointID; 039 public ArrayList<RespokeConnection> connections; 040 private RespokeSignalingChannel signalingChannel; 041 public Object presence; 042 private WeakReference<RespokeDirectConnection> directConnectionReference; 043 private WeakReference<RespokeClient> clientReference; 044 045 046 /** 047 * A listener interface to notify the receiver of events occurring with the endpoint 048 */ 049 public interface Listener { 050 051 /** 052 * Handle messages sent to the logged-in user from this one Endpoint. 053 * 054 * @param message The message 055 * @param timestamp The timestamp of the message 056 * @param endpoint The remote endpoint that sent the message 057 * @param didSend True if the specified endpoint sent the message, False if it received the message 058 */ 059 public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend); 060 061 062 /** 063 * A notification that the presence for an endpoint has changed 064 * 065 * @param presence The new presence 066 * @param sender The endpoint 067 */ 068 public void onPresence(Object presence, RespokeEndpoint sender); 069 070 } 071 072 073 /** 074 * The constructor for this class 075 * 076 * @param channel The signaling channel managing communications with this endpoint 077 * @param newEndpointID The ID for this endpoint 078 * @param client The client to which this endpoint instance belongs 079 */ 080 public RespokeEndpoint(RespokeSignalingChannel channel, String newEndpointID, RespokeClient client) { 081 endpointID = newEndpointID; 082 signalingChannel = channel; 083 connections = new ArrayList<RespokeConnection>(); 084 clientReference = new WeakReference<RespokeClient>(client); 085 } 086 087 088 /** 089 * Set a receiver for the Listener interface 090 * 091 * @param listener The new receiver for events from the Listener interface for this endpoint instance 092 */ 093 public void setListener(Listener listener) { 094 listenerReference = new WeakReference<Listener>(listener); 095 } 096 097 098 /** 099 * Send a message to the endpoint through the infrastructure. 100 * 101 * @param message The message to send 102 * @param push A flag indicating if a push notification should be sent for this message 103 * @param ccSelf A flag indicating if the message should be copied to other devices the client might be logged into 104 * @param completionListener A listener to receive a notification on the success of the asynchronous operation 105 */ 106 public void sendMessage(String message, boolean push, boolean ccSelf, final Respoke.TaskCompletionListener completionListener) { 107 if ((null != signalingChannel) && (signalingChannel.connected)) { 108 try { 109 JSONObject data = new JSONObject(); 110 data.put("to", endpointID); 111 data.put("message", message); 112 data.put("push", push); 113 data.put("ccSelf", ccSelf); 114 115 signalingChannel.sendRESTMessage("post", "/v1/messages", data, new RespokeSignalingChannel.RESTListener() { 116 @Override 117 public void onSuccess(Object response) { 118 Respoke.postTaskSuccess(completionListener); 119 } 120 121 @Override 122 public void onError(final String errorMessage) { 123 Respoke.postTaskError(completionListener, errorMessage); 124 } 125 }); 126 } catch (JSONException e) { 127 Respoke.postTaskError(completionListener, "Error encoding message"); 128 } 129 } else { 130 Respoke.postTaskError(completionListener, "Can't complete request when not connected. Please reconnect!"); 131 } 132 } 133 134 135 /** 136 * Create a new call with audio and optionally video. 137 * 138 * @param callListener A listener to receive notifications of call related events 139 * @param context An application context with which to access system resources 140 * @param glView A GLSurfaceView into which video from the call should be rendered, or null if the call is audio only 141 * @param audioOnly Specify true for an audio-only call 142 * 143 * @return A new RespokeCall instance 144 */ 145 public RespokeCall startCall(RespokeCall.Listener callListener, Context context, GLSurfaceView glView, boolean audioOnly) { 146 RespokeCall call = null; 147 148 if ((null != signalingChannel) && (signalingChannel.connected)) { 149 call = new RespokeCall(signalingChannel, this, false); 150 call.setListener(callListener); 151 152 call.startCall(context, glView, audioOnly); 153 } 154 155 return call; 156 } 157 158 159 /** 160 * Get the endpoint's ID 161 * 162 * @return The ID 163 */ 164 public String getEndpointID() { 165 return endpointID; 166 } 167 168 169 /** 170 * Returns a connection with the specified ID, and optionally creates one if it does not exist 171 * 172 * @param connectionID The ID of the connection 173 * @param skipCreate Whether or not to create a new connection if it is not found 174 * 175 * @return The connection that matches the specified ID, or null if not found and skipCreate is true 176 */ 177 public RespokeConnection getConnection(String connectionID, boolean skipCreate) { 178 RespokeConnection connection = null; 179 180 for (RespokeConnection eachConnection : connections) { 181 if (eachConnection.connectionID.equals(connectionID)) { 182 connection = eachConnection; 183 break; 184 } 185 } 186 187 if ((null == connection) && !skipCreate) { 188 connection = new RespokeConnection(connectionID, this); 189 connections.add(connection); 190 } 191 192 return connection; 193 } 194 195 196 /** 197 * Get an array of connections associated with this endpoint 198 * 199 * @return The array of connections 200 */ 201 public ArrayList<RespokeConnection> getConnections() { 202 return connections; 203 } 204 205 206 /** 207 * Process a received message. This is used internally to the SDK and should not be called directly by your client application. 208 * 209 * @param message The body of the message 210 * @param timestamp The message timestamp 211 */ 212 public void didReceiveMessage(final String message, final Date timestamp) { 213 new Handler(Looper.getMainLooper()).post(new Runnable() { 214 @Override 215 public void run() { 216 if (null != listenerReference) { 217 Listener listener = listenerReference.get(); 218 if (null != listener) { 219 listener.onMessage(message, timestamp, RespokeEndpoint.this, false); 220 } 221 } 222 } 223 }); 224 } 225 226 227 /** 228 * Process a sent message. This is used internally to the SDK and should not be called directly by your client application. 229 * 230 * @param message The body of the message 231 * @param timestamp The message timestamp 232 */ 233 public void didSendMessage(final String message, final Date timestamp) { 234 new Handler(Looper.getMainLooper()).post(new Runnable() { 235 @Override 236 public void run() { 237 if (null != listenerReference) { 238 Listener listener = listenerReference.get(); 239 if (null != listener) { 240 listener.onMessage(message, timestamp, RespokeEndpoint.this, true); 241 } 242 } 243 } 244 }); 245 } 246 247 248 /** 249 * Find the presence out of all known connections with the highest priority (most availability) 250 * and set it as the endpoint's resolved presence. 251 */ 252 public void resolvePresence() { 253 ArrayList<Object> list = new ArrayList<Object>(); 254 255 for (RespokeConnection eachConnection : connections) { 256 Object connectionPresence = eachConnection.presence; 257 258 if (null != connectionPresence) { 259 list.add(connectionPresence); 260 } 261 } 262 263 RespokeClient client = null; 264 RespokeClient.ResolvePresenceListener resolveListener = null; 265 if (null != clientReference) { 266 client = clientReference.get(); 267 268 if (client != null) { 269 resolveListener = client.getResolvePresenceListener(); 270 } 271 } 272 273 if (null != resolveListener) { 274 presence = resolveListener.resolvePresence(list); 275 } else { 276 ArrayList<String> options = new ArrayList<String>(); 277 options.add("chat"); 278 options.add("available"); 279 options.add("away"); 280 options.add("dnd"); 281 options.add("xa"); 282 options.add("unavailable"); 283 284 String newPresence = null; 285 for (String eachOption : options) { 286 for (Object eachObject : list) { 287 if (eachObject instanceof String) { 288 String eachObjectString = (String) eachObject; 289 290 if (eachObjectString.toLowerCase().equals(eachOption)) { 291 newPresence = eachOption; 292 } 293 } 294 } 295 296 if (null != newPresence) { 297 break; 298 } 299 } 300 301 if (null == newPresence) { 302 newPresence = "unavailable"; 303 } 304 305 presence = newPresence; 306 } 307 308 new Handler(Looper.getMainLooper()).post(new Runnable() { 309 @Override 310 public void run() { 311 if (null != listenerReference) { 312 Listener listener = listenerReference.get(); 313 if (null != listener) { 314 listener.onPresence(presence, RespokeEndpoint.this); 315 } 316 } 317 } 318 }); 319 } 320 321 322 /** 323 * Get the active direct connection with this endpoint (if any) 324 * 325 * @return The active direct connection instance, or null otherwise 326 */ 327 public RespokeDirectConnection directConnection() { 328 if (null != directConnectionReference) { 329 return directConnectionReference.get(); 330 } else { 331 return null; 332 } 333 } 334 335 336 /** 337 * Associate a direct connection object with this endpoint. This method is used internally by the SDK should not be called by your client application. 338 * 339 * @param newDirectConnection The direct connection to associate 340 */ 341 public void setDirectConnection(RespokeDirectConnection newDirectConnection) { 342 if (null != newDirectConnection) { 343 directConnectionReference = new WeakReference<RespokeDirectConnection>(newDirectConnection); 344 } else { 345 directConnectionReference = null; 346 } 347 } 348 349 350 /** 351 * Create a new DirectConnection. This method creates a new Call as well, attaching this DirectConnection to 352 * it for the purposes of creating a peer-to-peer link for sending data such as messages to the other endpoint. 353 * Information sent through a DirectConnection is not handled by the cloud infrastructure. 354 * 355 * @return The DirectConnection which can be used to send data and messages directly to the other endpoint. 356 */ 357 public RespokeDirectConnection startDirectConnection() { 358 // The constructor will call the setDirectConnection method on this endpoint instance with a reference to the new RespokeDirectConnection object 359 RespokeCall call = new RespokeCall(signalingChannel, this, true); 360 call.startCall(null, null, false); 361 362 return directConnection(); 363 } 364}