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.os.Handler;
015import android.os.Looper;
016
017import org.json.JSONException;
018import org.json.JSONObject;
019import org.webrtc.DataChannel;
020import org.webrtc.PeerConnection;
021
022import java.io.UnsupportedEncodingException;
023import java.lang.ref.WeakReference;
024import java.nio.ByteBuffer;
025import java.nio.CharBuffer;
026import java.nio.charset.CharacterCodingException;
027import java.nio.charset.Charset;
028import java.nio.charset.CharsetDecoder;
029
030/**
031 * A direct connection via RTCDataChannel, including state and path negotation.
032 */
033public class RespokeDirectConnection implements org.webrtc.DataChannel.Observer {
034
035    private WeakReference<Listener> listenerReference;
036    private WeakReference<RespokeCall> callReference;
037    private DataChannel dataChannel;
038
039
040    /**
041     *  A listener interface to notify the receiver of events occurring with the direct connection
042     */
043    public interface Listener {
044
045        /**
046         *  The direct connection setup has begun. This does NOT mean it's ready to send messages yet. Listen to
047         *  onOpen for that notification.
048         *
049         *  @param sender  The direct connection for which the event occurred
050         */
051        public void onStart(RespokeDirectConnection sender);
052
053        /**
054         *  Called when the direct connection is opened.
055         *
056         *  @param sender  The direct connection for which the event occurred
057         */
058        public void onOpen(RespokeDirectConnection sender);
059
060        /**
061         *  Called when the direct connection is closed.
062         *
063         *  @param sender  The direct connection for which the event occurred
064         */
065        public void onClose(RespokeDirectConnection sender);
066
067        /**
068         *  Called when a message is received over the direct connection.
069         *  @param message The message received.
070         *  @param sender  The direct connection for which the event occurred
071         */
072        public void onMessage(String message, RespokeDirectConnection sender);
073
074    }
075
076
077    /**
078     *  The constructor for this class
079     *
080     *  @param call  The call instance with which this direct connection is associated
081     */
082    public RespokeDirectConnection(RespokeCall call) {
083        callReference = new WeakReference<RespokeCall>(call);
084    }
085
086
087    /**
088     *  Set a receiver for the Listener interface
089     *
090     *  @param listener  The new receiver for events from the Listener interface for this instance
091     */
092    public void setListener(Listener listener) {
093        if (null != listener) {
094            listenerReference = new WeakReference<Listener>(listener);
095        } else {
096            listenerReference = null;
097        }
098    }
099
100
101    /**
102     *  Accept the direct connection and start the process of obtaining media. 
103     *
104     *  @param context       An application context with which to access system resources
105     */
106    public void accept(Context context) {
107        if (null != callReference) {
108            RespokeCall call = callReference.get();
109            if (null != call) {
110                call.directConnectionDidAccept(context);
111            }
112        }
113    }
114
115
116    /**
117     *  Indicate whether a datachannel is being setup or is in progress.
118     *
119     *  @return  True the direct connection is active, false otherwise
120     */
121    public boolean isActive() {
122        return ((null != dataChannel) && (dataChannel.state() == DataChannel.State.OPEN));
123    }
124
125
126    /**
127     *  Get the call object associated with this direct connection
128     *
129     *  @return  The call instance
130     */
131    public RespokeCall getCall() {
132        if (null != callReference) {
133            return callReference.get();
134        } else {
135            return null;
136        }
137    }
138
139
140    /**
141     *  Send a message to the remote client through the direct connection.
142     *
143     *  @param message             The message to send
144     *  @param completionListener  A listener to receive a notification on the success of the asynchronous operation
145     */
146    public void sendMessage(String message, final Respoke.TaskCompletionListener completionListener) {
147        if (isActive()) {
148            JSONObject jsonMessage = new JSONObject();
149            try {
150                jsonMessage.put("message", message);
151                byte[] rawMessage = jsonMessage.toString().getBytes(Charset.forName("UTF-8"));
152                ByteBuffer directData = ByteBuffer.allocateDirect(rawMessage.length);
153                directData.put(rawMessage);
154                directData.flip();
155                DataChannel.Buffer data = new DataChannel.Buffer(directData, false);
156
157                if (dataChannel.send(data)) {
158                    Respoke.postTaskSuccess(completionListener);
159                } else {
160                    Respoke.postTaskError(completionListener, "Error sending message");
161                }
162            } catch (JSONException e) {
163                Respoke.postTaskError(completionListener, "Unable to encode message to JSON");
164            }
165        } else {
166            Respoke.postTaskError(completionListener, "DataChannel not in an open state");
167        }
168    }
169
170
171    /**
172     *  Establish a new direct connection instance with the peer connection for the call. This is used internally to the SDK and should not be called directly by your client application.
173     */
174    public void createDataChannel() {
175        if (null != callReference) {
176            RespokeCall call = callReference.get();
177            if (null != call) {
178                PeerConnection peerConnection = call.getPeerConnection();
179                dataChannel = peerConnection.createDataChannel("respokeDataChannel", new DataChannel.Init());
180                dataChannel.registerObserver(this);
181            }
182        }
183    }
184
185
186    /**
187     *  Notify the direct connection instance that the peer connection has opened the specified data channel
188     *
189     *  @param newDataChannel    The DataChannel that has opened
190     */
191    public void peerConnectionDidOpenDataChannel(DataChannel newDataChannel) {
192        if (null != dataChannel) {
193            // Replacing the previous connection, so disable observer messages from the old instance
194            dataChannel.unregisterObserver();
195        } else {
196            new Handler(Looper.getMainLooper()).post(new Runnable() {
197                public void run() {
198                    if (null != listenerReference) {
199                        Listener listener = listenerReference.get();
200                        if (null != listener) {
201                            listener.onStart(RespokeDirectConnection.this);
202                        }
203                    }
204                }
205            });
206        }
207
208        dataChannel = newDataChannel;
209        newDataChannel.registerObserver(this);
210    }
211
212
213    // org.webrtc.DataChannel.Observer methods
214
215
216    public void onStateChange() {
217        switch (dataChannel.state()) {
218            case CONNECTING:
219                break;
220
221            case OPEN: {
222                    if (null != callReference) {
223                        RespokeCall call = callReference.get();
224                        if (null != call) {
225                            call.directConnectionDidOpen(this);
226                        }
227                    }
228
229                new Handler(Looper.getMainLooper()).post(new Runnable() {
230                    public void run() {
231                        if (null != listenerReference) {
232                            Listener listener = listenerReference.get();
233                            if (null != listener) {
234                                listener.onOpen(RespokeDirectConnection.this);
235                            }
236                        }
237                    }
238                });
239                }
240                break;
241
242            case CLOSING:
243                break;
244
245            case CLOSED: {
246                    if (null != callReference) {
247                        RespokeCall call = callReference.get();
248                        if (null != call) {
249                            call.directConnectionDidClose(this);
250                        }
251                    }
252
253                    new Handler(Looper.getMainLooper()).post(new Runnable() {
254                        public void run() {
255                            if (null != listenerReference) {
256                                Listener listener = listenerReference.get();
257                                if (null != listener) {
258                                    listener.onClose(RespokeDirectConnection.this);
259                                }
260                            }
261                        }
262                    });
263                }
264                break;
265        }
266    }
267
268
269    public void onMessage(org.webrtc.DataChannel.Buffer buffer) {
270        if (buffer.binary) {
271            // TODO
272        } else {
273            Charset charset = Charset.forName("UTF-8");
274            CharsetDecoder decoder = charset.newDecoder();
275            try {
276                String message = decoder.decode( buffer.data ).toString();
277
278                try {
279                    JSONObject jsonMessage = new JSONObject(message);
280                    final String messageText = jsonMessage.getString("message");
281
282                    if (null != messageText) {
283                        new Handler(Looper.getMainLooper()).post(new Runnable() {
284                            public void run() {
285                                if (null != listenerReference) {
286                                    Listener listener = listenerReference.get();
287                                    if (null != listener) {
288                                        listener.onMessage(messageText, RespokeDirectConnection.this);
289                                    }
290                                }
291                            }
292                        });
293                    }
294                } catch (JSONException e) {
295                    // If it is not valid json, ignore the message
296                }
297            } catch (CharacterCodingException e) {
298                // If the message can not be decoded, ignore it
299            }
300        }
301    }
302
303
304}