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}