package com.jphonelite;
/*
import android.app.*;
import android.os.*;
import android.view.*;
import android.widget.*;
import android.util.*;
import android.net.*;
import android.database.*;
import android.provider.*;
import android.provider.Contacts.*;
*/
import android.content.*;
import java.util.*;
import javaforce.*;
import javaforce.voip.*;
/**
* jPhoneLite for Android : Engine
*
* author : Peter Quiring (pquiring at gmail dot com)
*
* website : jphonelite.sourceforge.net
*/
public class Engine implements SIPClientInterface, RTPInterface {
private static Engine instance = null;
private java.util.Timer timerKeepAlive, timerRegisterExpires, timerRegisterRetries;
private int registerRetries;
private int localport = 5061;
private String lastDial;
private Main main;
private Context ctx;
private String lastDialed = "";
public int line = -1;
public PhoneLine lines[];
public Audio sound = new Audio();
public static boolean active = false;
private Engine() {}
public synchronized static Engine getInstance(Main main, Context ctx) {
if (instance == null) {
//JFLog.log("Engine.getInstance():create new");
instance = new Engine();
instance.main = main;
instance.ctx = ctx;
instance.init();
return instance;
}
//JFLog.log("Engine.getInstance():return old");
instance.main = main;
instance.ctx = ctx;
if (!active) instance.reinit();
for(int a=0;a<6;a++) instance.lines[a].clr = -1; //force color update
return instance;
}
public void release() {
main = null;
instance = null;
}
private void init() {
active = true;
JFLogAndroid.init(0, "JAVAFORCE", "/sdcard/.jphone.log");
// JFLog.log("Engine.init()");
lines = new PhoneLine[6];
for(int a=0;a<6;a++) lines[a] = new PhoneLine();
Settings.loadSettings();
sound.init(lines, ctx);
reRegisterAll();
keepAliveinit();
}
public void uninit() {
active = false;
unRegisterAll();
sound.uninit();
PhoneLine newLines[] = new PhoneLine[6];
for(int a=0;a<6;a++) newLines[a] = new PhoneLine();
lines = newLines;
}
public void reinit() {
active = true;
reRegisterAll();
sound.init(lines, ctx);
}
public void do_xfr() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (!pl.incall) return;
if (pl.xfr) {
if (pl.dial.length() == 0) {
//cancel xfer
pl.status = "Connected";
pl.xfr = false;
} else {
pl.sip.refer(pl.callid, pl.dial);
pl.status = "XFER to " + pl.dial;
pl.dial = "";
pl.xfr = false;
endLine(line);
}
} else {
pl.dial = "";
pl.status = "XFER : Enter dest and press XFER again";
pl.xfr = true;
}
}
public void do_hld() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (!pl.incall) return;
pl.hld = !pl.hld;
pl.sip.setHold(pl.callid, pl.hld);
pl.sip.reinvite(pl.callid);
}
public void do_dnd() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (pl.incall) return;
if (pl.dnd)
pl.dial = Settings.current.dndCodeOn;
else
pl.dial = Settings.current.dndCodeOff;
pl.dnd = !pl.dnd;
}
public void do_cnf() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (!pl.incall) return;
pl.cnf = !pl.cnf;
}
public void do_call() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (pl.incall) end(); else call();
}
public void addDigit(char digit) {
if (line == -1) return;
PhoneLine pl = lines[line];
if (pl.sip == null) return;
if (!pl.sip.isRegistered()) return;
if (pl.incoming) return;
if (digit == 'x') {
if ((pl.incall)&&(!pl.xfr)) return;
//delete digit
int len = pl.dial.length();
if (len > 0) pl.dial = pl.dial.substring(0, len-1);
} else {
if ((pl.incall)&&(!pl.xfr)) {
if (pl.dtmf == 'x') {
// JFLog.log("DTMF:" + digit);
pl.dtmfcnt = 7; //7 * 20ms = 140ms total
pl.dtmf = digit;
}
return;
}
pl.dial += digit;
}
}
public void selectLine(int newline) {
//make sure line is valid
if ((line != -1) && (lines[line].dtmf != 'x')) lines[line].dtmfend = true; //finish dtmf on current line
line = newline;
if (line == -1) return;
sound.selectLine(line);
if (main != null) main._updateScreen();
}
/** Registers all SIP connections. */
public void reRegisterAll() {
int idx;
String host;
int port;
for(int a=0;a<6;a++) {
lines[a].dial = "";
lines[a].status = "";
if ((a > 0) && (Settings.current.lines[a].same != -1)) continue;
if (Settings.current.lines[a].host.length() == 0) continue;
if (Settings.current.lines[a].user.length() == 0) continue;
if (Settings.current.lines[a].pass.length() == 0) continue;
lines[a].sip = new SIPClient();
idx = Settings.current.lines[a].host.indexOf(':');
if (idx == -1) {
host = Settings.current.lines[a].host;
port = 5060;
} else {
host = Settings.current.lines[a].host.substring(0,idx);
port = JF.atoi(Settings.current.lines[a].host.substring(idx+1));
}
// JFLog.log("registering:" + host + ":" + port);
try {
int b=0;
while (!lines[a].sip.init(host, port, localport++, this, SIP.Transport.UDP)) {b++; if (b==10) throw new Exception("err");}
lines[a].sip.register(Settings.current.lines[a].name, Settings.current.lines[a].user, Settings.current.lines[a].auth, Settings.getPassword(Settings.current.lines[a].pass));
} catch (Exception e) {
lines[a].status = "SIP init failed (Exception)";
lines[a].sip = null;
if (a == line) if (main != null) main._updateScreen();
}
}
for(int a=1;a<6;a++) {
if (Settings.current.lines[a].same != -1) {
if (Settings.current.lines[Settings.current.lines[a].same].same != -1) {
lines[a].status = "Line#" + (a+1) + " (Invalid)";
} else {
lines[a].sip = lines[Settings.current.lines[a].same].sip;
}
}
}
//setup reRegister timer (expires)
timerRegisterExpires = new java.util.Timer();
timerRegisterExpires.scheduleAtFixedRate(new ReRegisterExpires(), 3595*1000, 3595*1000); //do it 5 seconds early just to be sure
registerRetries = 0;
timerRegisterRetries = new java.util.Timer();
timerRegisterRetries.schedule(new ReRegisterRetries(), 1000);
if (main != null) main._updateScreen();
}
/** Expires registration with all SIP connections. */
public void unRegisterAll() {
if (timerRegisterExpires != null) {
timerRegisterExpires.cancel();
timerRegisterExpires = null;
}
for(int a=0;a<6;a++) {
if (lines[a].incall) {
selectLine(a);
end();
}
lines[a].dial = "";
lines[a].status = "";
lines[a].unauth = false;
if ((a > 0) && (Settings.current.lines[a].same != -1)) {
lines[a].sip = null;
continue;
}
if (lines[a].sip == null) continue;
if (lines[a].sip.isRegistered()) {
try {
lines[a].sip.unregister();
} catch (Exception e) {
}
}
}
int maxwait;
for(int a=0;a<6;a++) {
if (lines[a].sip == null) continue;
maxwait = 1000;
while (lines[a].sip.isRegistered()) { JF.sleep(10); maxwait -= 10; if (maxwait == 0) break; }
lines[a].sip.uninit();
lines[a].sip = null;
}
}
/** Creates a timer to send keep-alives on all SIP connections. Keep alive are done every 30 seconds (many routers have a 60 second timeout). */
public void keepAliveinit() {
timerKeepAlive = new java.util.Timer();
timerKeepAlive.scheduleAtFixedRate(new KeepAlive(),0,30*1000);
}
/** TimerTask to perform keep-alives. on all SIP connections. */
public class KeepAlive extends java.util.TimerTask {
public void run() {
//JFLog.log("KeepAlive start:" + System.currentTimeMillis());
for(int a=0;a<6;a++) {
if (Settings.current.lines[a].same != -1) continue;
if (lines[a].sip == null) continue;
if (!lines[a].sip.isRegistered()) continue;
lines[a].sip.keepalive();
}
//JFLog.log("KeepAlive stop:" + System.currentTimeMillis());
}
}
/** TimerTask that reregisters all SIP connection after they expire (every 3600 seconds). */
public class ReRegisterExpires extends java.util.TimerTask {
public void run() {
//JFLog.log("Expires start:" + System.currentTimeMillis());
for(int a=0;a<6;a++) {
if (Settings.current.lines[a].same != -1) continue;
if (lines[a].sip == null) continue;
lines[a].sip.reregister();
}
registerRetries = 0;
if (timerRegisterRetries != null) {
timerRegisterRetries = new java.util.Timer();
timerRegisterRetries.schedule(new ReRegisterRetries(), 1000);
}
//JFLog.log("Expires stop:" + System.currentTimeMillis());
}
}
/** TimerTask that reregisters any SIP connections that have failed to register (checks every 1 second upto 5 attempts). */
public class ReRegisterRetries extends java.util.TimerTask {
public void run() {
//JFLog.log("ReRegister start:" + System.currentTimeMillis());
boolean again = false;
for(int a=0;a<6;a++) {
if (Settings.current.lines[a].same != -1) continue;
if (lines[a].sip == null) continue;
if (lines[a].unauth) continue;
if (!lines[a].sip.isRegistered()) {
lines[a].sip.reregister();
again = true;
}
}
registerRetries++;
if ((again) && (registerRetries < 5)) {
timerRegisterRetries = new java.util.Timer();
timerRegisterRetries.schedule(new ReRegisterRetries(), 1000);
} else {
for(int a=0;a<6;a++) {
if (Settings.current.lines[a].same != -1) continue;
if (lines[a].sip == null) continue;
if (lines[a].unauth) continue;
if (!lines[a].sip.isRegistered()) {
lines[a].unauth = true; //server not responding after 5 attempts to register
}
}
timerRegisterRetries = null;
}
//JFLog.log("ReRegister stop:" + System.currentTimeMillis());
}
}
public void call() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (pl.sip == null) return;
if (!pl.sip.isRegistered()) return;
if (pl.incall) return; //already in call
if (pl.dial.length() == 0) return;
if (pl.incoming) {
callAccept();
} else {
callInvite();
}
if (Settings.current.ac) {
if (!pl.cnf) do_cnf();
}
}
public void redial() {
if (line == -1) return;
PhoneLine pl = lines[line];
pl.dial = lastDialed;
}
public void resetStatus(int forLine) {
PhoneLine pl = lines[forLine];
if ((pl.sip != null) && (!pl.unauth)) pl.status = "Line#" + (forLine+1) + " (" + pl.sip.getUser() + ")";
if (main != null) main._updateScreen();
}
public void clearDial() {
if (line == -1) return;
PhoneLine pl = lines[line];
pl.dial = "";
if (!pl.incall) {
resetStatus(line);
}
if (main != null) main.updateScreen(); //UI thread
}
/** Terminates a call. (may not be UI thread) */
public void end() {
if (line == -1) return;
PhoneLine pl = lines[line];
if (pl.incoming) {
pl.sip.deny(pl.callid, "IGNORE", 480);
pl.incoming = false;
pl.ringing = false;
pl.dial = "";
pl.status = "Hungup";
if (main != null) main._updateScreen();
return;
}
pl.dial = "";
if (!pl.incall) {
//no call (update status)
resetStatus(line);
if (main != null) main._updateScreen();
return;
}
if (pl.talking)
pl.sip.bye(pl.callid);
else
pl.sip.cancel(pl.callid);
endLine(line);
if (main != null) main._updateScreen();
}
/** Cleanup after a call is terminated (call terminated local or remote). (may not be UI thread) */
public void endLine(int forLine) {
PhoneLine pl = lines[forLine];
pl.dial = "";
pl.orgdial = "";
pl.status = "Hungup";
pl.trying = false;
pl.ringing = false;
pl.incoming = false;
pl.cnf = false;
pl.xfr = false;
pl.incall = false;
pl.talking = false;
pl.audioRTP.stop();
pl.audioRTP = null;
pl.callid = "";
pl.rtpStarted = false;
if (Settings.current.usePublish) pl.sip.publish("open");
if (main != null) main._updateScreen();
}
private byte[] genKey() {
byte ret[] = new byte[16];
new Random().nextBytes(ret);
return ret;
}
private byte[] genSalt() {
byte ret[] = new byte[14];
new Random().nextBytes(ret);
return ret;
}
private SDP getLocalSDPInvite(PhoneLine pl) {
SDP sdp = new SDP();
SDP.Stream stream = sdp.addStream(SDP.Type.audio);
stream.content = "audio1";
stream.port = pl.audioRTP.getlocalrtpport();
/*
if (pl.srtp) {
stream.profile = SDP.Profile.SAVP;
if (!pl.dtls) {
stream.keyExchange = SDP.KeyExchange.SDP;
stream.addKey("AES_CM_128_HMAC_SHA1_80", genKey(), genSalt());
} else {
stream.keyExchange = SDP.KeyExchange.DTLS;
stream.sdp.fingerprint = fingerprintSHA256;
stream.sdp.iceufrag = RTP.genIceufrag();
stream.sdp.icepwd = RTP.genIcepwd();
}
}
*/
String enabledCodecs[] = Settings.current.getAudioCodecs();
for(int a=0;a<enabledCodecs.length;a++) {
if (enabledCodecs[a].equals(RTP.CODEC_G729a.name)) stream.addCodec(RTP.CODEC_G729a);
if (enabledCodecs[a].equals(RTP.CODEC_G711u.name)) stream.addCodec(RTP.CODEC_G711u);
if (enabledCodecs[a].equals(RTP.CODEC_G711a.name)) stream.addCodec(RTP.CODEC_G711a);
if (enabledCodecs[a].equals(RTP.CODEC_G722.name)) stream.addCodec(RTP.CODEC_G722);
}
/*
if (!pl.disableVideo && Settings.current.nativeVideo) {
stream = sdp.addStream(SDP.Type.video);
stream.content = "video1";
stream.port = pl.videoRTP.getlocalrtpport();
if (pl.srtp) {
stream.profile = SDP.Profile.SAVP;
if (!pl.dtls) {
stream.keyExchange = SDP.KeyExchange.SDP;
stream.addKey("AES_CM_128_HMAC_SHA1_80", genKey(), genSalt());
} else {
stream.keyExchange = SDP.KeyExchange.DTLS;
stream.sdp.fingerprint = fingerprintSHA256;
stream.sdp.iceufrag = RTP.genIceufrag();
stream.sdp.icepwd = RTP.genIcepwd();
}
}
enabledCodecs = Settings.current.getVideoCodecs();
for(int a=0;a<enabledCodecs.length;a++) {
if (enabledCodecs[a].equals(RTP.CODEC_JPEG.name)) stream.addCodec(RTP.CODEC_JPEG);
if (Settings.hasFFMPEG) {
if (enabledCodecs[a].equals(RTP.CODEC_H263.name)) stream.addCodec(RTP.CODEC_H263);
if (enabledCodecs[a].equals(RTP.CODEC_H263_1998.name)) stream.addCodec(RTP.CODEC_H263_1998);
if (enabledCodecs[a].equals(RTP.CODEC_H263_2000.name)) stream.addCodec(RTP.CODEC_H263_2000);
if (enabledCodecs[a].equals(RTP.CODEC_H264.name)) stream.addCodec(RTP.CODEC_H264);
if (enabledCodecs[a].equals(RTP.CODEC_VP8.name)) stream.addCodec(RTP.CODEC_VP8);
}
}
}
*/
return sdp;
}
/** Returns the SDP Stream complementary mode (send <-> receive) */
private SDP.Mode complementMode(SDP.Mode mode) {
switch (mode) {
case recvonly: return SDP.Mode.sendonly;
case sendonly: return SDP.Mode.recvonly;
case inactive: //no break
case sendrecv: return mode;
}
return null;
}
/*
private void addVideoStream(PhoneLine pl, SDP sdp, SDP.Stream vstream, Codec codec) {
SDP.Stream newVstream = sdp.addStream(SDP.Type.video);
newVstream.port = pl.videoRTP.getlocalrtpport();
newVstream.mode = complementMode(vstream.mode);
newVstream.addCodec(codec);
if (pl.srtp) {
newVstream.profile = SDP.Profile.SAVP;
if (vstream.keyExchange == SDP.KeyExchange.SDP) {
newVstream.keyExchange = SDP.KeyExchange.SDP;
newVstream.addKey("AES_CM_128_HMAC_SHA1_80", genKey(), genSalt());
} else {
newVstream.keyExchange = SDP.KeyExchange.DTLS;
newVstream.sdp.fingerprint = fingerprintSHA256;
newVstream.sdp.iceufrag = RTP.genIceufrag();
newVstream.sdp.icepwd = RTP.genIcepwd();
}
}
}
*/
/** Returns SDP that matches requested SDP. */
private SDP getLocalSDPAccept(PhoneLine pl) {
SDP sdp = new SDP();
SDP.Stream astream = pl.sdp.getFirstAudioStream();
SDP.Stream vstream = pl.sdp.getFirstVideoStream();
SDP.Stream newAstream = sdp.addStream(SDP.Type.audio);
newAstream.port = pl.audioRTP.getlocalrtpport();
newAstream.mode = complementMode(astream.mode);
/*
if (pl.srtp) {
newAstream.profile = SDP.Profile.SAVP;
if (astream.keyExchange == SDP.KeyExchange.SDP) {
newAstream.keyExchange = SDP.KeyExchange.SDP;
newAstream.addKey("AES_CM_128_HMAC_SHA1_80", genKey(), genSalt());
} else {
newAstream.keyExchange = SDP.KeyExchange.DTLS;
newAstream.sdp.fingerprint = fingerprintSHA256;
newAstream.sdp.iceufrag = RTP.genIceufrag();
newAstream.sdp.icepwd = RTP.genIcepwd();
}
}
*/
String enabledCodecs[] = Settings.current.getAudioCodecs();
for(int a=0;a<enabledCodecs.length;a++) {
if ((enabledCodecs[a].equals(RTP.CODEC_G729a.name)) && (astream.hasCodec(RTP.CODEC_G729a))) {
newAstream.addCodec(RTP.CODEC_G729a);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_G711u.name)) && (astream.hasCodec(RTP.CODEC_G711u))) {
newAstream.addCodec(RTP.CODEC_G711u);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_G711a.name)) && (astream.hasCodec(RTP.CODEC_G711a))) {
newAstream.addCodec(RTP.CODEC_G711a);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_G722.name)) && (astream.hasCodec(RTP.CODEC_G722))) {
newAstream.addCodec(RTP.CODEC_G722);
break;
}
}
/*
if (!pl.disableVideo && vstream != null) {
enabledCodecs = Settings.current.getVideoCodecs();
for(int a=0;a<enabledCodecs.length;a++) {
if ((enabledCodecs[a].equals(RTP.CODEC_JPEG.name)) && (vstream.hasCodec(RTP.CODEC_JPEG))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_JPEG);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_H263.name)) && (vstream.hasCodec(RTP.CODEC_H263))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_H263);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_H263_1998.name)) && (vstream.hasCodec(RTP.CODEC_H263_1998))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_H263_1998);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_H263_2000.name)) && (vstream.hasCodec(RTP.CODEC_H263_2000))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_H263_2000);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_H264.name)) && (vstream.hasCodec(RTP.CODEC_H264))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_H264);
break;
}
if ((enabledCodecs[a].equals(RTP.CODEC_VP8.name)) && (vstream.hasCodec(RTP.CODEC_VP8))) {
addVideoStream(pl, sdp, vstream, RTP.CODEC_VP8);
break;
}
}
}
*/
return sdp;
}
/** Starts a outbound call. (may!UI thread) */
public void callInvite() {
PhoneLine pl = lines[line];
lastDialed = pl.dial;
pl.to = pl.dial;
pl.audioRTP = new RTP();
pl.audioRTP.init(this);
pl.incall = true;
pl.trying = false;
pl.ringing = false;
pl.talking = false;
pl.incoming = false;
pl.status = "Dialing";
lastDial = pl.dial;
pl.localsdp = getLocalSDPInvite(lines[line]);
pl.callid = pl.sip.invite(pl.dial, pl.localsdp);
pl.orgdial = pl.dial;
if (Settings.current.usePublish) pl.sip.publish("busy");
if (main != null) main._updateScreen();
}
/** Accepts an inbound call. (may!UI thread) */
public void callAccept() {
PhoneLine pl = lines[line];
pl.to = pl.dial;
pl.audioRTP = new RTP();
pl.audioRTP.init(this);
if (pl.sdp != null) {
pl.localsdp = getLocalSDPAccept(pl);
if (!startRTPinbound()) return;
} else {
//INVITE did not include SDP so start SDP negotiation on this side
pl.localsdp = getLocalSDPInvite(pl);
}
pl.sip.accept(pl.callid, pl.localsdp);
sound.flush();
pl.incall = true;
pl.ringing = false;
pl.incoming = false;
pl.talking = true;
pl.status = "Connected";
if (main != null) main._updateScreen();
}
/** Triggered when an outbound call (INVITE) was accepted. (!UI thread) */
public void callInviteSuccess(int forLine, SDP sdp) {
sound.flush();
PhoneLine pl = lines[forLine];
try {
pl.sdp = sdp;
if (!startRTPoutbound(forLine)) return;
if (Settings.current.aa) selectLine(forLine);
} catch (Exception e) {
JFLog.log(e);
pl.sip.bye(pl.callid);
onCancel(pl.sip, pl.callid, 500);
}
}
public void registered(SIPClient sip) {
for(int a=0;a<6;a++) {
if (lines[a].sip != sip) continue;
if (lines[a].status.length() == 0) resetStatus(a);
if (line == -1) {
selectLine(a);
}
}
sip.subscribe(sip.getUser(), "message-summary", 3600); //SUBSCRIBE to self for message-summary event (not needed with Asterisk but X-Lite does it)
if (Settings.current.usePublish) lines[line].sip.publish("open");
}
public void unauthorized(SIPClient sip) {
for(int a=0;a<6;a++) {
if (lines[a].sip == sip) {
lines[a].status = "Unauthorized";
lines[a].unauth = true;
if (line == a) selectLine(-1);
}
}
}
/** Changes SDP/RTP details. */
public void change(int xline, SDP sdp) {
try {
PhoneLine pl = lines[xline];
boolean used[] = new boolean[sdp.streams.length];
for(int c=0;c<pl.audioRTP.channels.size();) {
RTPChannel channel = pl.audioRTP.channels.get(c);
boolean ok = false;
for(int s=0;s<sdp.streams.length;s++) {
SDP.Stream stream = sdp.streams[s];
if (stream.content.equals(channel.stream.content)) {
channel.change(stream);
ok = true;
used[s] = true;
break;
}
}
if (!ok) {
//RTPChannel no longer in use
pl.audioRTP.removeChannel(channel);
} else {
c++;
}
}
/*
if (pl.videoRTP != null) {
for(int c=0;c<pl.videoRTP.channels.size();) {
RTPChannel channel = pl.videoRTP.channels.get(c);
boolean ok = false;
for(int s=0;s<sdp.streams.length;s++) {
SDP.Stream stream = sdp.streams[s];
if (stream.content.equals(channel.stream.content)) {
channel.change(stream);
ok = true;
used[s] = true;
break;
}
}
if (!ok) {
//RTPChannel no longer in use
JFLog.log("Video Channel no longer in use:" + channel.stream.content);
pl.videoRTP.removeChannel(channel);
RemoteCamera camera = findRemoteCamera(channel);
if (camera != null) delRemoteCamera(pl, camera);
} else {
c++;
}
}
}
//check if new video stream's were added
for(int s=0;s<sdp.streams.length;s++) {
SDP.Stream stream = sdp.streams[s];
if (used[s]) continue;
if (stream.type == SDP.Type.audio) continue; //ignore additional audio streams
if (!stream.canRecv() || !stream.canSend()) continue; //ignore inactive streams
if (pl.videoRTP == null) {
//video never started yet
pl.videoRTP = new RTP();
pl.videoRTP.start();
}
RTPChannel channel = pl.videoRTP.createChannel(stream);
if (channel == null) {
JFLog.log("RTP.createChannel() failed");
continue;
}
if (stream.isSecure() && pl.sdp.getFirstVideoStream().keyExchange == SDP.KeyExchange.SDP) {
//TODO : add crypto keys
JFLog.log("Not implemented yet!");
continue;
}
if (!channel.start()) {
JFLog.log("RTP start failed");
continue;
}
if (pl.videoWindow == null) continue;
addRemoteCamera(pl, channel);
}
*/
} catch (Exception e) {
JFLog.log(e);
}
}
/** Starts RTP after negotiation is complete (inbound call only). */
public boolean startRTPinbound() {
PhoneLine pl = lines[line];
try {
SDP.Stream astream = pl.sdp.getFirstAudioStream();
SDP.Stream vstream = pl.sdp.getFirstVideoStream();
/*
String list[] = Settings.current.getAudioCodecs();
for(int a=0;a<list.length;a++) {
JFLog.log("enabled[a]=" + list[a]);
}
Codec codecs[] = astream.codecs;
for(int a=0;a<codecs.length;a++) {
JFLog.log("offer[a]=" + codecs[a]);
}
*/
if ( (!astream.hasCodec(RTP.CODEC_G729a) || !Settings.current.hasAudioCodec(RTP.CODEC_G729a))
&& (!astream.hasCodec(RTP.CODEC_G711u) || !Settings.current.hasAudioCodec(RTP.CODEC_G711u))
&& (!astream.hasCodec(RTP.CODEC_G711a) || !Settings.current.hasAudioCodec(RTP.CODEC_G711a))
&& (!astream.hasCodec(RTP.CODEC_G722) || !Settings.current.hasAudioCodec(RTP.CODEC_G722)) )
{
JFLog.log("err:callAccept() : No compatible audio codec offered");
pl.sip.deny(pl.callid, "NO_COMPATIBLE_CODEC", 415);
onCancel(pl.sip, pl.callid, 415);
return false;
}
astream.setCodec(pl.localsdp.getFirstAudioStream().codecs[0]);
if (vstream != null) {
if (pl.localsdp.hasVideo()) {
vstream.setCodec(pl.localsdp.getFirstVideoStream().codecs[0]);
} else {
vstream = null; //no video codecs match
}
}
if (!pl.audioRTP.start()) {
throw new Exception("RTP.start() failed");
}
if (pl.audioRTP.createChannel(astream) == null) {
throw new Exception("RTP.createChannel() failed");
}
if (pl.sdp.getFirstAudioStream().isSecure()) {
SRTPChannel channel = (SRTPChannel)pl.audioRTP.getDefaultChannel();
if (pl.sdp.getFirstAudioStream().keyExchange == SDP.KeyExchange.SDP) {
SDP.Stream local = pl.localsdp.getFirstAudioStream();
SDP.Key localKey = local.getKey("AES_CM_128_HMAC_SHA1_80");
if (localKey == null) throw new Exception("Local SRTP keys not found");
channel.setLocalKeys(localKey.key, localKey.salt);
SDP.Stream remote = pl.sdp.getFirstAudioStream();
SDP.Key remoteKey = remote.getKey("AES_CM_128_HMAC_SHA1_80");
if (remoteKey == null) throw new Exception("Remote SRTP keys not found");
channel.setRemoteKeys(remoteKey.key, remoteKey.salt);
} else {
SDP.Stream local = pl.localsdp.getFirstAudioStream();
channel.setDTLS(true, local.sdp.iceufrag, local.sdp.icepwd);
}
}
if (!pl.audioRTP.getDefaultChannel().start()) {
throw new Exception("RTPChannel.start() failed");
}
/*
if (!pl.videoRTP.start()) {
throw new Exception("RTP.start() failed");
}
if (vstream != null) {
if (pl.videoRTP.createChannel(vstream) == null) {
throw new Exception("RTP.createChannel() failed");
}
if (pl.sdp.getFirstVideoStream().isSecure()) {
SRTPChannel channel = (SRTPChannel)pl.videoRTP.getDefaultChannel();
if (pl.sdp.getFirstVideoStream().keyExchange == SDP.KeyExchange.SDP) {
SDP.Stream local = pl.localsdp.getFirstVideoStream();
SDP.Key localKey = local.getKey("AES_CM_128_HMAC_SHA1_80");
if (localKey == null) throw new Exception("Local SRTP keys not found");
channel.setLocalKeys(localKey.key, localKey.salt);
SDP.Stream remote = pl.sdp.getFirstVideoStream();
SDP.Key remoteKey = remote.getKey("AES_CM_128_HMAC_SHA1_80");
if (remoteKey == null) throw new Exception("Remote SRTP keys not found");
channel.setRemoteKeys(remoteKey.key, remoteKey.salt);
} else {
SDP.Stream local = pl.localsdp.getFirstVideoStream();
channel.setDTLS(true, local.sdp.iceufrag, local.sdp.icepwd);
}
}
if (!pl.videoRTP.getDefaultChannel().start()) {
throw new Exception("RTPChannel.start() failed");
}
}
*/
pl.rtpStarted = true;
return true;
} catch (Exception e) {
JFLog.log(e);
pl.sip.deny(pl.callid, "RTP_START_FAILED", 500);
onCancel(pl.sip, pl.callid, 500);
return false;
}
}
public boolean startRTPoutbound(int xline) {
PhoneLine pl = lines[xline];
try {
SDP.Stream astream = pl.sdp.getFirstAudioStream();
SDP.Stream vstream = pl.sdp.getFirstVideoStream();
JFLog.log("note:callInviteSuccess():remotertpport=" + astream.port + ",remoteVrtport=" + (vstream != null ? vstream.port : -1));
if ( (!astream.hasCodec(RTP.CODEC_G729a) || !Settings.current.hasAudioCodec(RTP.CODEC_G729a))
&& (!astream.hasCodec(RTP.CODEC_G711u) || !Settings.current.hasAudioCodec(RTP.CODEC_G711u))
&& (!astream.hasCodec(RTP.CODEC_G711a) || !Settings.current.hasAudioCodec(RTP.CODEC_G711a))
&& (!astream.hasCodec(RTP.CODEC_G722) || !Settings.current.hasAudioCodec(RTP.CODEC_G722)) )
{
JFLog.log("err:callInviteSuccess() : No compatible audio codec returned");
pl.sip.bye(pl.callid);
onCancel(pl.sip, pl.callid, 415);
return false;
}
if (!pl.audioRTP.start()) {
throw new Exception("RTP.start() failed");
}
if (pl.audioRTP.createChannel(astream) == null) {
throw new Exception("RTP.createChannel() failed");
}
if (pl.sdp.getFirstAudioStream().isSecure()) {
SRTPChannel channel = (SRTPChannel)pl.audioRTP.getDefaultChannel();
if (pl.sdp.getFirstAudioStream().keyExchange == SDP.KeyExchange.SDP) {
SDP.Stream local = pl.localsdp.getFirstAudioStream();
SDP.Key localKey = local.getKey("AES_CM_128_HMAC_SHA1_80");
if (localKey == null) throw new Exception("Local SRTP keys not found");
channel.setLocalKeys(localKey.key, localKey.salt);
SDP.Stream remote = pl.sdp.getFirstAudioStream();
SDP.Key remoteKey = remote.getKey("AES_CM_128_HMAC_SHA1_80");
if (remoteKey == null) throw new Exception("Remote SRTP keys not found");
channel.setRemoteKeys(remoteKey.key, remoteKey.salt);
} else {
SDP.Stream local = pl.localsdp.getFirstAudioStream();
channel.setDTLS(false, local.sdp.iceufrag, local.sdp.icepwd);
}
}
if (!pl.audioRTP.getDefaultChannel().start()) {
throw new Exception("RTPChannel.start() failed");
}
/*
if (!pl.videoRTP.start()) {
throw new Exception("RTP.start() failed");
}
if (vstream != null) {
if (pl.videoRTP.createChannel(vstream) == null) {
throw new Exception("RTP.createChannel() failed");
}
if (pl.sdp.getFirstVideoStream().isSecure()) {
SRTPChannel channel = (SRTPChannel)pl.videoRTP.getDefaultChannel();
if (pl.sdp.getFirstVideoStream().keyExchange == SDP.KeyExchange.SDP) {
SDP.Stream local = pl.localsdp.getFirstVideoStream();
SDP.Key localKey = local.getKey("AES_CM_128_HMAC_SHA1_80");
if (localKey == null) throw new Exception("Local SRTP keys not found");
channel.setLocalKeys(localKey.key, localKey.salt);
SDP.Stream remote = pl.sdp.getFirstVideoStream();
SDP.Key remoteKey = remote.getKey("AES_CM_128_HMAC_SHA1_80");
if (remoteKey == null) throw new Exception("Remote SRTP keys not found");
channel.setRemoteKeys(remoteKey.key, remoteKey.salt);
} else {
SDP.Stream local = pl.localsdp.getFirstVideoStream();
channel.setDTLS(false, local.sdp.iceufrag, local.sdp.icepwd);
}
}
if (!pl.videoRTP.getDefaultChannel().start()) {
throw new Exception("RTPChannel.start() failed");
}
}
*/
pl.rtpStarted = true;
return true;
} catch (Exception e) {
JFLog.log(e);
pl.sip.deny(pl.callid, "RTP_START_FAILED", 500);
onCancel(pl.sip, pl.callid, 500);
return false;
}
}
/** Send a reINVITE when the callee returns multiple codecs to select only most prefered codec (if reinvite is enabled). */
public boolean reinvite(PhoneLine pl) {
SDP.Stream astream = pl.sdp.getFirstAudioStream();
SDP.Stream vstream = pl.sdp.getFirstVideoStream();
int acnt = 0;
int vcnt = 0;
if (SIP.hasCodec(astream.codecs, RTP.CODEC_G729a)) acnt++;
if (SIP.hasCodec(astream.codecs, RTP.CODEC_G711u)) acnt++;
if (SIP.hasCodec(astream.codecs, RTP.CODEC_G711a)) acnt++;
if (SIP.hasCodec(astream.codecs, RTP.CODEC_G722)) acnt++;
if (vstream != null) {
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_JPEG)) vcnt++;
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_H263)) vcnt++;
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_H263_1998)) vcnt++;
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_H263_2000)) vcnt++;
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_H264)) vcnt++;
if (SIP.hasCodec(vstream.codecs, RTP.CODEC_VP8)) vcnt++;
}
if ((acnt > 1 || vcnt > 1) && (Settings.current.reinvite)) {
//returned more than one audio codec, reinvite with only one codec
//do NOT reINVITE from a 183 - server will respond with 491 (request pending) and abort the call
pl.localsdp = getLocalSDPAccept(pl);
pl.sip.reinvite(pl.callid, pl.localsdp);
return true;
}
return false;
}
//SIPClientInterface interface
/** SIPClientInterface : onRegister() : triggered when a SIPClient has successfully or failed to register with server. */
public void onRegister(SIPClient sip, boolean status) {
if (status) registered(sip); else unauthorized(sip);
}
/** SIPClientInterface : onTrying() : triggered when an INVITE returns status code 100 (TRYING). */
public void onTrying(SIPClient sip, String callid) {
//is a line trying to do an invite
for(int a=0;a<6;a++) {
if ((lines[a].incall)&&(!lines[a].trying)) {
if (lines[a].callid.equals(callid)) {
lines[a].trying = true;
lines[a].status = "Trying";
if (line == a) if (main != null) main._updateScreen();
}
}
}
}
/** SIPClientInterface : onRinging() : triggered when an INVITE returns status code 180/183 (RINGING). */
public void onRinging(SIPClient sip, String callid) {
//is a line trying to do an invite
for(int a=0;a<6;a++) {
if ((lines[a].incall)&&(!lines[a].ringing)) {
if (lines[a].callid.equals(callid)) {
lines[a].ringing = true;
lines[a].status = "Ringing";
if (line == a) if (main != null) main._updateScreen();
}
}
}
}
/** SIPClientInterface : onSuccess() : triggered when an INVITE returns status code 200 (OK). */
public void onSuccess(SIPClient sip, String callid, SDP sdp, boolean complete) {
if (!complete) {
//183 - could start RTP and listen to ringback tone
onRinging(sip,callid);
return;
}
//is a line trying to do an invite or hold
for(int a=0;a<6;a++) {
if (!lines[a].incall) continue;
if (!lines[a].callid.equals(callid)) continue;
if (!lines[a].talking) {
lines[a].sdp = sdp;
if (reinvite(lines[a])) {
JFLog.log("reINVITE");
return;
}
lines[a].status = "Connected";
if (line == a) if (main != null) main._updateScreen();
callInviteSuccess(a, sdp);
lines[a].talking = true;
lines[a].ringing = false;
return;
} else {
//reINVITE accepted (200)
lines[a].sdp = sdp;
change(a, sdp);
return;
}
}
}
/** SIPClientInterface : onBye() : triggered when server terminates a call. */
public void onBye(SIPClient sip, String callid) {
for(int a=0;a<6;a++) {
if (lines[a].incall) {
if (lines[a].callid.equals(callid)) {
if (main != null) {
endLine(a);
if ((main != null) && (!main.active)) main.notify("Idle", false);
}
}
}
}
}
/** SIPClientInterface : onInvite() : triggered when server send an INVITE to jphonelite. */
public int onInvite(SIPClient sip, String callid, String fromid, String fromnumber, SDP sdp) {
for(int a=0;a<6;a++) {
PhoneLine pl = lines[a];
if (lines[a].sip == sip) {
if (lines[a].sip.getUser().equals(fromnumber)) {
return 486; //reply BUSY - attempt to call self (should goto voicemail now)
}
if (lines[a].callid.equals(callid)) {
//reINVITEd (usually to change RTP host/port) (codec should not change since we only accept with 1 codec)
if (sdp == null) {
JFLog.log("onInvite: SDP null on reinvite");
return -1; //TODO : send an error and drop call???
}
pl.sdp = sdp;
change(a, sdp);
pl.localsdp = getLocalSDPAccept(pl);
pl.sip.reaccept(callid, pl.localsdp); //send 200 rely with new SDP
return -1; //do not send a reply
}
if (pl.incall) continue;
if (pl.incoming) continue;
lines[a].dial = fromnumber;
lines[a].callerid = fromid;
if ((lines[a].callerid == null) || (lines[a].callerid.trim().length() == 0)) lines[a].callerid = "Unknown";
lines[a].status = fromid + " is calling";
lines[a].incoming = true;
pl.sdp = sdp;
lines[a].callid = callid;
lines[a].ringing = true;
if (Settings.current.aa) {
selectLine(a);
if (main != null) main._updateScreen();
call(); //this will send a reply
return -1; //do NOT send a reply
} else {
if (line == a) if (main != null) main._updateScreen();
}
if (main != null) {
if ((main != null) && (!main.active)) main.notify("" + fromnumber + " is calling.", true);
}
return 180; //reply RINGING
}
}
return 486; //reply BUSY
}
/** SIPClientInterface : onCancel() : triggered when server send a CANCEL request after an INVITE. */
public void onCancel(SIPClient sip, String callid, int code) {
for(int a=0;a<6;a++) {
if (lines[a].callid.equals(callid)) {
PhoneLine pl = lines[a];
pl.incall = false;
pl.trying = false;
if (pl.audioRTP != null) {
pl.audioRTP.uninit();
pl.audioRTP = null;
}
pl.callid = "";
pl.dial = "";
pl.status = "Hungup (" + code + ")";
pl.ringing = false;
pl.incoming = false;
pl.rtpStarted = false;
if (line == a) if (main != null) main._updateScreen();
}
}
}
/** SIPClientInterface : onRefer() : triggered when the server signals a successful transfer (REFER). */
public void onRefer(SIPClient sip, String callid) {
for(int a=0;a<6;a++) {
if (lines[a].callid.equals(callid)) {
if (line != a) selectLine(a);
end();
}
}
}
/** SIPClientInterface : onNotify() : processes SIP:NOTIFY messages. */
public void onNotify(SIPClient sip, String callid, String event, String content) {
String contentLines[] = content.split("\r\n");
if (event.equals("message-summary")) {
String msgwait = SIP.getHeader("Messages-Waiting:", contentLines);
if (msgwait != null) {
for(int a=0;a<6;a++) {
if (lines[a].sip == sip) {
lines[a].msgwaiting = msgwait.equalsIgnoreCase("yes");
}
}
}
return;
}
/* //not supported in android release
if (event.equals("presence")) {
if (!content.startsWith("<?xml")) return;
content = content.replaceAll("\r", "").replaceAll("\n", "");
XML xml = new XML(); //against my better judgement I'm going to use my own XML (un)marshaller to process the data
ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes());
if (!xml.read(bais)) return;
XML.XMLTag contact = xml.getTag(new String[] { "presence", "tuple", "contact" });
if (contact == null) return;
String fields[] = SIP.split("Unknown<" + contact.getContent() + ">");
if (fields == null) return;
XML.XMLTag status = xml.getTag(new String[] { "presence", "tuple", "status", "basic" });
if (status == null) return;
setStatus(fields[1], fields[2], status.getContent().trim());
return;
}
*/
}
/** SIPClientInterface : getResponse() : not used in jPhoneLite */
public String getResponse(SIPClient client, String realm, String cmd, String uri, String nonce, String qop, String nc, String cnonce) {
return null; //not used
}
public void onAck(SIPClient client, String callid, SDP sdp) {
if (sdp == null) return;
for(int a=0;a<6;a++) {
PhoneLine pl = lines[a];
if (pl.sip == client) {
if (!pl.rtpStarted) {
//RFC 3665 section 3.6 - ACK provides SDP instead of INVITE
pl.sdp = sdp;
startRTPinbound();
}
}
}
}
//interface RTPInterface (not used in android release)
public void rtpSamples(RTPChannel rtp) {}
public void rtpDigit(RTPChannel rtp, char digit) {}
public void rtpPacket(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtcpPacket(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH263(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH263_1998(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH263_2000(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH264(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpVP8(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpJPEG(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpInactive(RTPChannel rtp) {}
}