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}