/* * BasePhone.java * * Created on Oct 22, 2011, 10:26:03 AM * * @author pquiring@gmail.com * */ import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import java.lang.reflect.*; import javaforce.*; import javaforce.voip.*; import javaforce.media.*; /** Base Panel contains all phone logic code. */ public abstract class BasePhone extends javax.swing.JPanel implements SIPClientInterface, RTPInterface, ActionListener, KeyEventDispatcher { public static String version = "1.11"; public void initBasePhone(GUI gui, WindowController wc) { JFLog.init(JF.getUserPath() + "/.jphone.log", true); this.gui = gui; this.wc = wc; setLAF(); //must do this before any GUI elements are created KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this); if (Settings.isJavaScript) initRPC(); Settings.isWindows = JF.isWindows(); Settings.isLinux = !Settings.isWindows; Settings.hasFFMPEG = MediaCoder.loaded; initDTLS(); } //global data public GUI gui; public int sipmin = 5061; public int sipmax = 5199; public int sipnext = 5061; public int line = -1; //current selected line (0-5) (-1=none) public PhoneLine lines[]; public JToggleButton lineButtons[]; public JButton numButtons[]; public Audio sound = new Audio(); public WindowController wc; public String lastDial; public boolean showingContacts = false; public java.util.Timer timerKeepAlive, timerRegisterExpires, timerRegisterRetries; public ImageIcon ii[]; public String icons[] = { "blk.png", "grn.png", "red.png", "grey.png", "orange.png", "mic.png", "headset.png", "mute.png", "spk.png", "swscale.png", "hwscale.png", "icon_open.png", "icon_closed.png", "icon_busy.png", "icon_idle.png", "icon_dnd.png", "labels1.png", "labels2.png", "trayicon.png", "call.png", "end.png", "call2.png", "end2.png", "logo.png", "video.png", "record.png" }; public final int PIC_BLACK = 0; public final int PIC_GREEN = 1; public final int PIC_RED = 2; public final int PIC_GREY = 3; public final int PIC_ORANGE = 4; public final int PIC_MIC = 5; public final int PIC_HEADSET = 6; public final int PIC_MUTE = 7; public final int PIC_SPK = 8; public final int PIC_SWSCALE = 9; public final int PIC_HWSCALE = 10; public final int PIC_ICON_OPEN = 11; public final int PIC_ICON_CLOSED = 12; public final int PIC_ICON_BUSY = 13; public final int PIC_ICON_IDLE = 14; public final int PIC_ICON_DND = 15; public final int PIC_LABELS1 = 16; public final int PIC_LABELS2 = 17; public final int PIC_TRAY = 18; public final int PIC_CALL = 19; public final int PIC_END = 20; public final int PIC_CALL2 = 21; public final int PIC_END2 = 22; public final int PIC_LOGO = 23; public final int PIC_VIDEO = 24; public final int PIC_RECORD = 25; public int registerRetries; public SystemTray tray; public TrayIcon icon; public MenuItem exit, show; public Vector<Contact> contactList = new Vector<Contact>(); public Vector<String> monitorList = new Vector<String>(); public boolean active = true; public boolean muted = false; public abstract void rtp_jpeg_receive(RTPChannel rtp, byte data[], int pos, int len); public abstract void rtp_h263_receive(RTPChannel rtp, byte data[], int pos, int len); public abstract void rtp_h263_1998_receive(RTPChannel rtp, byte data[], int pos, int len); public abstract void rtp_h263_2000_receive(RTPChannel rtp, byte data[], int pos, int len); public abstract void rtp_h264_receive(RTPChannel rtp, byte data[], int pos, int len); public abstract void rtp_vp8_receive(RTPChannel rtp, byte data[], int pos, int len); public boolean registeringAll = false; public boolean doConfig = false; //do config after register all /** Registers all SIP connections. */ public void reRegisterAll() { registeringAll = true; int idx; String host; int port = -1; for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if ((a > 0) && (Settings.current.lines[a].same != -1)) continue; pl.disableVideo = Settings.current.lines[a].disableVideo; pl.srtp = Settings.current.lines[a].srtp; pl.dtls = Settings.current.lines[a].dtls; if (Settings.current.lines[a].host.length() == 0) continue; if (Settings.current.lines[a].user.length() == 0) continue; lines[a].sip = new SIPClient(); idx = Settings.current.lines[a].host.indexOf(':'); if (idx == -1) { host = Settings.current.lines[a].host; switch (Settings.current.lines[a].transport) { case 0: case 1: port = 5060; //default UDP/TCP port break; case 2: port = 5061; //default TLS port break; } } else { host = Settings.current.lines[a].host.substring(0,idx); port = JF.atoi(Settings.current.lines[a].host.substring(idx+1)); } switch (Settings.current.lines[a].transport) { case 0: pl.transport = SIP.Transport.UDP; break; case 1: pl.transport = SIP.Transport.TCP; break; case 2: pl.transport = SIP.Transport.TLS; break; } int attempt = 0; while (!pl.sip.init(host, port, getlocalport(), this, pl.transport)) { attempt++; if (attempt==10) { pl.sip = null; pl.status = "SIP init failed"; if (a == line) gui.updateLine(); break; } } if (pl.sip == null) continue; //sip.init failed pl.user = Settings.current.lines[a].user; //JFLog.log("lines[" + a + "].pass=" + Settings.current.lines[a].pass + "!"); if ((Settings.current.lines[a].pass == null) || (Settings.current.lines[a].pass.length() == 0) || (Settings.current.lines[a].pass.equals("crypto(1,)"))) { pl.auth = true; pl.noregister = true; pl.status = "Ready (" + pl.user + ")"; } } //setup "Same as" lines int same; for(int a=1;a<6;a++) { same = Settings.current.lines[a].same; if (same == -1) continue; PhoneLine pl = lines[a]; pl.disableVideo = lines[same].disableVideo; pl.srtp = lines[same].srtp; pl.transport = lines[same].transport; pl.dtls = lines[same].dtls; pl.sip = lines[same].sip; pl.user = lines[same].user; pl.noregister = lines[same].noregister; if (pl.noregister) { pl.auth = true; pl.status = "Ready (" + pl.user + ")"; } } //register lines for(int a=0;a<6;a++) { if ((a > 0) && (Settings.current.lines[a].same != -1)) continue; PhoneLine pl = lines[a]; if (pl.sip == null) continue; try { pl.sip.register(Settings.current.lines[a].name, Settings.current.lines[a].user, Settings.current.lines[a].auth , Settings.getPassword(Settings.current.lines[a].pass), Settings.current.sipexpires); } catch (Exception e) { JFLog.log(e); } } //setup reRegister timer (expires) int expires = Settings.current.sipexpires; expires -= 5; //do a little early just in case expires *= 1000; //convert to ms timerRegisterExpires = new java.util.Timer(); timerRegisterExpires.scheduleAtFixedRate(new ReRegisterExpires(), expires, expires); registerRetries = 0; timerRegisterRetries = new java.util.Timer(); timerRegisterRetries.schedule(new ReRegisterRetries(), 1000); registeringAll = false; if (doConfig) { doConfig = false; gui.doConfig(); } } /** Expires registration with all SIP connections. */ public void unRegisterAll() { if (timerRegisterExpires != null) { timerRegisterExpires.cancel(); timerRegisterExpires = null; } if (timerRegisterRetries != null) { timerRegisterRetries.cancel(); timerRegisterRetries = null; } for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.incall) { gui.selectLine(a); if (pl.talking) pl.sip.bye(pl.callid); else pl.sip.cancel(pl.callid); endLine(a); } pl.dial = ""; pl.status = ""; pl.unauth = false; pl.auth = false; pl.noregister = false; pl.user = ""; if ((a > 0) && (Settings.current.lines[a].same != -1)) { pl.sip = null; continue; } if (pl.sip == null) continue; if (pl.sip.isRegistered()) { try { pl.sip.unregister(); } catch (Exception e) { JFLog.log(e); } } } int maxwait; for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == null) continue; maxwait = 1000; while (pl.sip.isRegistered()) { JF.sleep(10); maxwait -= 10; if (maxwait == 0) break; } pl.sip.uninit(); pl.sip = null; } } /** Add a digit to be dialed. */ public void addDigit(char digit) { if (line == -1) return; PhoneLine pl = lines[line]; if (pl.sip == null) return; if (!pl.auth) return; if (pl.incoming) return; if (digit == KeyEvent.VK_BACK_SPACE) { if ((pl.incall)&&(!pl.xfer)) return; //delete digit int len = pl.dial.length(); if (len > 0) pl.dial = pl.dial.substring(0, len-1); } else { if ((pl.incall)&&(!pl.xfer)) return; pl.dial += digit; } gui.updateLine(); } /** KeyBinding action. Causes DTMF generation if in a call. */ public void pressDigit(char digit) { if (line == -1) return; PhoneLine pl = lines[line]; if (pl.xfer) return; pl.dtmf = digit; } /** KeyBinding action. Stops DTMF generation. */ public void releaseDigit(char digit) { if (line == -1) return; PhoneLine pl = lines[line]; if (pl.dtmf != 'x') pl.dtmfend = true; } /** Clears number to be dialed. */ public void clear() { if (line == -1) return; lines[line].dial = ""; gui.updateLine(); } /** Sets the entire # to be dialed. */ public void setDial(String number) { if (line == -1) return; lines[line].dial = number; gui.updateLine(); } /** Starts a call or accepts an inbound call on selected line. */ public void call() { gui.updateCallButton(false); if (line == -1) return; PhoneLine pl = lines[line]; if (pl.sip == null) return; if (!pl.auth) return; if (pl.incall) {gui.updateCallButton(true); return;} //already in call if (pl.dial.length() == 0) {gui.updateCallButton(false); return;} gui.updateCallButton(true); if (pl.incoming) { callAccept(); } else { callInvite(); } if (Settings.current.ac) { if (!pl.cnf) doConference(); } } /** Terminates a call. */ public void end() { gui.updateEndButton(false); if (line == -1) return; PhoneLine pl = lines[line]; if (pl.incoming) { pl.sip.deny(pl.callid, "IGNORE", 486); pl.incoming = false; pl.ringing = false; pl.ringback = false; pl.dial = ""; pl.status = "Hungup"; gui.updateLine(); return; } pl.dial = ""; if (!pl.incall) { //no call (update status) if ((pl.sip != null) && (!pl.unauth)) pl.status = "Ready (" + pl.user + ")"; gui.updateLine(); return; } if (pl.talking) pl.sip.bye(pl.callid); else pl.sip.cancel(pl.callid); endLine(line); } /** Cleanup after a call is terminated (call terminated local or remote). */ public void endLine(int xline) { gui.updateEndButton(false); gui.updateCallButton(false); PhoneLine pl = lines[xline]; pl.dial = ""; pl.status = "Hungup"; pl.trying = false; pl.ringing = false; pl.ringback = false; pl.incoming = false; pl.cnf = false; pl.xfer = false; pl.incall = false; pl.talking = false; pl.hld = false; pl.rtpStarted = false; if (pl.audioRTP != null) { pl.audioRTP.stop(); pl.audioRTP = null; } if (pl.videoRTP != null) { pl.videoRTP.stop(); pl.videoRTP = null; } pl.callid = ""; if (line == xline) gui.updateLine(); gui.endLineUpdate(xline); } /** Starts a outbound call. */ public void callInvite() { PhoneLine pl = lines[line]; pl.to = pl.dial; pl.incall = true; pl.trying = false; pl.ringing = false; pl.ringback = false; pl.talking = false; pl.incoming = false; pl.status = "Dialing"; lastDial = pl.dial; Settings.addCallLog(pl.dial); pl.audioRTP = new RTP(); if (!pl.audioRTP.init(this)) { endLine(line); pl.dial = ""; pl.status = "RTP init failed"; gui.updateLine(); return; } pl.videoRTP = new RTP(); if (!pl.videoRTP.init(this)) { endLine(line); pl.dial = ""; pl.status = "RTP init failed"; gui.updateLine(); return; } pl.localsdp = getLocalSDPInvite(pl); pl.callid = pl.sip.invite(pl.dial, pl.localsdp); gui.updateLine(); gui.callInviteUpdate(); } /** 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_GSM.name)) && (astream.hasCodec(RTP.CODEC_GSM))) { newAstream.addCodec(RTP.CODEC_GSM); 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; } /** Returns SDP packet for all enabled codecs (audio and video). */ 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_GSM.name)) stream.addCodec(RTP.CODEC_GSM); 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; } /** 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(); 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_GSM) || !Settings.current.hasAudioCodec(RTP.CODEC_GSM)) && (!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_GSM) || !Settings.current.hasAudioCodec(RTP.CODEC_GSM)) && (!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; } } /** Accepts an inbound call on selected line. */ public void callAccept() { PhoneLine pl = lines[line]; try { pl.to = pl.dial; pl.audioRTP = new RTP(); if (!pl.audioRTP.init(this)) { throw new Exception("RTP.init() failed"); } pl.videoRTP = new RTP(); if (!pl.videoRTP.init(this)) { throw new Exception("RTP.init() failed"); } pl.videoRTP.setMTU(1500); 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); pl.incall = true; pl.ringing = false; pl.ringback = false; pl.incoming = false; pl.talking = true; pl.status = "Connected"; gui.updateLine(); updateIconTray(); } catch (Exception e) { JFLog.log(e); pl.sip.deny(pl.callid, "RTP_START_FAILED", 500); onCancel(pl.sip, pl.callid, 500); } } /** Triggered when an outbound call (INVITE) was accepted. */ public boolean callInviteSuccess(int xline, SDP sdp) { PhoneLine pl = lines[xline]; try { pl.sdp = sdp; if (!startRTPoutbound(xline)) return false; if (Settings.current.aa) gui.selectLine(xline); } catch (Exception e) { JFLog.log(e); pl.sip.bye(pl.callid); onCancel(pl.sip, pl.callid, 500); return false; } return true; } /** Triggered when an outbound call (INVITE) was refused. */ public void callInviteFail(int xline) { PhoneLine pl = lines[xline]; pl.incall = false; pl.trying = false; if (pl.audioRTP != null) { pl.audioRTP.uninit(); } if (pl.videoRTP != null) { pl.videoRTP.uninit(); } pl.callid = ""; } /** Start or finish a call transfer. */ public void doXfer() { if (line == -1) return; PhoneLine pl = lines[line]; if (!pl.talking) return; if (pl.xfer) { if (pl.dial.length() == 0) { //cancel xfer pl.status = "Connected"; } else { pl.sip.refer(pl.callid, pl.dial); pl.status = "XFER Requested"; } pl.xfer = false; } else { pl.dial = ""; pl.status = "Enter dest and then XFER again"; pl.xfer = true; } gui.updateLine(); } /** Put a call into or out of hold. */ public void doHold() { gui.hld_setIcon(ii[PIC_GREY]); 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); gui.hld_setIcon(ii[pl.hld ? PIC_RED : PIC_GREY]); } /** Redial last number dialed. */ public void doRedial() { if (line == -1) return; PhoneLine pl = lines[line]; if (pl.incall) return; if (lastDial == null) return; if (lastDial.length() == 0) return; pl.dial = lastDial; gui.updateLine(); call(); } /** Toggles AA (Auto Answer). */ public void toggleAA() { Settings.current.aa = !Settings.current.aa; gui.aa_setIcon(ii[Settings.current.aa ? PIC_GREEN : PIC_GREY]); } /** Toggle AC (Auto Conference). */ public void toggleAC() { Settings.current.ac = !Settings.current.ac; gui.ac_setIcon(ii[Settings.current.ac ? PIC_GREEN : PIC_GREY]); } /** Toggle DND (Do-Not-Disturb). */ public void toggleDND() { if (line == -1) return; PhoneLine pl = lines[line]; if (pl.incall) return; pl.dnd = !pl.dnd; gui.dnd_setIcon(ii[pl.dnd ? PIC_RED : PIC_GREY]); if (pl.dnd) pl.dial = Settings.current.dndCodeOn; else pl.dial = Settings.current.dndCodeOff; call(); } /** Toggles the conference state of a line. */ public void doConference() { if (line == -1) return; PhoneLine pl = lines[line]; if (!pl.incall) return; pl.cnf = !pl.cnf; gui.cnf_setIcon(ii[pl.cnf ? PIC_GREEN : PIC_GREY]); } /** Toggles mute. */ public void toggleMute() { muted = !muted; sound.setMute(muted); gui.mute_setIcon(ii[muted ? PIC_RED : PIC_GREY]); } /** 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() { for(int a=0;a<6;a++) { if (Settings.current.lines[a].same != -1) continue; PhoneLine pl = lines[a]; if (pl.sip == null) continue; if (!pl.sip.isRegistered()) continue; pl.sip.keepalive(); if (pl.talking) { if (pl.audioRTP != null) pl.audioRTP.keepalive(); if (pl.videoRTP != null) pl.videoRTP.keepalive(); } } } } /** TimerTask that reregisters all SIP connection after they expire (every 3600 seconds). */ public class ReRegisterExpires extends java.util.TimerTask { public void run() { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (Settings.current.lines[a].same != -1) continue; if (pl.sip == null) continue; if (pl.noregister) continue; pl.sip.reregister(); } registerRetries = 0; if (timerRegisterRetries != null) { timerRegisterRetries = new java.util.Timer(); timerRegisterRetries.schedule(new ReRegisterRetries(), 1000); } } } /** 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() { boolean again = false; if (registerRetries < 5) { for(int a=0;a<6;a++) { if (Settings.current.lines[a].same != -1) continue; PhoneLine pl = lines[a]; if (pl.sip == null) continue; if (pl.unauth) continue; if (pl.noregister) continue; if (!pl.sip.isRegistered()) { JFLog.log("warn:retry register on line:" + (a+1)); pl.sip.reregister(); again = true; } } registerRetries++; if (again) { timerRegisterRetries = new java.util.Timer(); timerRegisterRetries.schedule(new ReRegisterRetries(), 1000); return; } } for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == null) continue; if (pl.unauth) continue; if (pl.noregister) continue; if (!pl.sip.isRegistered()) { pl.unauth = true; //server not responding after 5 attempts to register pl.status = "Server not responding"; if (a == line) gui.updateLine(); } } timerRegisterRetries = null; } } /** Loads icons during startup. */ public void loadIcons() { ii = new ImageIcon[icons.length]; for(int a=0;a<icons.length;a++) { try { InputStream is = getClass().getClassLoader().getResourceAsStream(icons[a]); int len = is.available(); byte data[] = new byte[len]; is.read(data); is.close(); ii[a] = new ImageIcon(data); } catch (Exception e) { JFLog.log("err:loadIcons() Failed:" + e); BasePhone.unlockFile(); System.exit(0); } } } /** Checks for an online update on startup. */ public void checkVersion() { try { BufferedReader reader = new BufferedReader(new InputStreamReader( new URL("http://jphonelite.sourceforge.net/version" + (version.indexOf("beta") == -1 ? "" : "beta") + ".php").openStream())); final String line = reader.readLine(); if (line.equals(version)) {JFLog.log("version is up-to-date"); return;} JFLog.log("newer version is available : " + line); java.awt.EventQueue.invokeLater(new Runnable() { public void run() { JF.showMessage("Upgrade available", "A newer version of jphonelite is available! (v" + line + ")\r\nPlease goto http://jphonelite.sourceforge.net to download it"); } }); } catch (Exception e) { JFLog.log("err:unable to check for version update"); JFLog.log(e); } } /** Updates notification area test. */ public void updateIconTray() { if (icon == null) return; if (active) return; StringBuffer buf = new StringBuffer(); for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.incoming == true) { if (buf.length() > 0) buf.append("\r\n"); buf.append("\"" + pl.callerid + "\" " + pl.dial + " is on Line " + (a+1)); } } if (buf.length() > 0) { icon.displayMessage("Incoming Call(s)", buf.toString(), TrayIcon.MessageType.INFO); } else { //BUG? How do you hide a message if visible??? } } /** Toggles speaker phone mode. */ public void toggleSpeaker() { Settings.current.speakerMode = !Settings.current.speakerMode; gui.spk_setIcon(ii[Settings.current.speakerMode ? PIC_GREEN : PIC_GREY]); } /** Returns status of a line. */ public String getStatus(int a) { return lines[a].status; } /** 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_GSM)) 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 confirmation of a registration with server. */ public void onRegister(SIPClient sip, boolean status) { if (status) { //success for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip != sip) continue; if (pl.status.length() == 0) pl.status = "Ready (" + pl.user + ")"; pl.auth = true; if (line == -1) { gui.selectLine(a); } else { if (line == a) gui.updateLine(); } } sip.subscribe(sip.getUser(), "message-summary", Settings.current.sipexpires); //SUBSCRIBE to self for message-summary event (not needed with Asterisk but X-Lite does it) gui.onRegister(sip); } else { //failed for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == sip) { pl.status = "Unauthorized"; pl.unauth = true; if (line == a) gui.selectLine(-1); } } } } /** 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++) { PhoneLine pl = lines[a]; if ((pl.incall)&&(!pl.trying)) { if (pl.callid.equals(callid)) { pl.trying = true; pl.status = "Trying"; if (line == a) gui.updateLine(); } } } } /** SIPClientInterface.onRinging() : triggered when an INVITE returns status code 180 (RINGING). */ public void onRinging(SIPClient sip, String callid) { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if ((pl.incall)&&(!pl.ringing)) { if (pl.callid.equals(callid)) { pl.ringing = true; pl.status = "Ringing"; if (line == a) gui.updateLine(); } } } } /** SIPClientInterface.onSuccess() : triggered when an INVITE returns status code 200 (OK) or 183 (ringback). */ public void onSuccess(SIPClient sip, String callid, SDP sdp, boolean complete) { JFLog.log("onSuccess : streams=" + sdp.streams.length); for(int s=0;s<sdp.streams.length;s++) { JFLog.log("onSuccess : stream=" + sdp.streams[s].getType() + "," + sdp.streams[s].getMode() + "," + sdp.streams[s].content); SDP.Stream stream = sdp.streams[s]; for(int c=0;c<stream.codecs.length;c++) { JFLog.log("onSuccess : codecs[] = " + stream.codecs[c].name + ":" + stream.codecs[c].id); } } //is a line trying to do an invite or reinvite? for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (!pl.incall) continue; if (!pl.callid.equals(callid)) continue; if (!pl.talking) { pl.sdp = sdp; if (complete && reinvite(pl)) return; if (!callInviteSuccess(a, sdp)) return; if (complete) { //200 call complete pl.status = "Connected"; if (line == a) gui.updateLine(); pl.talking = true; pl.ringing = false; pl.ringback = false; } else { //183 call progress (ie: ringback tones) pl.status = "Ringing"; if (line == a) gui.updateLine(); pl.talking = true; //NOTE:Must send silent data via RTP or firewall will BLOCK inbound anyways pl.ringing = true; pl.ringback = true; } return; } else { if (pl.ringback && complete) { pl.status = "Connected"; if (line == a) gui.updateLine(); pl.ringback = false; pl.ringing = false; pl.sdp = sdp; if (reinvite(pl)) return; //??? could this cause inf loop ??? } } //update RTP data in case reINVITE changes them (or when making progress from 183 to 200) pl.sdp = sdp; change(a, sdp); return; } JFLog.log("err:onSuccess() for unknown call:" + callid); } /** SIPClientInterface.onBye() : triggered when server terminates a call. */ public void onBye(SIPClient sip, String callid) { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.incall) { if (pl.callid.equals(callid)) { endLine(a); updateIconTray(); } } } } /** SIPClientInterface.onInvite() : triggered when server send an INVITE to jphonelite. */ public int onInvite(SIPClient sip, String callid, String fromid, String fromnumber, SDP sdp) { //NOTE : onInvite() can not change codecs (use SIP.reaccept() to do that) if (sdp != null) { for(int s=0;s<sdp.streams.length;s++) { JFLog.log("onInvite : stream=" + sdp.streams[s].getType() + "," + sdp.streams[s].getMode() + "," + sdp.streams[s].content); SDP.Stream stream = sdp.streams[s]; for(int c=0;c<stream.codecs.length;c++) { JFLog.log("onInvite : codecs[] = " + stream.codecs[c].name + ":" + stream.codecs[c].id); } } } for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == sip) { if (!pl.auth) { pl.auth = true; //received a call on a line that failed to register (can happen if server was down briefly) } if (pl.callid.equals(callid)) { if (pl.talking) { //reINVITE 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 } return 180; //reply RINGING } if (pl.incall) continue; if (pl.incoming) continue; pl.dial = fromnumber; pl.callerid = fromid; if ((pl.callerid == null) || (pl.callerid.trim().length() == 0)) pl.callerid = "Unknown"; Settings.addCallLog(pl.dial); gui.updateRecentList(); pl.status = fromid + " is calling"; pl.incoming = true; pl.sdp = sdp; pl.callid = callid; pl.ringing = true; if (Settings.current.aa) { gui.selectLine(a); gui.updateLine(); call(); //this will send a reply return -1; //do NOT send a reply } else { if (line == a) gui.updateLine(); updateIconTray(); return 180; //reply RINGING } } } return 486; //reply BUSY } /** SIPClientInterface.onCancel() : triggered when server send a CANCEL request after an INVITE, or an error occured. */ public void onCancel(SIPClient sip, String callid, int code) { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.callid.equals(callid)) { endLine(a); pl.status = "Hungup (" + code + ")"; if (line == a) gui.updateLine(); updateIconTray(); } } } /** SIPClientInterface.onRefer() : triggered when the server accepts a transfer for processing (SIP code 202). */ public void onRefer(SIPClient sip, String callid) { //NOTE:SIP code 202 doesn't really tell you the transfer was successful, it just tells you the transfer is in progress. // see onNotify() event="refer" to determine if transfer was successful for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.callid.equals(callid)) { pl.status = "XFER Accepted"; if (line == a) gui.updateLine(); } } } /** SIPClientInterface.onNotify() : processes SIP:NOTIFY messages. */ public void onNotify(SIPClient sip, String callid, String event, String content) { // JFLog.log("notify()"); String contentLines[] = content.split("\r\n"); event = event.toLowerCase(); if (event.equals("message-summary")) { String msgwait = SIP.getHeader("Messages-Waiting:", contentLines); if (msgwait != null) { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == sip) { // JFLog.log("notify() line=" + a + ", msgwaiting = " + msgwaiting); pl.msgwaiting = msgwait.equalsIgnoreCase("yes"); } } } return; } if (event.equals("presence")) { JFLog.log("note:Presence:" + content); if (!content.startsWith("<?xml")) {JFLog.log("Not valid presence data (1)"); return;} content = content.replaceAll("\r", "").replaceAll("\n", ""); XML xml = new XML(); ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes()); if (!xml.read(bais)) {JFLog.log("Not valid presence data (2)"); return;} XML.XMLTag contact = xml.getTag(new String[] { "presence", "tuple", "contact" }); if (contact == null) {JFLog.log("Not valid presence data (3)"); return;} String fields[] = SIP.split("Unknown<" + contact.getContent() + ">"); if (fields == null) {JFLog.log("Not valid presence data (4)"); return;} XML.XMLTag status = xml.getTag(new String[] { "presence", "tuple", "status", "basic" }); if (status == null) {JFLog.log("Not valid presence data (5)"); return;} gui.setStatus(fields[1], fields[2], status.getContent().trim()); return; } String parts[] = event.split(";"); if (parts[0].equals("refer")) { int notifyLine = -1; for(int a=0;a<6;a++) { if (lines[a].callid.equals(callid)) { notifyLine = a; break; } } if (notifyLine == -1) {JFLog.log("Received NOTIFY:REFER that doesn't match any lines"); return;} parts = content.split(" "); //SIP/2.0 code desc int code = JF.atoi(parts[1]); switch (code) { case 100: //trying (not used by Asterisk) lines[notifyLine].status = "XFER Trying"; break; case 180: //ringing case 183: //ringing (not used by Asterisk) lines[notifyLine].status = "XFER Ringing"; break; case 200: //refer successful case 202: //accepted (not used by Asterisk) lines[notifyLine].status = "XFER Success"; break; case 404: //refer failed default: lines[notifyLine].status = "XFER Failed (" + code + ")"; break; } if (line == notifyLine) gui.updateLine(); return; } JFLog.log("Warning : unknown NOTIFY type : " + event); } /** SIPClientInterface.onAck() : triggered when server send an ACK to jphonelite. */ public void onAck(SIPClient sip, String callid, SDP sdp) { if (sdp == null) return; for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (pl.sip == sip) { if (!pl.rtpStarted) { //RFC 3665 section 3.6 - ACK provides SDP instead of INVITE pl.sdp = sdp; startRTPinbound(); } } } } /** Processes keyboard input. */ public boolean dispatchKeyEvent(KeyEvent e) { if (line == -1) return false; Object awtsrc = e.getSource(); if (!(awtsrc instanceof JComponent)) return false; JComponent src = (JComponent)awtsrc; if (src.getParent() != (Object)this) return false; // JFLog.log("KeyEvent : KeyCode=" + e.getKeyCode() + " KeyChar=" + e.getKeyChar() + " Mods=" + e.getModifiers() + " ID=" + e.getID()); int id = e.getID(); char ch = e.getKeyChar(); int cc = e.getKeyCode(); PhoneLine pl = lines[line]; switch (id) { case KeyEvent.KEY_TYPED: if ((ch >= '0') && (ch <= '9')) addDigit(ch); if (ch == '*') addDigit(ch); if (ch == '#') addDigit(ch); if (ch == '/') addDigit('#'); //for keypad usage break; case KeyEvent.KEY_PRESSED: if (pl.xfer) { if (cc == KeyEvent.VK_ESCAPE) {pl.dial = ""; doXfer();} if (ch == KeyEvent.VK_ENTER) doXfer(); break; } if ((ch >= '0') && (ch <= '9')) pressDigit(ch); switch (ch) { case '*': pressDigit(ch); break; case '#': pressDigit(ch); break; case '/': pressDigit('#'); break; //for keypad usage case KeyEvent.VK_ENTER: { if (!pl.incall) call(); else pressDigit('#'); break; } } switch (cc) { case KeyEvent.VK_BACK_SPACE: addDigit((char)cc); break; case KeyEvent.VK_ESCAPE: { if (!pl.incall) {pl.dial = ""; gui.updateLine();} else pressDigit('*'); break; } case KeyEvent.VK_F1: gui.selectLine(0); break; case KeyEvent.VK_F2: gui.selectLine(1); break; case KeyEvent.VK_F3: gui.selectLine(2); break; case KeyEvent.VK_F4: gui.selectLine(3); break; case KeyEvent.VK_F5: gui.selectLine(4); break; case KeyEvent.VK_F6: gui.selectLine(5); break; } break; case KeyEvent.KEY_RELEASED: if (pl.xfer) break; if ((ch >= '0') && (ch <= '9')) releaseDigit(ch); if (ch == '*') releaseDigit(ch); if (ch == '#') releaseDigit(ch); if (ch == '/') releaseDigit('#'); //for keypad usage if (ch == KeyEvent.VK_ENTER) { if (pl.incall) releaseDigit('#'); } if (cc == KeyEvent.VK_ESCAPE) releaseDigit('*'); break; } return false; //pass on as normal } //interface RTPInterface /** RTPInterface.rtpDigit() */ public void rtpDigit(RTPChannel rtp, char digit) {} /** RTPInterface.rtpSamples() */ public void rtpSamples(RTPChannel rtp) {} /** RTPInterface.rtpPacket() */ public void rtpPacket(RTPChannel rtp, byte data[], int off, int len) {} /** RTPInterface.rtcpPacket() */ public void rtcpPacket(RTPChannel rtp, byte data[], int off, int len) {} /** RTPInterface.rtpH263() */ public void rtpH263(RTPChannel rtp, byte data[], int off, int len) { rtp_h263_receive(rtp, data, off, len); } /** RTPInterface.rtpH263_1998() */ public void rtpH263_1998(RTPChannel rtp, byte data[], int off, int len) { rtp_h263_1998_receive(rtp, data, off, len); } /** RTPInterface.rtpH263() */ public void rtpH263_2000(RTPChannel rtp, byte data[], int off, int len) { rtp_h263_2000_receive(rtp, data, off, len); } /** RTPInterface.rtpH264() */ public void rtpH264(RTPChannel rtp, byte data[], int off, int len) { rtp_h264_receive(rtp, data, off, len); } /** RTPInterface.rtpVP8() */ public void rtpVP8(RTPChannel rtp, byte data[], int off, int len) { rtp_vp8_receive(rtp, data, off, len); } /** RTPInterface.rtpJPEG() */ public void rtpJPEG(RTPChannel rtp, byte data[], int off, int len) { rtp_jpeg_receive(rtp, data, off, len); } /** RTPInterface.rtpInactive() */ public void rtpInactive(RTPChannel rtp) { //find line this belongs to for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; if (!pl.talking) continue; for(int r=0;r<pl.audioRTP.channels.size();r++) { if (pl.audioRTP.channels.get(a) == rtp) { pl.sip.bye(pl.callid); endLine(a); pl.status = "Hangup (audio inactive)"; if (line == a) gui.updateLine(); return; } } } } /** ActionListener : actionPerformed() - for the SystemTray Icon actions. */ public void actionPerformed(ActionEvent e) { Object o = e.getSource(); if (o == exit) { unRegisterAll(); BasePhone.unlockFile(); System.exit(0); } if (o == show) { wc.setPanelVisible(); } } public void setLAF() { // LookAndFeel laf = UIManager.getLookAndFeel(); // JFLog.log("current laf=" + laf); try { UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); } catch (Exception e) { JFLog.log(e); } //only acceptable LAF // try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); } catch (Exception e) { JFLog.log(e); } //crud // try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel"); } catch (Exception e) { JFLog.log(e); } //crud // try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); } catch (Exception e) { JFLog.log(e); } //crud /* UIManager.LookAndFeelInfo infos[] = UIManager.getInstalledLookAndFeels(); for(int a=0;a<infos.length;a++) { JFLog.log("infos[] = " + infos[a]); } UIManager.setLookAndFeel(); */ } public void selectLine(String ln) { gui.selectLine(JF.atoi(ln)); } /* * These functions are for calling functions from outside the EDT. (ie: javascript) * and also preserve the applet's elevated security permissions (was not easy) * It seems like when an applet is called from javascript it's called within a lowered * security context, so I had to create a sort of RPC system to get around that. */ private static Method method; private static Object object; private static String param; private static Object lock = new Object(); private static Object lockReturn = new Object(); private static String retValue; private static Vector<String> funcs = new Vector<String>(); public void initRPC() { object = this; new RPCServer().start(); } public static class RPCServer extends Thread { public void run() { synchronized(lock) { while (true) { try {lock.wait();} catch (Exception e) {JFLog.log(e);} if (funcs.size() == 0) continue; String func = funcs.remove(0); if (func.startsWith("=")) { try { method = object.getClass().getMethod(func.substring(1)); EventQueue.invokeLater(new Runnable() { public void run() { try { retValue = (String)method.invoke(object); } catch (InvocationTargetException e) { JFLog.log(e.getCause()); } catch (Exception e) { JFLog.log(e); } try { synchronized(lockReturn) { lockReturn.notifyAll(); } } catch (Exception e) { JFLog.log(e); } } }); } catch (Exception e) { JFLog.log(e); } continue; } int idx = func.indexOf(","); if (idx == -1) { try { method = object.getClass().getMethod(func); EventQueue.invokeLater(new Runnable() { public void run() { try { method.invoke(object); } catch (InvocationTargetException e) { JFLog.log(e.getCause()); } catch (Exception e) { JFLog.log(e); } } }); } catch (Exception e) { JFLog.log(e); } } else { param = func.substring(idx+1); func = func.substring(0, idx); try { method = object.getClass().getMethod(func, String.class); EventQueue.invokeLater(new Runnable() { public void run() { try { method.invoke(object, param); } catch (InvocationTargetException e) { JFLog.log(e.getCause()); } catch (Exception e) { JFLog.log(e); } } }); } catch (Exception e) { JFLog.log(e); } } } } } } public void callEDT(String func) { synchronized(lock) { funcs.add(func); lock.notifyAll(); } } public void callEDT(String func, String param) { this.param = param; synchronized(lock) { funcs.add(func + "," + param); lock.notifyAll(); } } public String callEDTreturn(String func) { synchronized(lockReturn) { retValue = ""; synchronized(lock) { funcs.add("=" + func); lock.notifyAll(); } try { lockReturn.wait(); } catch (Exception e) {} return retValue; } } private synchronized int getlocalport() { int port = sipnext++; if (sipnext > sipmax) sipnext = sipmin; return port; } public void setSIPPortRange(int min, int max) { sipmin = min; sipmax = max; sipnext = min; } /** 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); } } public RemoteCamera addRemoteCamera(PhoneLine pl, RTPChannel channel) { RemoteCamera camera = new RemoteCamera(channel, pl.videoWindow); pl.remoteCameras.add(camera); camera.start(); return camera; } public void delRemoteCamera(PhoneLine pl, RemoteCamera camera) { pl.remoteCameras.remove(camera); camera.cancel(); } public RemoteCamera findRemoteCamera(RTPChannel channel) { for(int a=0;a<6;a++) { PhoneLine pl = lines[a]; synchronized(pl.remoteCamerasLock) { for(int b=0;b<pl.remoteCameras.size();b++) { if (pl.remoteCameras.get(b).channel == channel) { return pl.remoteCameras.get(b); } } } } return null; } private static byte crt[], privateKey[]; private static String fingerprintSHA256; protected void initDTLS() { char password[] = "password".toCharArray(); try { FileInputStream fis = new FileInputStream(JF.getUserPath() + "/.jphone.key"); KeyMgmt key = new KeyMgmt(); key.open(fis, password); fis.close(); crt = key.getCRT("jphonelite").getEncoded(); fingerprintSHA256 = KeyMgmt.fingerprintSHA256(crt); ArrayList<byte[]> chain = new ArrayList<byte[]>(); chain.add(crt); java.security.cert.Certificate root = key.getCRT("root"); if (root != null) { chain.add(root.getEncoded()); } privateKey = key.getKEY("jphonelite", password).getEncoded(); SRTPChannel.initDTLS(chain, privateKey, false); } catch(FileNotFoundException e) { //do nothing } catch(Exception e) { JFLog.log(e); } } //see ticket # 23 public void pressDigit(String digit) { pressDigit(digit.charAt(0)); } //see ticket # 23 public void releaseDigit(String digit) { releaseDigit(digit.charAt(0)); } private static JFLockFile lockFile = new JFLockFile(); public static boolean lockFile() { return lockFile.lock(JF.getUserPath() + "/.jphone.lck"); } public static void unlockFile() { lockFile.unlock(); } }