summaryrefslogtreecommitdiffstats
path: root/voip
diff options
context:
space:
mode:
Diffstat (limited to 'voip')
-rw-r--r--voip/java/android/net/rtp/AudioCodec.java56
-rw-r--r--voip/java/android/net/rtp/AudioGroup.java92
-rw-r--r--voip/java/android/net/rtp/AudioStream.java136
-rw-r--r--voip/java/android/net/rtp/RtpStream.java173
-rw-r--r--voip/java/android/net/sip/ISipService.aidl42
-rw-r--r--voip/java/android/net/sip/ISipSession.aidl147
-rw-r--r--voip/java/android/net/sip/ISipSessionListener.aidl126
-rw-r--r--voip/java/android/net/sip/SdpSessionDescription.java428
-rw-r--r--voip/java/android/net/sip/SessionDescription.aidl19
-rw-r--r--voip/java/android/net/sip/SessionDescription.java83
-rw-r--r--voip/java/android/net/sip/SipAudioCall.java303
-rw-r--r--voip/java/android/net/sip/SipAudioCallImpl.java703
-rw-r--r--voip/java/android/net/sip/SipManager.java496
-rw-r--r--voip/java/android/net/sip/SipProfile.aidl19
-rw-r--r--voip/java/android/net/sip/SipProfile.java401
-rw-r--r--voip/java/android/net/sip/SipRegistrationListener.java48
-rw-r--r--voip/java/android/net/sip/SipSessionAdapter.java67
-rw-r--r--voip/java/android/net/sip/SipSessionState.java66
-rw-r--r--voip/jni/rtp/Android.mk44
-rw-r--r--voip/jni/rtp/AudioCodec.cpp161
-rw-r--r--voip/jni/rtp/AudioCodec.h36
-rw-r--r--voip/jni/rtp/AudioGroup.cpp933
-rw-r--r--voip/jni/rtp/RtpStream.cpp126
-rw-r--r--voip/jni/rtp/rtp_jni.cpp32
-rw-r--r--voip/jni/rtp/util.cpp61
25 files changed, 4798 insertions, 0 deletions
diff --git a/voip/java/android/net/rtp/AudioCodec.java b/voip/java/android/net/rtp/AudioCodec.java
new file mode 100644
index 0000000..89e6aa9
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioCodec.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.rtp;
+
+/** @hide */
+public class AudioCodec {
+ public static final AudioCodec ULAW = new AudioCodec("PCMU", 8000, 160, 0);
+ public static final AudioCodec ALAW = new AudioCodec("PCMA", 8000, 160, 8);
+
+ /**
+ * Returns system supported codecs.
+ */
+ public static AudioCodec[] getSystemSupportedCodecs() {
+ return new AudioCodec[] {AudioCodec.ULAW, AudioCodec.ALAW};
+ }
+
+ /**
+ * Returns the codec instance if it is supported by the system.
+ *
+ * @param name name of the codec
+ * @return the matched codec or null if the codec name is not supported by
+ * the system
+ */
+ public static AudioCodec getSystemSupportedCodec(String name) {
+ for (AudioCodec codec : getSystemSupportedCodecs()) {
+ if (codec.name.equals(name)) return codec;
+ }
+ return null;
+ }
+
+ public final String name;
+ public final int sampleRate;
+ public final int sampleCount;
+ public final int defaultType;
+
+ private AudioCodec(String name, int sampleRate, int sampleCount, int defaultType) {
+ this.name = name;
+ this.sampleRate = sampleRate;
+ this.sampleCount = sampleCount;
+ this.defaultType = defaultType;
+ }
+}
diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java
new file mode 100644
index 0000000..37cc121
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioGroup.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.rtp;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ */
+/** @hide */
+public class AudioGroup {
+ public static final int MODE_ON_HOLD = 0;
+ public static final int MODE_MUTED = 1;
+ public static final int MODE_NORMAL = 2;
+ public static final int MODE_EC_ENABLED = 3;
+
+ private final Map<AudioStream, Integer> mStreams;
+ private int mMode = MODE_ON_HOLD;
+
+ private int mNative;
+ static {
+ System.loadLibrary("rtp_jni");
+ }
+
+ public AudioGroup() {
+ mStreams = new HashMap<AudioStream, Integer>();
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ public synchronized native void setMode(int mode);
+
+ synchronized void add(AudioStream stream, AudioCodec codec, int codecType, int dtmfType) {
+ if (!mStreams.containsKey(stream)) {
+ try {
+ int socket = stream.dup();
+ add(stream.getMode(), socket,
+ stream.getRemoteAddress().getHostAddress(), stream.getRemotePort(),
+ codec.name, codec.sampleRate, codec.sampleCount, codecType, dtmfType);
+ mStreams.put(stream, socket);
+ } catch (NullPointerException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private native void add(int mode, int socket, String remoteAddress, int remotePort,
+ String codecName, int sampleRate, int sampleCount, int codecType, int dtmfType);
+
+ synchronized void remove(AudioStream stream) {
+ Integer socket = mStreams.remove(stream);
+ if (socket != null) {
+ remove(socket);
+ }
+ }
+
+ private native void remove(int socket);
+
+ /**
+ * Sends a DTMF digit to every {@link AudioStream} in this group. Currently
+ * only event {@code 0} to {@code 15} are supported.
+ *
+ * @throws IllegalArgumentException if the event is invalid.
+ */
+ public native synchronized void sendDtmf(int event);
+
+ public synchronized void reset() {
+ remove(-1);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ reset();
+ super.finalize();
+ }
+}
diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java
new file mode 100644
index 0000000..a955fd2
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioStream.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.rtp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+
+/**
+ * AudioStream represents a RTP stream carrying audio payloads.
+ */
+/** @hide */
+public class AudioStream extends RtpStream {
+ private AudioCodec mCodec;
+ private int mCodecType = -1;
+ private int mDtmfType = -1;
+ private AudioGroup mGroup;
+
+ /**
+ * Creates an AudioStream on the given local address. Note that the local
+ * port is assigned automatically to conform with RFC 3550.
+ *
+ * @param address The network address of the local host to bind to.
+ * @throws SocketException if the address cannot be bound or a problem
+ * occurs during binding.
+ */
+ public AudioStream(InetAddress address) throws SocketException {
+ super(address);
+ }
+
+ /**
+ * Returns {@code true} if the stream already joined an {@link AudioGroup}.
+ */
+ @Override
+ public final boolean isBusy() {
+ return mGroup != null;
+ }
+
+ /**
+ * Returns the joined {@link AudioGroup}.
+ */
+ public AudioGroup getAudioGroup() {
+ return mGroup;
+ }
+
+ /**
+ * Joins an {@link AudioGroup}. Each stream can join only one group at a
+ * time. The group can be changed by passing a different one or removed
+ * by calling this method with {@code null}.
+ *
+ * @param group The AudioGroup to join or {@code null} to leave.
+ * @throws IllegalStateException if the stream is not properly configured.
+ * @see AudioGroup
+ */
+ public void join(AudioGroup group) {
+ if (mGroup == group) {
+ return;
+ }
+ if (mGroup != null) {
+ mGroup.remove(this);
+ mGroup = null;
+ }
+ if (group != null) {
+ group.add(this, mCodec, mCodecType, mDtmfType);
+ mGroup = group;
+ }
+ }
+
+ /**
+ * Sets the {@link AudioCodec} and its RTP payload type. According to RFC
+ * 3551, the type must be in the range of 0 and 127, where 96 and above are
+ * dynamic types. For codecs with static mappings (non-negative
+ * {@link AudioCodec#defaultType}), assigning a different non-dynamic type
+ * is disallowed.
+ *
+ * @param codec The AudioCodec to be used.
+ * @param type The RTP payload type.
+ * @throws IllegalArgumentException if the type is invalid or used by DTMF.
+ * @throws IllegalStateException if the stream is busy.
+ */
+ public void setCodec(AudioCodec codec, int type) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ if (type < 0 || type > 127 || (type != codec.defaultType && type < 96)) {
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (type == mDtmfType) {
+ throw new IllegalArgumentException("The type is used by DTMF");
+ }
+ mCodec = codec;
+ mCodecType = type;
+ }
+
+ /**
+ * Sets the RTP payload type for dual-tone multi-frequency (DTMF) digits.
+ * The primary usage is to send digits to the remote gateway to perform
+ * certain tasks, such as second-stage dialing. According to RFC 2833, the
+ * RTP payload type for DTMF is assigned dynamically, so it must be in the
+ * range of 96 and 127. One can use {@code -1} to disable DTMF and free up
+ * the previous assigned value. This method cannot be called when the stream
+ * already joined an {@link AudioGroup}.
+ *
+ * @param type The RTP payload type to be used or {@code -1} to disable it.
+ * @throws IllegalArgumentException if the type is invalid or used by codec.
+ * @throws IllegalStateException if the stream is busy.
+ * @see AudioGroup#sendDtmf(int)
+ */
+ public void setDtmfType(int type) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ if (type != -1) {
+ if (type < 96 || type > 127) {
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (type == mCodecType) {
+ throw new IllegalArgumentException("The type is used by codec");
+ }
+ }
+ mDtmfType = type;
+ }
+}
diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java
new file mode 100644
index 0000000..ef5ca17
--- /dev/null
+++ b/voip/java/android/net/rtp/RtpStream.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.rtp;
+
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketException;
+
+/**
+ * RtpStream represents a base class of media streams running over
+ * Real-time Transport Protocol (RTP).
+ */
+/** @hide */
+public class RtpStream {
+ public static final int MODE_NORMAL = 0;
+ public static final int MODE_SEND_ONLY = 1;
+ public static final int MODE_RECEIVE_ONLY = 2;
+
+ private final InetAddress mLocalAddress;
+ private final int mLocalPort;
+
+ private InetAddress mRemoteAddress;
+ private int mRemotePort = -1;
+ private int mMode = MODE_NORMAL;
+
+ private int mNative;
+ static {
+ System.loadLibrary("rtp_jni");
+ }
+
+ /**
+ * Creates a RtpStream on the given local address. Note that the local
+ * port is assigned automatically to conform with RFC 3550.
+ *
+ * @param address The network address of the local host to bind to.
+ * @throws SocketException if the address cannot be bound or a problem
+ * occurs during binding.
+ */
+ RtpStream(InetAddress address) throws SocketException {
+ mLocalPort = create(address.getHostAddress());
+ mLocalAddress = address;
+ }
+
+ private native int create(String address) throws SocketException;
+
+ /**
+ * Returns the network address of the local host.
+ */
+ public InetAddress getLocalAddress() {
+ return mLocalAddress;
+ }
+
+ /**
+ * Returns the network port of the local host.
+ */
+ public int getLocalPort() {
+ return mLocalPort;
+ }
+
+ /**
+ * Returns the network address of the remote host or {@code null} if the
+ * stream is not associated.
+ */
+ public InetAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /**
+ * Returns the network port of the remote host or {@code -1} if the stream
+ * is not associated.
+ */
+ public int getRemotePort() {
+ return mRemotePort;
+ }
+
+ /**
+ * Returns {@code true} if the stream is busy. This method is intended to be
+ * overridden by subclasses.
+ */
+ public boolean isBusy() {
+ return false;
+ }
+
+ /**
+ * Returns the current mode. The initial mode is {@link #MODE_NORMAL}.
+ */
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * Changes the current mode. It must be one of {@link #MODE_NORMAL},
+ * {@link #MODE_SEND_ONLY}, and {@link #MODE_RECEIVE_ONLY}.
+ *
+ * @param mode The mode to change to.
+ * @throws IllegalArgumentException if the mode is invalid.
+ * @throws IllegalStateException if the stream is busy.
+ * @see #isBusy()
+ */
+ public void setMode(int mode) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ if (mode != MODE_NORMAL && mode != MODE_SEND_ONLY && mode != MODE_RECEIVE_ONLY) {
+ throw new IllegalArgumentException("Invalid mode");
+ }
+ mMode = mode;
+ }
+
+ /**
+ * Associates with a remote host.
+ *
+ * @param address The network address of the remote host.
+ * @param port The network port of the remote host.
+ * @throws IllegalArgumentException if the address is not supported or the
+ * port is invalid.
+ * @throws IllegalStateException if the stream is busy.
+ * @see #isBusy()
+ */
+ public void associate(InetAddress address, int port) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ if (!(address instanceof Inet4Address && mLocalAddress instanceof Inet4Address) &&
+ !(address instanceof Inet6Address && mLocalAddress instanceof Inet6Address)) {
+ throw new IllegalArgumentException("Unsupported address");
+ }
+ if (port < 0 || port > 65535) {
+ throw new IllegalArgumentException("Invalid port");
+ }
+ mRemoteAddress = address;
+ mRemotePort = port;
+ }
+
+ synchronized native int dup();
+
+ /**
+ * Releases allocated resources. The stream becomes inoperable after calling
+ * this method.
+ *
+ * @throws IllegalStateException if the stream is busy.
+ * @see #isBusy()
+ */
+ public void release() {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ close();
+ }
+
+ private synchronized native void close();
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+}
diff --git a/voip/java/android/net/sip/ISipService.aidl b/voip/java/android/net/sip/ISipService.aidl
new file mode 100644
index 0000000..6c68213
--- /dev/null
+++ b/voip/java/android/net/sip/ISipService.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipProfile;
+
+/**
+ * {@hide}
+ */
+interface ISipService {
+ void open(in SipProfile localProfile);
+ void open3(in SipProfile localProfile,
+ String incomingCallBroadcastAction,
+ in ISipSessionListener listener);
+ void close(in String localProfileUri);
+ boolean isOpened(String localProfileUri);
+ boolean isRegistered(String localProfileUri);
+ void setRegistrationListener(String localProfileUri,
+ ISipSessionListener listener);
+
+ ISipSession createSession(in SipProfile localProfile,
+ in ISipSessionListener listener);
+ ISipSession getPendingSession(String callId);
+
+ SipProfile[] getListOfProfiles();
+}
diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl
new file mode 100644
index 0000000..fbcb056
--- /dev/null
+++ b/voip/java/android/net/sip/ISipSession.aidl
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SessionDescription;
+import android.net.sip.SipProfile;
+
+/**
+ * A SIP session that is associated with a SIP dialog or a transaction
+ * (e.g., registration) not within a dialog.
+ * @hide
+ */
+interface ISipSession {
+ /**
+ * Gets the IP address of the local host on which this SIP session runs.
+ *
+ * @return the IP address of the local host
+ */
+ String getLocalIp();
+
+ /**
+ * Gets the SIP profile that this session is associated with.
+ *
+ * @return the SIP profile that this session is associated with
+ */
+ SipProfile getLocalProfile();
+
+ /**
+ * Gets the SIP profile that this session is connected to. Only available
+ * when the session is associated with a SIP dialog.
+ *
+ * @return the SIP profile that this session is connected to
+ */
+ SipProfile getPeerProfile();
+
+ /**
+ * Gets the session state. The value returned must be one of the states in
+ * {@link SipSessionState}. One may convert it to {@link SipSessionState} by
+ * <code>
+ * Enum.valueOf(SipSessionState.class, session.getState());
+ * </code>
+ *
+ * @return the session state
+ */
+ String getState();
+
+ /**
+ * Checks if the session is in a call.
+ *
+ * @return true if the session is in a call
+ */
+ boolean isInCall();
+
+ /**
+ * Gets the call ID of the session.
+ *
+ * @return the call ID
+ */
+ String getCallId();
+
+
+ /**
+ * Sets the listener to listen to the session events. A {@link ISipSession}
+ * can only hold one listener at a time. Subsequent calls to this method
+ * override the previous listener.
+ *
+ * @param listener to listen to the session events of this object
+ */
+ void setListener(in ISipSessionListener listener);
+
+
+ /**
+ * Performs registration to the server specified by the associated local
+ * profile. The session listener is called back upon success or failure of
+ * registration. The method is only valid to call when the session state is
+ * in {@link SipSessionState#READY_TO_CALL}.
+ *
+ * @param duration duration in second before the registration expires
+ * @see ISipSessionListener
+ */
+ void register(int duration);
+
+ /**
+ * Performs unregistration to the server specified by the associated local
+ * profile. Unregistration is technically the same as registration with zero
+ * expiration duration. The session listener is called back upon success or
+ * failure of unregistration. The method is only valid to call when the
+ * session state is in {@link SipSessionState#READY_TO_CALL}.
+ *
+ * @see ISipSessionListener
+ */
+ void unregister();
+
+ /**
+ * Initiates a call to the specified profile. The session listener is called
+ * back upon defined session events. The method is only valid to call when
+ * the session state is in {@link SipSessionState#READY_TO_CALL}.
+ *
+ * @param callee the SIP profile to make the call to
+ * @param sessionDescription the session description of this call
+ * @see ISipSessionListener
+ */
+ void makeCall(in SipProfile callee,
+ in SessionDescription sessionDescription);
+
+ /**
+ * Answers an incoming call with the specified session description. The
+ * method is only valid to call when the session state is in
+ * {@link SipSessionState#INCOMING_CALL}.
+ *
+ * @param sessionDescription the session description to answer this call
+ */
+ void answerCall(in SessionDescription sessionDescription);
+
+ /**
+ * Ends an established call, terminates an outgoing call or rejects an
+ * incoming call. The method is only valid to call when the session state is
+ * in {@link SipSessionState#IN_CALL},
+ * {@link SipSessionState#INCOMING_CALL},
+ * {@link SipSessionState#OUTGOING_CALL} or
+ * {@link SipSessionState#OUTGOING_CALL_RING_BACK}.
+ */
+ void endCall();
+
+ /**
+ * Changes the session description during a call. The method is only valid
+ * to call when the session state is in {@link SipSessionState#IN_CALL}.
+ *
+ * @param sessionDescription the new session description
+ */
+ void changeCall(in SessionDescription sessionDescription);
+}
diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl
new file mode 100644
index 0000000..8570958
--- /dev/null
+++ b/voip/java/android/net/sip/ISipSessionListener.aidl
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.SipProfile;
+
+/**
+ * Listener class to listen to SIP session events.
+ * @hide
+ */
+interface ISipSessionListener {
+ /**
+ * Called when an INVITE request is sent to initiate a new call.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ void onCalling(in ISipSession session);
+
+ /**
+ * Called when an INVITE request is received.
+ *
+ * @param session the session object that carries out the transaction
+ * @param caller the SIP profile of the caller
+ * @param sessionDescription the caller's session description
+ */
+ void onRinging(in ISipSession session, in SipProfile caller,
+ in byte[] sessionDescription);
+
+ /**
+ * Called when a RINGING response is received for the INVITE request sent
+ *
+ * @param session the session object that carries out the transaction
+ */
+ void onRingingBack(in ISipSession session);
+
+ /**
+ * Called when the session is established.
+ *
+ * @param session the session object that is associated with the dialog
+ * @param sessionDescription the peer's session description
+ */
+ void onCallEstablished(in ISipSession session,
+ in byte[] sessionDescription);
+
+ /**
+ * Called when the session is terminated.
+ *
+ * @param session the session object that is associated with the dialog
+ */
+ void onCallEnded(in ISipSession session);
+
+ /**
+ * Called when the peer is busy during session initialization.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ void onCallBusy(in ISipSession session);
+
+ /**
+ * Called when an error occurs during session initialization and
+ * termination.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorClass name of the exception class
+ * @param errorMessage error message
+ */
+ void onError(in ISipSession session, String errorClass,
+ String errorMessage);
+
+ /**
+ * Called when an error occurs during session modification negotiation.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorClass name of the exception class
+ * @param errorMessage error message
+ */
+ void onCallChangeFailed(in ISipSession session, String errorClass,
+ String errorMessage);
+
+ /**
+ * Called when a registration request is sent.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ void onRegistering(in ISipSession session);
+
+ /**
+ * Called when registration is successfully done.
+ *
+ * @param session the session object that carries out the transaction
+ * @param duration duration in second before the registration expires
+ */
+ void onRegistrationDone(in ISipSession session, int duration);
+
+ /**
+ * Called when the registration fails.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorClass name of the exception class
+ * @param errorMessage error message
+ */
+ void onRegistrationFailed(in ISipSession session, String errorClass,
+ String errorMessage);
+
+ /**
+ * Called when the registration gets timed out.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ void onRegistrationTimeout(in ISipSession session);
+}
diff --git a/voip/java/android/net/sip/SdpSessionDescription.java b/voip/java/android/net/sip/SdpSessionDescription.java
new file mode 100644
index 0000000..0c29935
--- /dev/null
+++ b/voip/java/android/net/sip/SdpSessionDescription.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import gov.nist.javax.sdp.SessionDescriptionImpl;
+import gov.nist.javax.sdp.fields.AttributeField;
+import gov.nist.javax.sdp.fields.ConnectionField;
+import gov.nist.javax.sdp.fields.MediaField;
+import gov.nist.javax.sdp.fields.OriginField;
+import gov.nist.javax.sdp.fields.ProtoVersionField;
+import gov.nist.javax.sdp.fields.SessionNameField;
+import gov.nist.javax.sdp.fields.TimeField;
+import gov.nist.javax.sdp.parser.SDPAnnounceParser;
+
+import android.util.Log;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+import javax.sdp.Connection;
+import javax.sdp.MediaDescription;
+import javax.sdp.SdpException;
+
+/**
+ * A session description that follows SDP (Session Description Protocol).
+ * Refer to <a href="http://tools.ietf.org/html/rfc4566">RFC 4566</a>.
+ * @hide
+ */
+public class SdpSessionDescription extends SessionDescription {
+ private static final String TAG = "SDP";
+ private static final String AUDIO = "audio";
+ private static final String RTPMAP = "rtpmap";
+ private static final String PTIME = "ptime";
+ private static final String SENDONLY = "sendonly";
+ private static final String RECVONLY = "recvonly";
+ private static final String INACTIVE = "inactive";
+
+ private SessionDescriptionImpl mSessionDescription;
+
+ /**
+ * The audio codec information parsed from "rtpmap".
+ */
+ public static class AudioCodec {
+ public final int payloadType;
+ public final String name;
+ public final int sampleRate;
+ public final int sampleCount;
+
+ public AudioCodec(int payloadType, String name, int sampleRate,
+ int sampleCount) {
+ this.payloadType = payloadType;
+ this.name = name;
+ this.sampleRate = sampleRate;
+ this.sampleCount = sampleCount;
+ }
+ }
+
+ /**
+ * The builder class used to create an {@link SdpSessionDescription} object.
+ */
+ public static class Builder {
+ private SdpSessionDescription mSdp = new SdpSessionDescription();
+ private SessionDescriptionImpl mSessionDescription;
+
+ public Builder(String sessionName) throws SdpException {
+ mSessionDescription = new SessionDescriptionImpl();
+ mSdp.mSessionDescription = mSessionDescription;
+ try {
+ ProtoVersionField proto = new ProtoVersionField();
+ proto.setVersion(0);
+ mSessionDescription.addField(proto);
+
+ TimeField time = new TimeField();
+ time.setZero();
+ mSessionDescription.addField(time);
+
+ SessionNameField session = new SessionNameField();
+ session.setValue(sessionName);
+ mSessionDescription.addField(session);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ }
+
+ public Builder setConnectionInfo(String networkType, String addressType,
+ String addr) throws SdpException {
+ try {
+ ConnectionField connection = new ConnectionField();
+ connection.setNetworkType(networkType);
+ connection.setAddressType(addressType);
+ connection.setAddress(addr);
+ mSessionDescription.addField(connection);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ return this;
+ }
+
+ public Builder setOrigin(SipProfile user, long sessionId,
+ long sessionVersion, String networkType, String addressType,
+ String address) throws SdpException {
+ try {
+ OriginField origin = new OriginField();
+ origin.setUsername(user.getUserName());
+ origin.setSessionId(sessionId);
+ origin.setSessionVersion(sessionVersion);
+ origin.setAddressType(addressType);
+ origin.setNetworkType(networkType);
+ origin.setAddress(address);
+ mSessionDescription.addField(origin);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ return this;
+ }
+
+ public Builder addMedia(String media, int port, int numPorts,
+ String transport, Integer... types) throws SdpException {
+ MediaField field = new MediaField();
+ Vector<Integer> typeVector = new Vector<Integer>();
+ Collections.addAll(typeVector, types);
+ try {
+ field.setMediaType(media);
+ field.setMediaPort(port);
+ field.setPortCount(numPorts);
+ field.setProtocol(transport);
+ field.setMediaFormats(typeVector);
+ mSessionDescription.addField(field);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ return this;
+ }
+
+ public Builder addMediaAttribute(String type, String name, String value)
+ throws SdpException {
+ try {
+ MediaDescription md = mSdp.getMediaDescription(type);
+ if (md == null) {
+ throw new SdpException("Should add media first!");
+ }
+ AttributeField attribute = new AttributeField();
+ attribute.setName(name);
+ attribute.setValueAllowNull(value);
+ mSessionDescription.addField(attribute);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ return this;
+ }
+
+ public Builder addSessionAttribute(String name, String value)
+ throws SdpException {
+ try {
+ AttributeField attribute = new AttributeField();
+ attribute.setName(name);
+ attribute.setValueAllowNull(value);
+ mSessionDescription.addField(attribute);
+ } catch (Exception e) {
+ throwSdpException(e);
+ }
+ return this;
+ }
+
+ private void throwSdpException(Exception e) throws SdpException {
+ if (e instanceof SdpException) {
+ throw (SdpException) e;
+ } else {
+ throw new SdpException(e.toString(), e);
+ }
+ }
+
+ public SdpSessionDescription build() {
+ return mSdp;
+ }
+ }
+
+ private SdpSessionDescription() {
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param sdpString an SDP session description to parse
+ */
+ public SdpSessionDescription(String sdpString) throws SdpException {
+ try {
+ mSessionDescription = new SDPAnnounceParser(sdpString).parse();
+ } catch (ParseException e) {
+ throw new SdpException(e.toString(), e);
+ }
+ verify();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param content a raw SDP session description to parse
+ */
+ public SdpSessionDescription(byte[] content) throws SdpException {
+ this(new String(content));
+ }
+
+ private void verify() throws SdpException {
+ // make sure the syntax is correct over the fields we're interested in
+ Vector<MediaDescription> descriptions = (Vector<MediaDescription>)
+ mSessionDescription.getMediaDescriptions(false);
+ for (MediaDescription md : descriptions) {
+ md.getMedia().getMediaPort();
+ Connection connection = md.getConnection();
+ if (connection != null) connection.getAddress();
+ md.getMedia().getFormats();
+ }
+ Connection connection = mSessionDescription.getConnection();
+ if (connection != null) connection.getAddress();
+ }
+
+ /**
+ * Gets the connection address of the media.
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return the media connection address of the peer
+ */
+ public String getPeerMediaAddress(String type) {
+ try {
+ MediaDescription md = getMediaDescription(type);
+ Connection connection = md.getConnection();
+ if (connection == null) {
+ connection = mSessionDescription.getConnection();
+ }
+ return ((connection == null) ? null : connection.getAddress());
+ } catch (SdpException e) {
+ // should not occur
+ return null;
+ }
+ }
+
+ /**
+ * Gets the connection port number of the media.
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return the media connection port number of the peer
+ */
+ public int getPeerMediaPort(String type) {
+ try {
+ MediaDescription md = getMediaDescription(type);
+ return md.getMedia().getMediaPort();
+ } catch (SdpException e) {
+ // should not occur
+ return -1;
+ }
+ }
+
+ private boolean containsAttribute(String type, String name) {
+ if (name == null) return false;
+ MediaDescription md = getMediaDescription(type);
+ Vector<AttributeField> v = (Vector<AttributeField>)
+ md.getAttributeFields();
+ for (AttributeField field : v) {
+ if (name.equals(field.getAttribute().getName())) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the media is "sendonly".
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return true if the media is "sendonly"
+ */
+ public boolean isSendOnly(String type) {
+ boolean answer = containsAttribute(type, SENDONLY);
+ Log.d(TAG, " sendonly? " + answer);
+ return answer;
+ }
+
+ /**
+ * Checks if the media is "recvonly".
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return true if the media is "recvonly"
+ */
+ public boolean isReceiveOnly(String type) {
+ boolean answer = containsAttribute(type, RECVONLY);
+ Log.d(TAG, " recvonly? " + answer);
+ return answer;
+ }
+
+ /**
+ * Checks if the media is in sending; i.e., not "recvonly" and not
+ * "inactive".
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return true if the media is sending
+ */
+ public boolean isSending(String type) {
+ boolean answer = !containsAttribute(type, RECVONLY)
+ && !containsAttribute(type, INACTIVE);
+
+ Log.d(TAG, " sending? " + answer);
+ return answer;
+ }
+
+ /**
+ * Checks if the media is in receiving; i.e., not "sendonly" and not
+ * "inactive".
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return true if the media is receiving
+ */
+ public boolean isReceiving(String type) {
+ boolean answer = !containsAttribute(type, SENDONLY)
+ && !containsAttribute(type, INACTIVE);
+ Log.d(TAG, " receiving? " + answer);
+ return answer;
+ }
+
+ private AudioCodec parseAudioCodec(String rtpmap, int ptime) {
+ String[] ss = rtpmap.split(" ");
+ int payloadType = Integer.parseInt(ss[0]);
+
+ ss = ss[1].split("/");
+ String name = ss[0];
+ int sampleRate = Integer.parseInt(ss[1]);
+ int channelCount = 1;
+ if (ss.length > 2) channelCount = Integer.parseInt(ss[2]);
+ int sampleCount = sampleRate / (1000 / ptime) * channelCount;
+ return new AudioCodec(payloadType, name, sampleRate, sampleCount);
+ }
+
+ /**
+ * Gets the list of audio codecs in this session description.
+ *
+ * @return the list of audio codecs in this session description
+ */
+ public List<AudioCodec> getAudioCodecs() {
+ MediaDescription md = getMediaDescription(AUDIO);
+ if (md == null) return new ArrayList<AudioCodec>();
+
+ // FIXME: what happens if ptime is missing
+ int ptime = 20;
+ try {
+ String value = md.getAttribute(PTIME);
+ if (value != null) ptime = Integer.parseInt(value);
+ } catch (Throwable t) {
+ Log.w(TAG, "getCodecs(): ignored: " + t);
+ }
+
+ List<AudioCodec> codecs = new ArrayList<AudioCodec>();
+ Vector<AttributeField> v = (Vector<AttributeField>)
+ md.getAttributeFields();
+ for (AttributeField field : v) {
+ try {
+ if (RTPMAP.equals(field.getName())) {
+ AudioCodec codec = parseAudioCodec(field.getValue(), ptime);
+ if (codec != null) codecs.add(codec);
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "getCodecs(): ignored: " + t);
+ }
+ }
+ return codecs;
+ }
+
+ /**
+ * Gets the media description of the specified type.
+ *
+ * @param type the media type; e.g., "AUDIO"
+ * @return the media description of the specified type
+ */
+ public MediaDescription getMediaDescription(String type) {
+ MediaDescription[] all = getMediaDescriptions();
+ if ((all == null) || (all.length == 0)) return null;
+ for (MediaDescription md : all) {
+ String t = md.getMedia().getMedia();
+ if (t.equalsIgnoreCase(type)) return md;
+ }
+ return null;
+ }
+
+ /**
+ * Gets all the media descriptions in this session description.
+ *
+ * @return all the media descriptions in this session description
+ */
+ public MediaDescription[] getMediaDescriptions() {
+ try {
+ Vector<MediaDescription> descriptions = (Vector<MediaDescription>)
+ mSessionDescription.getMediaDescriptions(false);
+ MediaDescription[] all = new MediaDescription[descriptions.size()];
+ return descriptions.toArray(all);
+ } catch (SdpException e) {
+ Log.e(TAG, "getMediaDescriptions", e);
+ }
+ return null;
+ }
+
+ @Override
+ public String getType() {
+ return "sdp";
+ }
+
+ @Override
+ public byte[] getContent() {
+ return mSessionDescription.toString().getBytes();
+ }
+
+ @Override
+ public String toString() {
+ return mSessionDescription.toString();
+ }
+}
diff --git a/voip/java/android/net/sip/SessionDescription.aidl b/voip/java/android/net/sip/SessionDescription.aidl
new file mode 100644
index 0000000..a120d16
--- /dev/null
+++ b/voip/java/android/net/sip/SessionDescription.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+parcelable SessionDescription;
diff --git a/voip/java/android/net/sip/SessionDescription.java b/voip/java/android/net/sip/SessionDescription.java
new file mode 100644
index 0000000..d476f0b
--- /dev/null
+++ b/voip/java/android/net/sip/SessionDescription.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Abstract class of a session description.
+ * @hide
+ */
+public abstract class SessionDescription implements Parcelable {
+ /** @hide */
+ public static final Parcelable.Creator<SessionDescription> CREATOR =
+ new Parcelable.Creator<SessionDescription>() {
+ public SessionDescription createFromParcel(Parcel in) {
+ return new SessionDescriptionImpl(in);
+ }
+
+ public SessionDescription[] newArray(int size) {
+ return new SessionDescriptionImpl[size];
+ }
+ };
+
+ /**
+ * Gets the type of the session description; e.g., "SDP".
+ *
+ * @return the session description type
+ */
+ public abstract String getType();
+
+ /**
+ * Gets the raw content of the session description.
+ *
+ * @return the content of the session description
+ */
+ public abstract byte[] getContent();
+
+ /** @hide */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(getType());
+ out.writeByteArray(getContent());
+ }
+
+ /** @hide */
+ public int describeContents() {
+ return 0;
+ }
+
+ private static class SessionDescriptionImpl extends SessionDescription {
+ private String mType;
+ private byte[] mContent;
+
+ SessionDescriptionImpl(Parcel in) {
+ mType = in.readString();
+ mContent = in.createByteArray();
+ }
+
+ @Override
+ public String getType() {
+ return mType;
+ }
+
+ @Override
+ public byte[] getContent() {
+ return mContent;
+ }
+ }
+}
diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java
new file mode 100644
index 0000000..f4be839
--- /dev/null
+++ b/voip/java/android/net/sip/SipAudioCall.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.os.Message;
+
+import javax.sip.SipException;
+
+/**
+ * Interface for making audio calls over SIP.
+ * @hide
+ */
+public interface SipAudioCall {
+ /** Listener class for all event callbacks. */
+ public interface Listener {
+ /**
+ * Called when the call object is ready to make another call.
+ *
+ * @param call the call object that is ready to make another call
+ */
+ void onReadyToCall(SipAudioCall call);
+
+ /**
+ * Called when a request is sent out to initiate a new call.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onCalling(SipAudioCall call);
+
+ /**
+ * Called when a new call comes in.
+ *
+ * @param call the call object that carries out the audio call
+ * @param caller the SIP profile of the caller
+ */
+ void onRinging(SipAudioCall call, SipProfile caller);
+
+ /**
+ * Called when a RINGING response is received for the INVITE request sent
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onRingingBack(SipAudioCall call);
+
+ /**
+ * Called when the session is established.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onCallEstablished(SipAudioCall call);
+
+ /**
+ * Called when the session is terminated.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onCallEnded(SipAudioCall call);
+
+ /**
+ * Called when the peer is busy during session initialization.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onCallBusy(SipAudioCall call);
+
+ /**
+ * Called when the call is on hold.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ void onCallHeld(SipAudioCall call);
+
+ /**
+ * Called when an error occurs.
+ *
+ * @param call the call object that carries out the audio call
+ * @param errorMessage error message
+ */
+ void onError(SipAudioCall call, String errorMessage);
+ }
+
+ /**
+ * The adapter class for {@link SipAudioCall#Listener}. The default
+ * implementation of all callback methods is no-op.
+ */
+ public class Adapter implements Listener {
+ protected void onChanged(SipAudioCall call) {
+ }
+ public void onReadyToCall(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onCalling(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onRinging(SipAudioCall call, SipProfile caller) {
+ onChanged(call);
+ }
+ public void onRingingBack(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onCallEstablished(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onCallEnded(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onCallBusy(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onCallHeld(SipAudioCall call) {
+ onChanged(call);
+ }
+ public void onError(SipAudioCall call, String errorMessage) {
+ onChanged(call);
+ }
+ }
+
+ /**
+ * Sets the listener to listen to the audio call events. The method calls
+ * {@link #setListener(Listener, false)}.
+ *
+ * @param listener to listen to the audio call events of this object
+ * @see #setListener(Listener, boolean)
+ */
+ void setListener(Listener listener);
+
+ /**
+ * Sets the listener to listen to the audio call events. A
+ * {@link SipAudioCall} can only hold one listener at a time. Subsequent
+ * calls to this method override the previous listener.
+ *
+ * @param listener to listen to the audio call events of this object
+ * @param callbackImmediately set to true if the caller wants to be called
+ * back immediately on the current state
+ */
+ void setListener(Listener listener, boolean callbackImmediately);
+
+ /**
+ * Closes this object. The object is not usable after being closed.
+ */
+ void close();
+
+ /**
+ * Initiates an audio call to the specified profile.
+ *
+ * @param callee the SIP profile to make the call to
+ * @param sipManager the {@link SipManager} object to help make call with
+ */
+ void makeCall(SipProfile callee, SipManager sipManager) throws SipException;
+
+ /**
+ * Attaches an incoming call to this call object.
+ *
+ * @param session the session that receives the incoming call
+ * @param sdp the session description of the incoming call
+ */
+ void attachCall(ISipSession session, SdpSessionDescription sdp)
+ throws SipException;
+
+ /** Ends a call. */
+ void endCall() throws SipException;
+
+ /**
+ * Puts a call on hold. When succeeds,
+ * {@link #Listener#onCallHeld(SipAudioCall)} is called.
+ */
+ void holdCall() throws SipException;
+
+ /** Answers a call. */
+ void answerCall() throws SipException;
+
+ /**
+ * Continues a call that's on hold. When succeeds,
+ * {@link #Listener#onCallEstablished(SipAudioCall)} is called.
+ */
+ void continueCall() throws SipException;
+
+ /** Puts the device to speaker mode. */
+ void setSpeakerMode(boolean speakerMode);
+
+ /** Toggles mute. */
+ void toggleMute();
+
+ /**
+ * Checks if the call is on hold.
+ *
+ * @return true if the call is on hold
+ */
+ boolean isOnHold();
+
+ /**
+ * Checks if the call is muted.
+ *
+ * @return true if the call is muted
+ */
+ boolean isMuted();
+
+ /**
+ * Sends a DTMF code.
+ *
+ * @param code the DTMF code to send
+ */
+ void sendDtmf(int code);
+
+ /**
+ * Sends a DTMF code.
+ *
+ * @param code the DTMF code to send
+ * @param result the result message to send when done
+ */
+ void sendDtmf(int code, Message result);
+
+ /**
+ * Gets the {@link AudioStream} object used in this call. The object
+ * represents the RTP stream that carries the audio data to and from the
+ * peer. The object may not be created before the call is established. And
+ * it is undefined after the call ends or the {@link #close} method is
+ * called.
+ *
+ * @return the {@link AudioStream} object or null if the RTP stream has not
+ * yet been set up
+ */
+ AudioStream getAudioStream();
+
+ /**
+ * Gets the {@link AudioGroup} object which the {@link AudioStream} object
+ * joins. The group object may not exist before the call is established.
+ * Also, the {@code AudioStream} may change its group during a call (e.g.,
+ * after the call is held/un-held). Finally, the {@code AudioGroup} object
+ * returned by this method is undefined after the call ends or the
+ * {@link #close} method is called.
+ *
+ * @return the {@link AudioGroup} object or null if the RTP stream has not
+ * yet been set up
+ * @see #getAudioStream
+ */
+ AudioGroup getAudioGroup();
+
+ /**
+ * Checks if the call is established.
+ *
+ * @return true if the call is established
+ */
+ boolean isInCall();
+
+ /**
+ * Gets the local SIP profile.
+ *
+ * @return the local SIP profile
+ */
+ SipProfile getLocalProfile();
+
+ /**
+ * Gets the peer's SIP profile.
+ *
+ * @return the peer's SIP profile
+ */
+ SipProfile getPeerProfile();
+
+ /**
+ * Gets the state of the {@link ISipSession} that carries this call.
+ *
+ * @return the session state
+ */
+ SipSessionState getState();
+
+ /**
+ * Gets the {@link ISipSession} that carries this call.
+ *
+ * @return the session object that carries this call
+ */
+ ISipSession getSipSession();
+
+ /**
+ * Enables/disables the ring-back tone.
+ *
+ * @param enabled true to enable; false to disable
+ */
+ void setRingbackToneEnabled(boolean enabled);
+
+ /**
+ * Enables/disables the ring tone.
+ *
+ * @param enabled true to enable; false to disable
+ */
+ void setRingtoneEnabled(boolean enabled);
+}
diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java
new file mode 100644
index 0000000..7161309
--- /dev/null
+++ b/voip/java/android/net/sip/SipAudioCallImpl.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import gov.nist.javax.sdp.fields.SDPKeywords;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.net.sip.ISipSession;
+import android.net.sip.SdpSessionDescription;
+import android.net.sip.SessionDescription;
+import android.net.sip.SipAudioCall;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSessionAdapter;
+import android.net.sip.SipSessionState;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.sdp.SdpException;
+import javax.sip.SipException;
+
+/**
+ * Class that handles an audio call over SIP.
+ */
+/** @hide */
+public class SipAudioCallImpl extends SipSessionAdapter
+ implements SipAudioCall {
+ private static final String TAG = SipAudioCallImpl.class.getSimpleName();
+ private static final boolean RELEASE_SOCKET = true;
+ private static final boolean DONT_RELEASE_SOCKET = false;
+ private static final String AUDIO = "audio";
+ private static final int DTMF = 101;
+
+ private Context mContext;
+ private SipProfile mLocalProfile;
+ private SipAudioCall.Listener mListener;
+ private ISipSession mSipSession;
+ private SdpSessionDescription mPeerSd;
+
+ private AudioStream mRtpSession;
+ private SdpSessionDescription.AudioCodec mCodec;
+ private long mSessionId = -1L; // SDP session ID
+ private boolean mInCall = false;
+ private boolean mMuted = false;
+ private boolean mHold = false;
+
+ private boolean mRingbackToneEnabled = true;
+ private boolean mRingtoneEnabled = true;
+ private Ringtone mRingtone;
+ private ToneGenerator mRingbackTone;
+
+ private SipProfile mPendingCallRequest;
+
+ public SipAudioCallImpl(Context context, SipProfile localProfile) {
+ mContext = context;
+ mLocalProfile = localProfile;
+ }
+
+ public void setListener(SipAudioCall.Listener listener) {
+ setListener(listener, false);
+ }
+
+ public void setListener(SipAudioCall.Listener listener,
+ boolean callbackImmediately) {
+ mListener = listener;
+ if ((listener == null) || !callbackImmediately) return;
+ try {
+ SipSessionState state = getState();
+ switch (state) {
+ case READY_TO_CALL:
+ listener.onReadyToCall(this);
+ break;
+ case INCOMING_CALL:
+ listener.onRinging(this, getPeerProfile(mSipSession));
+ startRinging();
+ break;
+ case OUTGOING_CALL:
+ listener.onCalling(this);
+ break;
+ default:
+ listener.onError(this, "wrong state to attach call: " + state);
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "setListener()", t);
+ }
+ }
+
+ public synchronized boolean isInCall() {
+ return mInCall;
+ }
+
+ public synchronized boolean isOnHold() {
+ return mHold;
+ }
+
+ public void close() {
+ close(true);
+ }
+
+ private synchronized void close(boolean closeRtp) {
+ if (closeRtp) stopCall(RELEASE_SOCKET);
+ stopRingbackTone();
+ stopRinging();
+ mSipSession = null;
+ mInCall = false;
+ mHold = false;
+ mSessionId = -1L;
+ }
+
+ public synchronized SipProfile getLocalProfile() {
+ return mLocalProfile;
+ }
+
+ public synchronized SipProfile getPeerProfile() {
+ try {
+ return (mSipSession == null) ? null : mSipSession.getPeerProfile();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ public synchronized SipSessionState getState() {
+ if (mSipSession == null) return SipSessionState.READY_TO_CALL;
+ try {
+ return Enum.valueOf(SipSessionState.class, mSipSession.getState());
+ } catch (RemoteException e) {
+ return SipSessionState.REMOTE_ERROR;
+ }
+ }
+
+
+ public synchronized ISipSession getSipSession() {
+ return mSipSession;
+ }
+
+ @Override
+ public void onCalling(ISipSession session) {
+ Log.d(TAG, "calling... " + session);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCalling(SipAudioCallImpl.this);
+ } catch (Throwable t) {
+ Log.e(TAG, "onCalling()", t);
+ }
+ }
+ }
+
+ @Override
+ public void onRingingBack(ISipSession session) {
+ Log.d(TAG, "sip call ringing back: " + session);
+ if (!mInCall) startRingbackTone();
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onRingingBack(SipAudioCallImpl.this);
+ } catch (Throwable t) {
+ Log.e(TAG, "onRingingBack()", t);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onRinging(ISipSession session,
+ SipProfile peerProfile, byte[] sessionDescription) {
+ try {
+ if ((mSipSession == null) || !mInCall
+ || !session.getCallId().equals(mSipSession.getCallId())) {
+ // should not happen
+ session.endCall();
+ return;
+ }
+
+ // session changing request
+ try {
+ mPeerSd = new SdpSessionDescription(sessionDescription);
+ answerCall();
+ } catch (Throwable e) {
+ Log.e(TAG, "onRinging()", e);
+ session.endCall();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "onRinging()", e);
+ }
+ }
+
+ private synchronized void establishCall(byte[] sessionDescription) {
+ stopRingbackTone();
+ stopRinging();
+ try {
+ SdpSessionDescription sd =
+ new SdpSessionDescription(sessionDescription);
+ Log.d(TAG, "sip call established: " + sd);
+ startCall(sd);
+ mInCall = true;
+ } catch (SdpException e) {
+ Log.e(TAG, "createSessionDescription()", e);
+ }
+ }
+
+ @Override
+ public void onCallEstablished(ISipSession session,
+ byte[] sessionDescription) {
+ establishCall(sessionDescription);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ if (mHold) {
+ listener.onCallHeld(SipAudioCallImpl.this);
+ } else {
+ listener.onCallEstablished(SipAudioCallImpl.this);
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "onCallEstablished()", t);
+ }
+ }
+ }
+
+ @Override
+ public void onCallEnded(ISipSession session) {
+ Log.d(TAG, "sip call ended: " + session);
+ close();
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCallEnded(SipAudioCallImpl.this);
+ } catch (Throwable t) {
+ Log.e(TAG, "onCallEnded()", t);
+ }
+ }
+ }
+
+ @Override
+ public void onCallBusy(ISipSession session) {
+ Log.d(TAG, "sip call busy: " + session);
+ close(false);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCallBusy(SipAudioCallImpl.this);
+ } catch (Throwable t) {
+ Log.e(TAG, "onCallBusy()", t);
+ }
+ }
+ }
+
+ @Override
+ public void onCallChangeFailed(ISipSession session,
+ String className, String message) {
+ Log.d(TAG, "sip call change failed: " + message);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onError(SipAudioCallImpl.this,
+ className + ": " + message);
+ } catch (Throwable t) {
+ Log.e(TAG, "onCallBusy()", t);
+ }
+ }
+ }
+
+ @Override
+ public void onError(ISipSession session, String className,
+ String message) {
+ Log.d(TAG, "sip session error: " + className + ": " + message);
+ synchronized (this) {
+ if (!isInCall()) close(true);
+ }
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onError(SipAudioCallImpl.this,
+ className + ": " + message);
+ } catch (Throwable t) {
+ Log.e(TAG, "onError()", t);
+ }
+ }
+ }
+
+ public synchronized void attachCall(ISipSession session,
+ SdpSessionDescription sdp) throws SipException {
+ mSipSession = session;
+ mPeerSd = sdp;
+ try {
+ session.setListener(this);
+ } catch (Throwable e) {
+ Log.e(TAG, "attachCall()", e);
+ throwSipException(e);
+ }
+ }
+
+ public synchronized void makeCall(SipProfile peerProfile,
+ SipManager sipManager) throws SipException {
+ try {
+ mSipSession = sipManager.createSipSession(mLocalProfile, this);
+ if (mSipSession == null) {
+ throw new SipException(
+ "Failed to create SipSession; network available?");
+ }
+ mSipSession.makeCall(peerProfile, createOfferSessionDescription());
+ } catch (Throwable e) {
+ if (e instanceof SipException) {
+ throw (SipException) e;
+ } else {
+ throwSipException(e);
+ }
+ }
+ }
+
+ public synchronized void endCall() throws SipException {
+ try {
+ stopRinging();
+ stopCall(true);
+ mInCall = false;
+
+ // perform the above local ops first and then network op
+ if (mSipSession != null) mSipSession.endCall();
+ } catch (Throwable e) {
+ throwSipException(e);
+ }
+ }
+
+ public synchronized void holdCall() throws SipException {
+ if (mHold) return;
+ try {
+ mSipSession.changeCall(createHoldSessionDescription());
+ mHold = true;
+ } catch (Throwable e) {
+ throwSipException(e);
+ }
+
+ AudioGroup audioGroup = getAudioGroup();
+ if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+ }
+
+ public synchronized void answerCall() throws SipException {
+ try {
+ stopRinging();
+ mSipSession.answerCall(createAnswerSessionDescription());
+ } catch (Throwable e) {
+ Log.e(TAG, "answerCall()", e);
+ throwSipException(e);
+ }
+ }
+
+ public synchronized void continueCall() throws SipException {
+ if (!mHold) return;
+ try {
+ mHold = false;
+ mSipSession.changeCall(createContinueSessionDescription());
+ } catch (Throwable e) {
+ throwSipException(e);
+ }
+
+ AudioGroup audioGroup = getAudioGroup();
+ if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_NORMAL);
+ }
+
+ private SessionDescription createOfferSessionDescription() {
+ AudioCodec[] codecs = AudioCodec.getSystemSupportedCodecs();
+ return createSdpBuilder(true, convert(codecs)).build();
+ }
+
+ private SessionDescription createAnswerSessionDescription() {
+ try {
+ // choose an acceptable media from mPeerSd to answer
+ SdpSessionDescription.AudioCodec codec = getCodec(mPeerSd);
+ SdpSessionDescription.Builder sdpBuilder =
+ createSdpBuilder(false, codec);
+ if (mPeerSd.isSendOnly(AUDIO)) {
+ sdpBuilder.addMediaAttribute(AUDIO, "recvonly", (String) null);
+ } else if (mPeerSd.isReceiveOnly(AUDIO)) {
+ sdpBuilder.addMediaAttribute(AUDIO, "sendonly", (String) null);
+ }
+ return sdpBuilder.build();
+ } catch (SdpException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private SessionDescription createHoldSessionDescription() {
+ try {
+ return createSdpBuilder(false, mCodec)
+ .addMediaAttribute(AUDIO, "sendonly", (String) null)
+ .build();
+ } catch (SdpException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private SessionDescription createContinueSessionDescription() {
+ return createSdpBuilder(true, mCodec).build();
+ }
+
+ private String getMediaDescription(SdpSessionDescription.AudioCodec codec) {
+ return String.format("%d %s/%d", codec.payloadType, codec.name,
+ codec.sampleRate);
+ }
+
+ private long getSessionId() {
+ if (mSessionId < 0) {
+ mSessionId = System.currentTimeMillis();
+ }
+ return mSessionId;
+ }
+
+ private SdpSessionDescription.Builder createSdpBuilder(
+ boolean addTelephoneEvent,
+ SdpSessionDescription.AudioCodec... codecs) {
+ String localIp = getLocalIp();
+ SdpSessionDescription.Builder sdpBuilder;
+ try {
+ long sessionVersion = System.currentTimeMillis();
+ sdpBuilder = new SdpSessionDescription.Builder("SIP Call")
+ .setOrigin(mLocalProfile, getSessionId(), sessionVersion,
+ SDPKeywords.IN, SDPKeywords.IPV4, localIp)
+ .setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4,
+ localIp);
+ List<Integer> codecIds = new ArrayList<Integer>();
+ for (SdpSessionDescription.AudioCodec codec : codecs) {
+ codecIds.add(codec.payloadType);
+ }
+ if (addTelephoneEvent) codecIds.add(DTMF);
+ sdpBuilder.addMedia(AUDIO, getLocalMediaPort(), 1, "RTP/AVP",
+ codecIds.toArray(new Integer[codecIds.size()]));
+ for (SdpSessionDescription.AudioCodec codec : codecs) {
+ sdpBuilder.addMediaAttribute(AUDIO, "rtpmap",
+ getMediaDescription(codec));
+ }
+ if (addTelephoneEvent) {
+ sdpBuilder.addMediaAttribute(AUDIO, "rtpmap",
+ DTMF + " telephone-event/8000");
+ }
+ // FIXME: deal with vbr codec
+ sdpBuilder.addMediaAttribute(AUDIO, "ptime", "20");
+ } catch (SdpException e) {
+ throw new RuntimeException(e);
+ }
+ return sdpBuilder;
+ }
+
+ public synchronized void toggleMute() {
+ AudioGroup audioGroup = getAudioGroup();
+ if (audioGroup != null) {
+ audioGroup.setMode(
+ mMuted ? AudioGroup.MODE_NORMAL : AudioGroup.MODE_MUTED);
+ mMuted = !mMuted;
+ }
+ }
+
+ public synchronized boolean isMuted() {
+ return mMuted;
+ }
+
+ public synchronized void setSpeakerMode(boolean speakerMode) {
+ ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+ .setSpeakerphoneOn(speakerMode);
+ }
+
+ public void sendDtmf(int code) {
+ sendDtmf(code, null);
+ }
+
+ public synchronized void sendDtmf(int code, Message result) {
+ AudioGroup audioGroup = getAudioGroup();
+ if ((audioGroup != null) && (mSipSession != null)
+ && (SipSessionState.IN_CALL == getState())) {
+ Log.v(TAG, "send DTMF: " + code);
+ audioGroup.sendDtmf(code);
+ }
+ if (result != null) result.sendToTarget();
+ }
+
+ public synchronized AudioStream getAudioStream() {
+ return mRtpSession;
+ }
+
+ public synchronized AudioGroup getAudioGroup() {
+ return ((mRtpSession == null) ? null : mRtpSession.getAudioGroup());
+ }
+
+ private SdpSessionDescription.AudioCodec getCodec(SdpSessionDescription sd) {
+ HashMap<String, AudioCodec> acceptableCodecs =
+ new HashMap<String, AudioCodec>();
+ for (AudioCodec codec : AudioCodec.getSystemSupportedCodecs()) {
+ acceptableCodecs.put(codec.name, codec);
+ }
+ for (SdpSessionDescription.AudioCodec codec : sd.getAudioCodecs()) {
+ AudioCodec matchedCodec = acceptableCodecs.get(codec.name);
+ if (matchedCodec != null) return codec;
+ }
+ Log.w(TAG, "no common codec is found, use PCM/0");
+ return convert(AudioCodec.ULAW);
+ }
+
+ private AudioCodec convert(SdpSessionDescription.AudioCodec codec) {
+ AudioCodec c = AudioCodec.getSystemSupportedCodec(codec.name);
+ return ((c == null) ? AudioCodec.ULAW : c);
+ }
+
+ private SdpSessionDescription.AudioCodec convert(AudioCodec codec) {
+ return new SdpSessionDescription.AudioCodec(codec.defaultType,
+ codec.name, codec.sampleRate, codec.sampleCount);
+ }
+
+ private SdpSessionDescription.AudioCodec[] convert(AudioCodec[] codecs) {
+ SdpSessionDescription.AudioCodec[] copies =
+ new SdpSessionDescription.AudioCodec[codecs.length];
+ for (int i = 0, len = codecs.length; i < len; i++) {
+ copies[i] = convert(codecs[i]);
+ }
+ return copies;
+ }
+
+ private void startCall(SdpSessionDescription peerSd) {
+ stopCall(DONT_RELEASE_SOCKET);
+
+ mPeerSd = peerSd;
+ String peerMediaAddress = peerSd.getPeerMediaAddress(AUDIO);
+ // TODO: handle multiple media fields
+ int peerMediaPort = peerSd.getPeerMediaPort(AUDIO);
+ Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort);
+
+ int localPort = getLocalMediaPort();
+ int sampleRate = 8000;
+ int frameSize = sampleRate / 50; // 160
+ try {
+ // TODO: get sample rate from sdp
+ mCodec = getCodec(peerSd);
+
+ AudioStream audioStream = mRtpSession;
+ audioStream.associate(InetAddress.getByName(peerMediaAddress),
+ peerMediaPort);
+ audioStream.setCodec(convert(mCodec), mCodec.payloadType);
+ audioStream.setDtmfType(DTMF);
+ Log.d(TAG, "start media: localPort=" + localPort + ", peer="
+ + peerMediaAddress + ":" + peerMediaPort);
+
+ audioStream.setMode(RtpStream.MODE_NORMAL);
+ if (!mHold) {
+ // FIXME: won't work if peer is not sending nor receiving
+ if (!peerSd.isSending(AUDIO)) {
+ Log.d(TAG, " not receiving");
+ audioStream.setMode(RtpStream.MODE_SEND_ONLY);
+ }
+ if (!peerSd.isReceiving(AUDIO)) {
+ Log.d(TAG, " not sending");
+ audioStream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+ }
+ } else {
+ /* The recorder volume will be very low if the device is in
+ * IN_CALL mode. Therefore, we have to set the mode to NORMAL
+ * in order to have the normal microphone level.
+ */
+ ((AudioManager) mContext.getSystemService
+ (Context.AUDIO_SERVICE))
+ .setMode(AudioManager.MODE_NORMAL);
+ }
+
+ AudioGroup audioGroup = new AudioGroup();
+ audioStream.join(audioGroup);
+ if (mHold) {
+ audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+ } else if (mMuted) {
+ audioGroup.setMode(AudioGroup.MODE_MUTED);
+ } else {
+ audioGroup.setMode(AudioGroup.MODE_NORMAL);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "call()", e);
+ }
+ }
+
+ private void stopCall(boolean releaseSocket) {
+ Log.d(TAG, "stop audiocall");
+ if (mRtpSession != null) {
+ mRtpSession.join(null);
+
+ if (releaseSocket) {
+ mRtpSession.release();
+ mRtpSession = null;
+ }
+ }
+ }
+
+ private int getLocalMediaPort() {
+ if (mRtpSession != null) return mRtpSession.getLocalPort();
+ try {
+ AudioStream s = mRtpSession =
+ new AudioStream(InetAddress.getByName(getLocalIp()));
+ return s.getLocalPort();
+ } catch (IOException e) {
+ Log.w(TAG, "getLocalMediaPort(): " + e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getLocalIp() {
+ try {
+ return mSipSession.getLocalIp();
+ } catch (RemoteException e) {
+ // FIXME
+ return "127.0.0.1";
+ }
+ }
+
+ public synchronized void setRingbackToneEnabled(boolean enabled) {
+ mRingbackToneEnabled = enabled;
+ }
+
+ public synchronized void setRingtoneEnabled(boolean enabled) {
+ mRingtoneEnabled = enabled;
+ }
+
+ private void startRingbackTone() {
+ if (!mRingbackToneEnabled) return;
+ if (mRingbackTone == null) {
+ // The volume relative to other sounds in the stream
+ int toneVolume = 80;
+ mRingbackTone = new ToneGenerator(
+ AudioManager.STREAM_VOICE_CALL, toneVolume);
+ }
+ mRingbackTone.startTone(ToneGenerator.TONE_CDMA_LOW_PBX_L);
+ }
+
+ private void stopRingbackTone() {
+ if (mRingbackTone != null) {
+ mRingbackTone.stopTone();
+ mRingbackTone.release();
+ mRingbackTone = null;
+ }
+ }
+
+ private void startRinging() {
+ if (!mRingtoneEnabled) return;
+ ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE))
+ .vibrate(new long[] {0, 1000, 1000}, 1);
+ AudioManager am = (AudioManager)
+ mContext.getSystemService(Context.AUDIO_SERVICE);
+ if (am.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+ String ringtoneUri =
+ Settings.System.DEFAULT_RINGTONE_URI.toString();
+ mRingtone = RingtoneManager.getRingtone(mContext,
+ Uri.parse(ringtoneUri));
+ mRingtone.play();
+ }
+ }
+
+ private void stopRinging() {
+ ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE))
+ .cancel();
+ if (mRingtone != null) mRingtone.stop();
+ }
+
+ private void throwSipException(Throwable throwable) throws SipException {
+ if (throwable instanceof SipException) {
+ throw (SipException) throwable;
+ } else {
+ throw new SipException("", throwable);
+ }
+ }
+
+ private SipProfile getPeerProfile(ISipSession session) {
+ try {
+ return session.getPeerProfile();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+}
diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java
new file mode 100644
index 0000000..287a13a
--- /dev/null
+++ b/voip/java/android/net/sip/SipManager.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import java.text.ParseException;
+import javax.sip.SipException;
+
+/**
+ * The class provides API for various SIP related tasks. Specifically, the API
+ * allows the application to:
+ * <ul>
+ * <li>register a {@link SipProfile} to have the background SIP service listen
+ * to incoming calls and broadcast them with registered command string. See
+ * {@link #open(SipProfile, String, SipRegistrationListener)},
+ * {@link #open(SipProfile)}, {@link #close(String)},
+ * {@link #isOpened(String)} and {@link isRegistered(String)}. It also
+ * facilitates handling of the incoming call broadcast intent. See
+ * {@link #isIncomingCallIntent(Intent)}, {@link #getCallId(Intent)},
+ * {@link #getOfferSessionDescription(Intent)} and
+ * {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.</li>
+ * <li>make/take SIP-based audio calls. See
+ * {@link #makeAudioCall(Context, SipProfile, SipProfile, SipAudioCall.Listener)}
+ * and {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener}.</li>
+ * <li>register/unregister with a SIP service provider. See
+ * {@link #register(SipProfile, int, ISipSessionListener)} and
+ * {@link #unregister(SipProfile, ISipSessionListener)}.</li>
+ * <li>process SIP events directly with a {@link ISipSession} created by
+ * {@link createSipSession(SipProfile, ISipSessionListener)}.</li>
+ * </ul>
+ * @hide
+ */
+public class SipManager {
+ /** @hide */
+ public static final String SIP_INCOMING_CALL_ACTION =
+ "com.android.phone.SIP_INCOMING_CALL";
+ /** @hide */
+ public static final String SIP_ADD_PHONE_ACTION =
+ "com.android.phone.SIP_ADD_PHONE";
+ /** @hide */
+ public static final String SIP_REMOVE_PHONE_ACTION =
+ "com.android.phone.SIP_REMOVE_PHONE";
+ /** @hide */
+ public static final String LOCAL_URI_KEY = "LOCAL SIPURI";
+
+ private static final String CALL_ID_KEY = "CallID";
+ private static final String OFFER_SD_KEY = "OfferSD";
+
+ private ISipService mSipService;
+
+ /**
+ * Creates a manager instance and initializes the background SIP service.
+ * Will be removed once the SIP service is integrated into framework.
+ *
+ * @param context context to start the SIP service
+ * @return the manager instance
+ */
+ public static SipManager getInstance(final Context context) {
+ final SipManager manager = new SipManager();
+ manager.createSipService(context);
+ return manager;
+ }
+
+ private SipManager() {
+ }
+
+ private void createSipService(Context context) {
+ if (mSipService != null) return;
+ IBinder b = ServiceManager.getService(Context.SIP_SERVICE);
+ mSipService = ISipService.Stub.asInterface(b);
+ }
+
+ /**
+ * Opens the profile for making calls and/or receiving calls. Subsequent
+ * SIP calls can be made through the default phone UI. The caller may also
+ * make subsequent calls through
+ * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}.
+ * If the receiving-call option is enabled in the profile, the SIP service
+ * will register the profile to the corresponding server periodically in
+ * order to receive calls from the server.
+ *
+ * @param localProfile the SIP profile to make calls from
+ * @throws SipException if the profile contains incorrect settings or
+ * calling the SIP service results in an error
+ */
+ public void open(SipProfile localProfile) throws SipException {
+ try {
+ mSipService.open(localProfile);
+ } catch (RemoteException e) {
+ throw new SipException("open()", e);
+ }
+ }
+
+ /**
+ * Opens the profile for making calls and/or receiving calls. Subsequent
+ * SIP calls can be made through the default phone UI. The caller may also
+ * make subsequent calls through
+ * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}.
+ * If the receiving-call option is enabled in the profile, the SIP service
+ * will register the profile to the corresponding server periodically in
+ * order to receive calls from the server.
+ *
+ * @param localProfile the SIP profile to receive incoming calls for
+ * @param incomingCallBroadcastAction the action to be broadcast when an
+ * incoming call is received
+ * @param listener to listen to registration events; can be null
+ * @throws SipException if the profile contains incorrect settings or
+ * calling the SIP service results in an error
+ */
+ public void open(SipProfile localProfile,
+ String incomingCallBroadcastAction,
+ SipRegistrationListener listener) throws SipException {
+ try {
+ mSipService.open3(localProfile, incomingCallBroadcastAction,
+ createRelay(listener));
+ } catch (RemoteException e) {
+ throw new SipException("open()", e);
+ }
+ }
+
+ /**
+ * Sets the listener to listen to registration events. No effect if the
+ * profile has not been opened to receive calls
+ * (see {@link #open(SipProfile, String, SipRegistrationListener)} and
+ * {@link #open(SipProfile)}).
+ *
+ * @param localProfileUri the URI of the profile
+ * @param listener to listen to registration events; can be null
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public void setRegistrationListener(String localProfileUri,
+ SipRegistrationListener listener) throws SipException {
+ try {
+ mSipService.setRegistrationListener(
+ localProfileUri, createRelay(listener));
+ } catch (RemoteException e) {
+ throw new SipException("setRegistrationListener()", e);
+ }
+ }
+
+ /**
+ * Closes the specified profile to not make/receive calls. All the resources
+ * that were allocated to the profile are also released.
+ *
+ * @param localProfileUri the URI of the profile to close
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public void close(String localProfileUri) throws SipException {
+ try {
+ mSipService.close(localProfileUri);
+ } catch (RemoteException e) {
+ throw new SipException("close()", e);
+ }
+ }
+
+ /**
+ * Checks if the specified profile is enabled to receive calls.
+ *
+ * @param localProfileUri the URI of the profile in question
+ * @return true if the profile is enabled to receive calls
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public boolean isOpened(String localProfileUri) throws SipException {
+ try {
+ return mSipService.isOpened(localProfileUri);
+ } catch (RemoteException e) {
+ throw new SipException("isOpened()", e);
+ }
+ }
+
+ /**
+ * Checks if the specified profile is registered to the server for
+ * receiving calls.
+ *
+ * @param localProfileUri the URI of the profile in question
+ * @return true if the profile is registered to the server
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public boolean isRegistered(String localProfileUri) throws SipException {
+ try {
+ return mSipService.isRegistered(localProfileUri);
+ } catch (RemoteException e) {
+ throw new SipException("isRegistered()", e);
+ }
+ }
+
+ /**
+ * Creates a {@link SipAudioCall} to make a call.
+ *
+ * @param context context to create a {@link SipAudioCall} object
+ * @param localProfile the SIP profile to make the call from
+ * @param peerProfile the SIP profile to make the call to
+ * @param listener to listen to the call events from {@link SipAudioCall};
+ * can be null
+ * @return a {@link SipAudioCall} object
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public SipAudioCall makeAudioCall(Context context, SipProfile localProfile,
+ SipProfile peerProfile, SipAudioCall.Listener listener)
+ throws SipException {
+ SipAudioCall call = new SipAudioCallImpl(context, localProfile);
+ call.setListener(listener);
+ call.makeCall(peerProfile, this);
+ return call;
+ }
+
+ /**
+ * Creates a {@link SipAudioCall} to make a call. To use this method, one
+ * must call {@link #open(SipProfile)} first.
+ *
+ * @param context context to create a {@link SipAudioCall} object
+ * @param localProfileUri URI of the SIP profile to make the call from
+ * @param peerProfileUri URI of the SIP profile to make the call to
+ * @param listener to listen to the call events from {@link SipAudioCall};
+ * can be null
+ * @return a {@link SipAudioCall} object
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public SipAudioCall makeAudioCall(Context context, String localProfileUri,
+ String peerProfileUri, SipAudioCall.Listener listener)
+ throws SipException {
+ try {
+ return makeAudioCall(context,
+ new SipProfile.Builder(localProfileUri).build(),
+ new SipProfile.Builder(peerProfileUri).build(), listener);
+ } catch (ParseException e) {
+ throw new SipException("build SipProfile", e);
+ }
+ }
+
+ /**
+ * The method calls {@code takeAudioCall(context, incomingCallIntent,
+ * listener, true}.
+ *
+ * @see #takeAudioCall(Context, Intent, SipAudioCall.Listener, boolean)
+ */
+ public SipAudioCall takeAudioCall(Context context,
+ Intent incomingCallIntent, SipAudioCall.Listener listener)
+ throws SipException {
+ return takeAudioCall(context, incomingCallIntent, listener, true);
+ }
+
+ /**
+ * Creates a {@link SipAudioCall} to take an incoming call. Before the call
+ * is returned, the listener will receive a
+ * {@link SipAudioCall#Listener.onRinging(SipAudioCall, SipProfile)}
+ * callback.
+ *
+ * @param context context to create a {@link SipAudioCall} object
+ * @param incomingCallIntent the incoming call broadcast intent
+ * @param listener to listen to the call events from {@link SipAudioCall};
+ * can be null
+ * @return a {@link SipAudioCall} object
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public SipAudioCall takeAudioCall(Context context,
+ Intent incomingCallIntent, SipAudioCall.Listener listener,
+ boolean ringtoneEnabled) throws SipException {
+ if (incomingCallIntent == null) return null;
+
+ String callId = getCallId(incomingCallIntent);
+ if (callId == null) {
+ throw new SipException("Call ID missing in incoming call intent");
+ }
+
+ byte[] offerSd = getOfferSessionDescription(incomingCallIntent);
+ if (offerSd == null) {
+ throw new SipException("Session description missing in incoming "
+ + "call intent");
+ }
+
+ try {
+ SdpSessionDescription sdp = new SdpSessionDescription(offerSd);
+
+ ISipSession session = mSipService.getPendingSession(callId);
+ if (session == null) return null;
+ SipAudioCall call = new SipAudioCallImpl(
+ context, session.getLocalProfile());
+ call.setRingtoneEnabled(ringtoneEnabled);
+ call.attachCall(session, sdp);
+ call.setListener(listener);
+ return call;
+ } catch (Throwable t) {
+ throw new SipException("takeAudioCall()", t);
+ }
+ }
+
+ /**
+ * Checks if the intent is an incoming call broadcast intent.
+ *
+ * @param intent the intent in question
+ * @return true if the intent is an incoming call broadcast intent
+ */
+ public static boolean isIncomingCallIntent(Intent intent) {
+ if (intent == null) return false;
+ String callId = getCallId(intent);
+ byte[] offerSd = getOfferSessionDescription(intent);
+ return ((callId != null) && (offerSd != null));
+ }
+
+ /**
+ * Gets the call ID from the specified incoming call broadcast intent.
+ *
+ * @param incomingCallIntent the incoming call broadcast intent
+ * @return the call ID or null if the intent does not contain it
+ */
+ public static String getCallId(Intent incomingCallIntent) {
+ return incomingCallIntent.getStringExtra(CALL_ID_KEY);
+ }
+
+ /**
+ * Gets the offer session description from the specified incoming call
+ * broadcast intent.
+ *
+ * @param incomingCallIntent the incoming call broadcast intent
+ * @return the offer session description or null if the intent does not
+ * have it
+ */
+ public static byte[] getOfferSessionDescription(Intent incomingCallIntent) {
+ return incomingCallIntent.getByteArrayExtra(OFFER_SD_KEY);
+ }
+
+ /**
+ * Creates an incoming call broadcast intent.
+ *
+ * @param action the action string to broadcast
+ * @param callId the call ID of the incoming call
+ * @param sessionDescription the session description of the incoming call
+ * @return the incoming call intent
+ * @hide
+ */
+ public static Intent createIncomingCallBroadcast(String action,
+ String callId, byte[] sessionDescription) {
+ Intent intent = new Intent(action);
+ intent.putExtra(CALL_ID_KEY, callId);
+ intent.putExtra(OFFER_SD_KEY, sessionDescription);
+ return intent;
+ }
+
+ /**
+ * Registers the profile to the corresponding server for receiving calls.
+ * {@link #open(SipProfile, String, SipRegistrationListener)} is still
+ * needed to be called at least once in order for the SIP service to
+ * broadcast an intent when an incoming call is received.
+ *
+ * @param localProfile the SIP profile to register with
+ * @param expiryTime registration expiration time (in second)
+ * @param listener to listen to the registration events
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public void register(SipProfile localProfile, int expiryTime,
+ SipRegistrationListener listener) throws SipException {
+ try {
+ ISipSession session = mSipService.createSession(
+ localProfile, createRelay(listener));
+ session.register(expiryTime);
+ } catch (RemoteException e) {
+ throw new SipException("register()", e);
+ }
+ }
+
+ /**
+ * Unregisters the profile from the corresponding server for not receiving
+ * further calls.
+ *
+ * @param localProfile the SIP profile to register with
+ * @param listener to listen to the registration events
+ * @throws SipException if calling the SIP service results in an error
+ */
+ public void unregister(SipProfile localProfile,
+ SipRegistrationListener listener) throws SipException {
+ try {
+ ISipSession session = mSipService.createSession(
+ localProfile, createRelay(listener));
+ session.unregister();
+ } catch (RemoteException e) {
+ throw new SipException("unregister()", e);
+ }
+ }
+
+ /**
+ * Gets the {@link ISipSession} that handles the incoming call. For audio
+ * calls, consider to use {@link SipAudioCall} to handle the incoming call.
+ * See {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.
+ * Note that the method may be called only once for the same intent. For
+ * subsequent calls on the same intent, the method returns null.
+ *
+ * @param incomingCallIntent the incoming call broadcast intent
+ * @return the session object that handles the incoming call
+ */
+ public ISipSession getSessionFor(Intent incomingCallIntent)
+ throws SipException {
+ try {
+ String callId = getCallId(incomingCallIntent);
+ return mSipService.getPendingSession(callId);
+ } catch (RemoteException e) {
+ throw new SipException("getSessionFor()", e);
+ }
+ }
+
+ private static ISipSessionListener createRelay(
+ SipRegistrationListener listener) {
+ return ((listener == null) ? null : new ListenerRelay(listener));
+ }
+
+ /**
+ * Creates a {@link ISipSession} with the specified profile. Use other
+ * methods, if applicable, instead of interacting with {@link ISipSession}
+ * directly.
+ *
+ * @param localProfile the SIP profile the session is associated with
+ * @param listener to listen to SIP session events
+ */
+ public ISipSession createSipSession(SipProfile localProfile,
+ ISipSessionListener listener) throws SipException {
+ try {
+ return mSipService.createSession(localProfile, listener);
+ } catch (RemoteException e) {
+ throw new SipException("createSipSession()", e);
+ }
+ }
+
+ /**
+ * Gets the list of profiles hosted by the SIP service. The user information
+ * (username, password and display name) are crossed out.
+ * @hide
+ */
+ public SipProfile[] getListOfProfiles() {
+ try {
+ return mSipService.getListOfProfiles();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ private static class ListenerRelay extends SipSessionAdapter {
+ private SipRegistrationListener mListener;
+
+ // listener must not be null
+ public ListenerRelay(SipRegistrationListener listener) {
+ mListener = listener;
+ }
+
+ private String getUri(ISipSession session) {
+ try {
+ return session.getLocalProfile().getUriString();
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onRegistering(ISipSession session) {
+ mListener.onRegistering(getUri(session));
+ }
+
+ @Override
+ public void onRegistrationDone(ISipSession session, int duration) {
+ long expiryTime = duration;
+ if (duration > 0) expiryTime += System.currentTimeMillis();
+ mListener.onRegistrationDone(getUri(session), expiryTime);
+ }
+
+ @Override
+ public void onRegistrationFailed(ISipSession session, String className,
+ String message) {
+ mListener.onRegistrationFailed(getUri(session), className, message);
+ }
+
+ @Override
+ public void onRegistrationTimeout(ISipSession session) {
+ mListener.onRegistrationFailed(getUri(session),
+ SipException.class.getName(), "registration timed out");
+ }
+ }
+}
diff --git a/voip/java/android/net/sip/SipProfile.aidl b/voip/java/android/net/sip/SipProfile.aidl
new file mode 100644
index 0000000..3b6f68f
--- /dev/null
+++ b/voip/java/android/net/sip/SipProfile.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+parcelable SipProfile;
diff --git a/voip/java/android/net/sip/SipProfile.java b/voip/java/android/net/sip/SipProfile.java
new file mode 100644
index 0000000..e71c293
--- /dev/null
+++ b/voip/java/android/net/sip/SipProfile.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.PeerUnavailableException;
+import javax.sip.SipFactory;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.address.URI;
+
+/**
+ * Class containing a SIP account, domain and server information.
+ * @hide
+ */
+public class SipProfile implements Parcelable, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final int DEFAULT_PORT = 5060;
+ private Address mAddress;
+ private String mProxyAddress;
+ private String mPassword;
+ private String mDomain;
+ private String mProtocol = ListeningPoint.UDP;
+ private String mProfileName;
+ private boolean mSendKeepAlive = false;
+ private boolean mAutoRegistration = true;
+
+ /** @hide */
+ public static final Parcelable.Creator<SipProfile> CREATOR =
+ new Parcelable.Creator<SipProfile>() {
+ public SipProfile createFromParcel(Parcel in) {
+ return new SipProfile(in);
+ }
+
+ public SipProfile[] newArray(int size) {
+ return new SipProfile[size];
+ }
+ };
+
+ /**
+ * Class to help create a {@link SipProfile}.
+ */
+ public static class Builder {
+ private AddressFactory mAddressFactory;
+ private SipProfile mProfile = new SipProfile();
+ private SipURI mUri;
+ private String mDisplayName;
+ private String mProxyAddress;
+
+ {
+ try {
+ mAddressFactory =
+ SipFactory.getInstance().createAddressFactory();
+ } catch (PeerUnavailableException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param uriString the URI string as "sip:<user_name>@<domain>"
+ * @throws ParseException if the string is not a valid URI
+ */
+ public Builder(String uriString) throws ParseException {
+ if (uriString == null) {
+ throw new NullPointerException("uriString cannot be null");
+ }
+ URI uri = mAddressFactory.createURI(fix(uriString));
+ if (uri instanceof SipURI) {
+ mUri = (SipURI) uri;
+ } else {
+ throw new ParseException(uriString + " is not a SIP URI", 0);
+ }
+ mProfile.mDomain = mUri.getHost();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param username username of the SIP account
+ * @param serverDomain the SIP server domain; if the network address
+ * is different from the domain, use
+ * {@link #setOutboundProxy(String)} to set server address
+ * @throws ParseException if the parameters are not valid
+ */
+ public Builder(String username, String serverDomain)
+ throws ParseException {
+ if ((username == null) || (serverDomain == null)) {
+ throw new NullPointerException(
+ "username and serverDomain cannot be null");
+ }
+ mUri = mAddressFactory.createSipURI(username, serverDomain);
+ mProfile.mDomain = serverDomain;
+ }
+
+ private String fix(String uriString) {
+ return (uriString.trim().toLowerCase().startsWith("sip:")
+ ? uriString
+ : "sip:" + uriString);
+ }
+
+ /**
+ * Sets the name of the profile. This name is given by user.
+ *
+ * @param name name of the profile
+ * @return this builder object
+ */
+ public Builder setProfileName(String name) {
+ mProfile.mProfileName = name;
+ return this;
+ }
+
+ /**
+ * Sets the password of the SIP account
+ *
+ * @param password password of the SIP account
+ * @return this builder object
+ */
+ public Builder setPassword(String password) {
+ mUri.setUserPassword(password);
+ return this;
+ }
+
+ /**
+ * Sets the port number of the server. By default, it is 5060.
+ *
+ * @param port port number of the server
+ * @return this builder object
+ * @throws InvalidArgumentException if the port number is out of range
+ */
+ public Builder setPort(int port) throws InvalidArgumentException {
+ mUri.setPort(port);
+ return this;
+ }
+
+ /**
+ * Sets the protocol used to connect to the SIP server. Currently,
+ * only "UDP" and "TCP" are supported.
+ *
+ * @param protocol the protocol string
+ * @return this builder object
+ * @throws InvalidArgumentException if the protocol is not recognized
+ */
+ public Builder setProtocol(String protocol)
+ throws InvalidArgumentException {
+ if (protocol == null) {
+ throw new NullPointerException("protocol cannot be null");
+ }
+ protocol = protocol.toUpperCase();
+ if (!protocol.equals("UDP") && !protocol.equals("TCP")) {
+ throw new InvalidArgumentException(
+ "unsupported protocol: " + protocol);
+ }
+ mProfile.mProtocol = protocol;
+ return this;
+ }
+
+ /**
+ * Sets the outbound proxy of the SIP server.
+ *
+ * @param outboundProxy the network address of the outbound proxy
+ * @return this builder object
+ */
+ public Builder setOutboundProxy(String outboundProxy) {
+ mProxyAddress = outboundProxy;
+ return this;
+ }
+
+ /**
+ * Sets the display name of the user.
+ *
+ * @param displayName display name of the user
+ * @return this builder object
+ */
+ public Builder setDisplayName(String displayName) {
+ mDisplayName = displayName;
+ return this;
+ }
+
+ /**
+ * Sets the send keep-alive flag.
+ *
+ * @param flag true if sending keep-alive message is required,
+ * false otherwise
+ * @return this builder object
+ */
+ public Builder setSendKeepAlive(boolean flag) {
+ mProfile.mSendKeepAlive = flag;
+ return this;
+ }
+
+
+ /**
+ * Sets the auto. registration flag.
+ *
+ * @param flag true if the profile will be registered automatically,
+ * false otherwise
+ * @return this builder object
+ */
+ public Builder setAutoRegistration(boolean flag) {
+ mProfile.mAutoRegistration = flag;
+ return this;
+ }
+
+ /**
+ * Builds and returns the SIP profile object.
+ *
+ * @return the profile object created
+ */
+ public SipProfile build() {
+ // remove password from URI
+ mProfile.mPassword = mUri.getUserPassword();
+ mUri.setUserPassword(null);
+ try {
+ mProfile.mAddress = mAddressFactory.createAddress(
+ mDisplayName, mUri);
+ if (!TextUtils.isEmpty(mProxyAddress)) {
+ SipURI uri = (SipURI)
+ mAddressFactory.createURI(fix(mProxyAddress));
+ mProfile.mProxyAddress = uri.getHost();
+ }
+ } catch (ParseException e) {
+ // must not occur
+ throw new RuntimeException(e);
+ }
+ return mProfile;
+ }
+ }
+
+ private SipProfile() {
+ }
+
+ private SipProfile(Parcel in) {
+ mAddress = (Address) in.readSerializable();
+ mProxyAddress = in.readString();
+ mPassword = in.readString();
+ mDomain = in.readString();
+ mProtocol = in.readString();
+ mProfileName = in.readString();
+ mSendKeepAlive = (in.readInt() == 0) ? false : true;
+ mAutoRegistration = (in.readInt() == 0) ? false : true;
+ }
+
+ /** @hide */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeSerializable(mAddress);
+ out.writeString(mProxyAddress);
+ out.writeString(mPassword);
+ out.writeString(mDomain);
+ out.writeString(mProtocol);
+ out.writeString(mProfileName);
+ out.writeInt(mSendKeepAlive ? 1 : 0);
+ out.writeInt(mAutoRegistration ? 1 : 0);
+ }
+
+ /** @hide */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Gets the SIP URI of this profile.
+ *
+ * @return the SIP URI of this profile
+ */
+ public SipURI getUri() {
+ return (SipURI) mAddress.getURI();
+ }
+
+ /**
+ * Gets the SIP URI string of this profile.
+ *
+ * @return the SIP URI string of this profile
+ */
+ public String getUriString() {
+ return mAddress.getURI().toString();
+ }
+
+ /**
+ * Gets the SIP address of this profile.
+ *
+ * @return the SIP address of this profile
+ */
+ public Address getSipAddress() {
+ return mAddress;
+ }
+
+ /**
+ * Gets the display name of the user.
+ *
+ * @return the display name of the user
+ */
+ public String getDisplayName() {
+ return mAddress.getDisplayName();
+ }
+
+ /**
+ * Gets the username.
+ *
+ * @return the username
+ */
+ public String getUserName() {
+ return getUri().getUser();
+ }
+
+ /**
+ * Gets the password.
+ *
+ * @return the password
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /**
+ * Gets the SIP domain.
+ *
+ * @return the SIP domain
+ */
+ public String getSipDomain() {
+ return mDomain;
+ }
+
+ /**
+ * Gets the port number of the SIP server.
+ *
+ * @return the port number of the SIP server
+ */
+ public int getPort() {
+ int port = getUri().getPort();
+ return (port == -1) ? DEFAULT_PORT : port;
+ }
+
+ /**
+ * Gets the protocol used to connect to the server.
+ *
+ * @return the protocol
+ */
+ public String getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * Gets the network address of the server outbound proxy.
+ *
+ * @return the network address of the server outbound proxy
+ */
+ public String getProxyAddress() {
+ return mProxyAddress;
+ }
+
+ /**
+ * Gets the (user-defined) name of the profile.
+ *
+ * @return name of the profile
+ */
+ public String getProfileName() {
+ return mProfileName;
+ }
+
+ /**
+ * Gets the flag of 'Sending keep-alive'.
+ *
+ * @return the flag of sending SIP keep-alive messages.
+ */
+ public boolean getSendKeepAlive() {
+ return mSendKeepAlive;
+ }
+
+ /**
+ * Gets the flag of 'Auto Registration'.
+ *
+ * @return the flag of registering the profile automatically.
+ */
+ public boolean getAutoRegistration() {
+ return mAutoRegistration;
+ }
+}
diff --git a/voip/java/android/net/sip/SipRegistrationListener.java b/voip/java/android/net/sip/SipRegistrationListener.java
new file mode 100644
index 0000000..63faaf8
--- /dev/null
+++ b/voip/java/android/net/sip/SipRegistrationListener.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+/**
+ * Listener class to listen to SIP registration events.
+ * @hide
+ */
+public interface SipRegistrationListener {
+ /**
+ * Called when a registration request is sent.
+ *
+ * @param localProfileUri the URI string of the SIP profile to register with
+ */
+ void onRegistering(String localProfileUri);
+
+ /**
+ * Called when registration is successfully done.
+ *
+ * @param localProfileUri the URI string of the SIP profile to register with
+ * @param expiryTime duration in second before the registration expires
+ */
+ void onRegistrationDone(String localProfileUri, long expiryTime);
+
+ /**
+ * Called when the registration fails.
+ *
+ * @param localProfileUri the URI string of the SIP profile to register with
+ * @param errorClass name of the exception class
+ * @param errorMessage error message
+ */
+ void onRegistrationFailed(String localProfileUri, String errorClass,
+ String errorMessage);
+}
diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java
new file mode 100644
index 0000000..cfb71d7
--- /dev/null
+++ b/voip/java/android/net/sip/SipSessionAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+/**
+ * Adapter class for {@link ISipSessionListener}. Default implementation of all
+ * callback methods is no-op.
+ * @hide
+ */
+public class SipSessionAdapter extends ISipSessionListener.Stub {
+ public void onCalling(ISipSession session) {
+ }
+
+ public void onRinging(ISipSession session, SipProfile caller,
+ byte[] sessionDescription) {
+ }
+
+ public void onRingingBack(ISipSession session) {
+ }
+
+ public void onCallEstablished(
+ ISipSession session, byte[] sessionDescription) {
+ }
+
+ public void onCallEnded(ISipSession session) {
+ }
+
+ public void onCallBusy(ISipSession session) {
+ }
+
+ public void onCallChanged(ISipSession session, byte[] sessionDescription) {
+ }
+
+ public void onCallChangeFailed(ISipSession session, String className,
+ String message) {
+ }
+
+ public void onError(ISipSession session, String className, String message) {
+ }
+
+ public void onRegistering(ISipSession session) {
+ }
+
+ public void onRegistrationDone(ISipSession session, int duration) {
+ }
+
+ public void onRegistrationFailed(ISipSession session, String className,
+ String message) {
+ }
+
+ public void onRegistrationTimeout(ISipSession session) {
+ }
+}
diff --git a/voip/java/android/net/sip/SipSessionState.java b/voip/java/android/net/sip/SipSessionState.java
new file mode 100644
index 0000000..5bab112
--- /dev/null
+++ b/voip/java/android/net/sip/SipSessionState.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.sip;
+
+/**
+ * Defines {@link ISipSession} states.
+ * @hide
+ */
+public enum SipSessionState {
+ /** When session is ready to initiate a call or transaction. */
+ READY_TO_CALL,
+
+ /** When the registration request is sent out. */
+ REGISTERING,
+
+ /** When the unregistration request is sent out. */
+ DEREGISTERING,
+
+ /** When an INVITE request is received. */
+ INCOMING_CALL,
+
+ /** When an OK response is sent for the INVITE request received. */
+ INCOMING_CALL_ANSWERING,
+
+ /** When an INVITE request is sent. */
+ OUTGOING_CALL,
+
+ /** When a RINGING response is received for the INVITE request sent. */
+ OUTGOING_CALL_RING_BACK,
+
+ /** When a CANCEL request is sent for the INVITE request sent. */
+ OUTGOING_CALL_CANCELING,
+
+ /** When a call is established. */
+ IN_CALL,
+
+ /** Some error occurs when making a remote call to {@link ISipSession}. */
+ REMOTE_ERROR,
+
+ /** When an OPTIONS request is sent. */
+ PINGING;
+
+ /**
+ * Checks if the specified string represents the same state as this object.
+ *
+ * @return true if the specified string represents the same state as this
+ * object
+ */
+ public boolean equals(String state) {
+ return toString().equals(state);
+ }
+}
diff --git a/voip/jni/rtp/Android.mk b/voip/jni/rtp/Android.mk
new file mode 100644
index 0000000..a364355
--- /dev/null
+++ b/voip/jni/rtp/Android.mk
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := librtp_jni
+
+LOCAL_SRC_FILES := \
+ AudioCodec.cpp \
+ AudioGroup.cpp \
+ RtpStream.cpp \
+ util.cpp \
+ rtp_jni.cpp
+
+LOCAL_SHARED_LIBRARIES := \
+ libnativehelper \
+ libcutils \
+ libutils \
+ libmedia
+
+LOCAL_STATIC_LIBRARIES :=
+
+LOCAL_C_INCLUDES += \
+ $(JNI_H_INCLUDE)
+
+LOCAL_CFLAGS += -fvisibility=hidden
+
+LOCAL_PRELINK_MODULE := false
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/voip/jni/rtp/AudioCodec.cpp b/voip/jni/rtp/AudioCodec.cpp
new file mode 100644
index 0000000..ddd07fc
--- /dev/null
+++ b/voip/jni/rtp/AudioCodec.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyrightm (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string.h>
+
+#include "AudioCodec.h"
+
+namespace {
+
+int8_t gExponents[128] = {
+ 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+};
+
+//------------------------------------------------------------------------------
+
+class UlawCodec : public AudioCodec
+{
+public:
+ bool set(int sampleRate, int sampleCount) {
+ mSampleCount = sampleCount;
+ return sampleCount > 0;
+ }
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, void *payload, int length);
+private:
+ int mSampleCount;
+};
+
+int UlawCodec::encode(void *payload, int16_t *samples)
+{
+ int8_t *ulaws = (int8_t *)payload;
+ for (int i = 0; i < mSampleCount; ++i) {
+ int sample = samples[i];
+ int sign = (sample >> 8) & 0x80;
+ if (sample < 0) {
+ sample = -sample;
+ }
+ sample += 132;
+ if (sample > 32767) {
+ sample = 32767;
+ }
+ int exponent = gExponents[sample >> 8];
+ int mantissa = (sample >> (exponent + 3)) & 0x0F;
+ ulaws[i] = ~(sign | (exponent << 4) | mantissa);
+ }
+ return mSampleCount;
+}
+
+int UlawCodec::decode(int16_t *samples, void *payload, int length)
+{
+ int8_t *ulaws = (int8_t *)payload;
+ for (int i = 0; i < length; ++i) {
+ int ulaw = ~ulaws[i];
+ int exponent = (ulaw >> 4) & 0x07;
+ int mantissa = ulaw & 0x0F;
+ int sample = (((mantissa << 3) + 132) << exponent) - 132;
+ samples[i] = (ulaw < 0 ? -sample : sample);
+ }
+ return length;
+}
+
+AudioCodec *newUlawCodec()
+{
+ return new UlawCodec;
+}
+
+//------------------------------------------------------------------------------
+
+class AlawCodec : public AudioCodec
+{
+public:
+ bool set(int sampleRate, int sampleCount) {
+ mSampleCount = sampleCount;
+ return sampleCount > 0;
+ }
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, void *payload, int length);
+private:
+ int mSampleCount;
+};
+
+int AlawCodec::encode(void *payload, int16_t *samples)
+{
+ int8_t *alaws = (int8_t *)payload;
+ for (int i = 0; i < mSampleCount; ++i) {
+ int sample = samples[i];
+ int sign = (sample >> 8) & 0x80;
+ if (sample < 0) {
+ sample = -sample;
+ }
+ if (sample > 32767) {
+ sample = 32767;
+ }
+ int exponent = gExponents[sample >> 8];
+ int mantissa = (sample >> (exponent == 0 ? 4 : exponent + 3)) & 0x0F;
+ alaws[i] = (sign | (exponent << 4) | mantissa) ^ 0xD5;
+ }
+ return mSampleCount;
+}
+
+int AlawCodec::decode(int16_t *samples, void *payload, int length)
+{
+ int8_t *alaws = (int8_t *)payload;
+ for (int i = 0; i < length; ++i) {
+ int alaw = alaws[i] ^ 0x55;
+ int exponent = (alaw >> 4) & 0x07;
+ int mantissa = alaw & 0x0F;
+ int sample = (exponent == 0 ? (mantissa << 4) + 8 :
+ ((mantissa << 3) + 132) << exponent);
+ samples[i] = (alaw < 0 ? sample : -sample);
+ }
+ return length;
+}
+
+AudioCodec *newAlawCodec()
+{
+ return new AlawCodec;
+}
+
+struct AudioCodecType {
+ const char *name;
+ AudioCodec *(*create)();
+} gAudioCodecTypes[] = {
+ {"PCMA", newAlawCodec},
+ {"PCMU", newUlawCodec},
+ {NULL, NULL},
+};
+
+} // namespace
+
+AudioCodec *newAudioCodec(const char *codecName)
+{
+ AudioCodecType *type = gAudioCodecTypes;
+ while (type->name != NULL) {
+ if (strcmp(codecName, type->name) == 0) {
+ return type->create();
+ }
+ ++type;
+ }
+ return NULL;
+}
diff --git a/voip/jni/rtp/AudioCodec.h b/voip/jni/rtp/AudioCodec.h
new file mode 100644
index 0000000..797494c
--- /dev/null
+++ b/voip/jni/rtp/AudioCodec.h
@@ -0,0 +1,36 @@
+/*
+ * Copyrightm (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdint.h>
+
+#ifndef __AUDIO_CODEC_H__
+#define __AUDIO_CODEC_H__
+
+class AudioCodec
+{
+public:
+ virtual ~AudioCodec() {}
+ // Returns true if initialization succeeds.
+ virtual bool set(int sampleRate, int sampleCount) = 0;
+ // Returns the length of payload in bytes.
+ virtual int encode(void *payload, int16_t *samples) = 0;
+ // Returns the number of decoded samples.
+ virtual int decode(int16_t *samples, void *payload, int length) = 0;
+};
+
+AudioCodec *newAudioCodec(const char *codecName);
+
+#endif
diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp
new file mode 100644
index 0000000..08a8d1c
--- /dev/null
+++ b/voip/jni/rtp/AudioGroup.cpp
@@ -0,0 +1,933 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/epoll.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <time.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#define LOG_TAG "AudioGroup"
+#include <cutils/atomic.h>
+#include <utils/Log.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+#include <utils/threads.h>
+#include <utils/SystemClock.h>
+#include <media/AudioSystem.h>
+#include <media/AudioRecord.h>
+#include <media/AudioTrack.h>
+#include <media/mediarecorder.h>
+
+#include "jni.h"
+#include "JNIHelp.h"
+
+#include "AudioCodec.h"
+
+extern int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss);
+
+namespace {
+
+using namespace android;
+
+int gRandom = -1;
+
+// We use a circular array to implement jitter buffer. The simplest way is doing
+// a modulo operation on the index while accessing the array. However modulo can
+// be expensive on some platforms, such as ARM. Thus we round up the size of the
+// array to the nearest power of 2 and then use bitwise-and instead of modulo.
+// Currently we make it 256ms long and assume packet interval is 32ms or less.
+// The first 64ms is the place where samples get mixed. The rest 192ms is the
+// real jitter buffer. For a stream at 8000Hz it takes 4096 bytes. These numbers
+// are chosen by experiments and each of them can be adjusted as needed.
+
+// Other notes:
+// + We use elapsedRealtime() to get the time. Since we use 32bit variables
+// instead of 64bit ones, comparison must be done by subtraction.
+// + Sampling rate must be multiple of 1000Hz, and packet length must be in
+// milliseconds. No floating points.
+// + If we cannot get enough CPU, we drop samples and simulate packet loss.
+// + Resampling is not done yet, so streams in one group must use the same rate.
+// For the first release we might only support 8kHz and 16kHz.
+
+class AudioStream
+{
+public:
+ AudioStream();
+ ~AudioStream();
+ bool set(int mode, int socket, sockaddr_storage *remote,
+ const char *codecName, int sampleRate, int sampleCount,
+ int codecType, int dtmfType);
+
+ void sendDtmf(int event);
+ bool mix(int32_t *output, int head, int tail, int sampleRate);
+ void encode(int tick, AudioStream *chain);
+ void decode(int tick);
+
+private:
+ enum {
+ NORMAL = 0,
+ SEND_ONLY = 1,
+ RECEIVE_ONLY = 2,
+ LAST_MODE = 2,
+ };
+
+ int mMode;
+ int mSocket;
+ sockaddr_storage mRemote;
+ AudioCodec *mCodec;
+ uint32_t mCodecMagic;
+ uint32_t mDtmfMagic;
+
+ int mTick;
+ int mSampleRate;
+ int mSampleCount;
+ int mInterval;
+
+ int16_t *mBuffer;
+ int mBufferMask;
+ int mBufferHead;
+ int mBufferTail;
+ int mLatencyScore;
+
+ uint16_t mSequence;
+ uint32_t mTimestamp;
+ uint32_t mSsrc;
+
+ int mDtmfEvent;
+ int mDtmfStart;
+
+ AudioStream *mNext;
+
+ friend class AudioGroup;
+};
+
+AudioStream::AudioStream()
+{
+ mSocket = -1;
+ mCodec = NULL;
+ mBuffer = NULL;
+ mNext = NULL;
+}
+
+AudioStream::~AudioStream()
+{
+ close(mSocket);
+ delete mCodec;
+ delete [] mBuffer;
+ LOGD("stream[%d] is dead", mSocket);
+}
+
+bool AudioStream::set(int mode, int socket, sockaddr_storage *remote,
+ const char *codecName, int sampleRate, int sampleCount,
+ int codecType, int dtmfType)
+{
+ if (mode < 0 || mode > LAST_MODE) {
+ return false;
+ }
+ mMode = mode;
+
+ if (codecName) {
+ mRemote = *remote;
+ mCodec = newAudioCodec(codecName);
+ if (!mCodec || !mCodec->set(sampleRate, sampleCount)) {
+ return false;
+ }
+ }
+
+ mCodecMagic = (0x8000 | codecType) << 16;
+ mDtmfMagic = (dtmfType == -1) ? 0 : (0x8000 | dtmfType) << 16;
+
+ mTick = elapsedRealtime();
+ mSampleRate = sampleRate / 1000;
+ mSampleCount = sampleCount;
+ mInterval = mSampleCount / mSampleRate;
+
+ // Allocate jitter buffer.
+ for (mBufferMask = 8192; mBufferMask < sampleRate; mBufferMask <<= 1);
+ mBufferMask >>= 2;
+ mBuffer = new int16_t[mBufferMask];
+ --mBufferMask;
+ mBufferHead = 0;
+ mBufferTail = 0;
+ mLatencyScore = 0;
+
+ // Initialize random bits.
+ read(gRandom, &mSequence, sizeof(mSequence));
+ read(gRandom, &mTimestamp, sizeof(mTimestamp));
+ read(gRandom, &mSsrc, sizeof(mSsrc));
+
+ mDtmfEvent = -1;
+ mDtmfStart = 0;
+
+ // Only take over the socket when succeeded.
+ mSocket = socket;
+
+ LOGD("stream[%d] is configured as %s %dkHz %dms", mSocket,
+ (codecName ? codecName : "RAW"), mSampleRate, mInterval);
+ return true;
+}
+
+void AudioStream::sendDtmf(int event)
+{
+ if (mDtmfMagic != 0) {
+ mDtmfEvent = event << 24;
+ mDtmfStart = mTimestamp + mSampleCount;
+ }
+}
+
+bool AudioStream::mix(int32_t *output, int head, int tail, int sampleRate)
+{
+ if (mMode == SEND_ONLY) {
+ return false;
+ }
+
+ if (head - mBufferHead < 0) {
+ head = mBufferHead;
+ }
+ if (tail - mBufferTail > 0) {
+ tail = mBufferTail;
+ }
+ if (tail - head <= 0) {
+ return false;
+ }
+
+ head *= mSampleRate;
+ tail *= mSampleRate;
+
+ if (sampleRate == mSampleRate) {
+ for (int i = head; i - tail < 0; ++i) {
+ output[i - head] += mBuffer[i & mBufferMask];
+ }
+ } else {
+ // TODO: implement resampling.
+ return false;
+ }
+ return true;
+}
+
+void AudioStream::encode(int tick, AudioStream *chain)
+{
+ if (tick - mTick >= mInterval) {
+ // We just missed the train. Pretend that packets in between are lost.
+ int skipped = (tick - mTick) / mInterval;
+ mTick += skipped * mInterval;
+ mSequence += skipped;
+ mTimestamp += skipped * mSampleCount;
+ LOGD("stream[%d] skips %d packets", mSocket, skipped);
+ }
+
+ tick = mTick;
+ mTick += mInterval;
+ ++mSequence;
+ mTimestamp += mSampleCount;
+
+ if (mMode == RECEIVE_ONLY) {
+ return;
+ }
+
+ // If there is an ongoing DTMF event, send it now.
+ if (mDtmfEvent != -1) {
+ int duration = mTimestamp - mDtmfStart;
+ // Make sure duration is reasonable.
+ if (duration >= 0 && duration < mSampleRate * 100) {
+ duration += mSampleCount;
+ int32_t buffer[4] = {
+ htonl(mDtmfMagic | mSequence),
+ htonl(mDtmfStart),
+ mSsrc,
+ htonl(mDtmfEvent | duration),
+ };
+ if (duration >= mSampleRate * 100) {
+ buffer[3] |= htonl(1 << 23);
+ mDtmfEvent = -1;
+ }
+ sendto(mSocket, buffer, sizeof(buffer), MSG_DONTWAIT,
+ (sockaddr *)&mRemote, sizeof(mRemote));
+ return;
+ }
+ mDtmfEvent = -1;
+ }
+
+ // It is time to mix streams.
+ bool mixed = false;
+ int32_t buffer[mSampleCount + 3];
+ memset(buffer, 0, sizeof(buffer));
+ while (chain) {
+ if (chain != this &&
+ chain->mix(buffer, tick - mInterval, tick, mSampleRate)) {
+ mixed = true;
+ }
+ chain = chain->mNext;
+ }
+ if (!mixed) {
+ LOGD("stream[%d] no data", mSocket);
+ return;
+ }
+
+ // Cook the packet and send it out.
+ int16_t samples[mSampleCount];
+ for (int i = 0; i < mSampleCount; ++i) {
+ int32_t sample = buffer[i];
+ if (sample < -32768) {
+ sample = -32768;
+ }
+ if (sample > 32767) {
+ sample = 32767;
+ }
+ samples[i] = sample;
+ }
+ if (!mCodec) {
+ // Special case for device stream.
+ send(mSocket, samples, sizeof(samples), MSG_DONTWAIT);
+ return;
+ }
+
+ buffer[0] = htonl(mCodecMagic | mSequence);
+ buffer[1] = htonl(mTimestamp);
+ buffer[2] = mSsrc;
+ int length = mCodec->encode(&buffer[3], samples);
+ if (length <= 0) {
+ LOGD("stream[%d] encoder error", mSocket);
+ return;
+ }
+ sendto(mSocket, buffer, length + 12, MSG_DONTWAIT, (sockaddr *)&mRemote,
+ sizeof(mRemote));
+}
+
+void AudioStream::decode(int tick)
+{
+ char c;
+ if (mMode == SEND_ONLY) {
+ recv(mSocket, &c, 1, MSG_DONTWAIT);
+ return;
+ }
+
+ // Make sure mBufferHead and mBufferTail are reasonable.
+ if ((unsigned int)(tick + 256 - mBufferHead) > 1024) {
+ mBufferHead = tick - 64;
+ mBufferTail = mBufferHead;
+ }
+
+ if (tick - mBufferHead > 64) {
+ // Throw away outdated samples.
+ mBufferHead = tick - 64;
+ if (mBufferTail - mBufferHead < 0) {
+ mBufferTail = mBufferHead;
+ }
+ }
+
+ if (mBufferTail - tick <= 80) {
+ mLatencyScore = tick;
+ } else if (tick - mLatencyScore >= 5000) {
+ // Reset the jitter buffer to 40ms if the latency keeps larger than 80ms
+ // in the past 5s. This rarely happens, so let us just keep it simple.
+ LOGD("stream[%d] latency control", mSocket);
+ mBufferTail = tick + 40;
+ }
+
+ if (mBufferTail - mBufferHead > 256 - mInterval) {
+ // Buffer overflow. Drop the packet.
+ LOGD("stream[%d] buffer overflow", mSocket);
+ recv(mSocket, &c, 1, MSG_DONTWAIT);
+ return;
+ }
+
+ // Receive the packet and decode it.
+ int16_t samples[mSampleCount];
+ int length = 0;
+ if (!mCodec) {
+ // Special case for device stream.
+ length = recv(mSocket, samples, sizeof(samples),
+ MSG_TRUNC | MSG_DONTWAIT) >> 1;
+ } else {
+ __attribute__((aligned(4))) uint8_t buffer[2048];
+ length = recv(mSocket, buffer, sizeof(buffer),
+ MSG_TRUNC | MSG_DONTWAIT);
+
+ // Do we need to check SSRC, sequence, and timestamp? They are not
+ // reliable but at least they can be used to identify duplicates?
+ if (length < 12 || length > (int)sizeof(buffer) ||
+ (ntohl(*(uint32_t *)buffer) & 0xC07F0000) != mCodecMagic) {
+ LOGD("stream[%d] malformed packet", mSocket);
+ return;
+ }
+ int offset = 12 + ((buffer[0] & 0x0F) << 2);
+ if ((buffer[0] & 0x10) != 0) {
+ offset += 4 + (ntohs(*(uint16_t *)&buffer[offset + 2]) << 2);
+ }
+ if ((buffer[0] & 0x20) != 0) {
+ length -= buffer[length - 1];
+ }
+ length -= offset;
+ if (length >= 0) {
+ length = mCodec->decode(samples, &buffer[offset], length);
+ }
+ }
+ if (length != mSampleCount) {
+ LOGD("stream[%d] decoder error", mSocket);
+ return;
+ }
+
+ if (tick - mBufferTail > 0) {
+ // Buffer underrun. Reset the jitter buffer to 40ms.
+ LOGD("stream[%d] buffer underrun", mSocket);
+ if (mBufferTail - mBufferHead <= 0) {
+ mBufferHead = tick + 40;
+ mBufferTail = mBufferHead;
+ } else {
+ int tail = (tick + 40) * mSampleRate;
+ for (int i = mBufferTail * mSampleRate; i - tail < 0; ++i) {
+ mBuffer[i & mBufferMask] = 0;
+ }
+ mBufferTail = tick + 40;
+ }
+ }
+
+ // Append to the jitter buffer.
+ int tail = mBufferTail * mSampleRate;
+ for (int i = 0; i < mSampleCount; ++i) {
+ mBuffer[tail & mBufferMask] = samples[i];
+ ++tail;
+ }
+ mBufferTail += mInterval;
+}
+
+//------------------------------------------------------------------------------
+
+class AudioGroup
+{
+public:
+ AudioGroup();
+ ~AudioGroup();
+ bool set(int sampleRate, int sampleCount);
+
+ bool setMode(int mode);
+ bool sendDtmf(int event);
+ bool add(AudioStream *stream);
+ bool remove(int socket);
+
+private:
+ enum {
+ ON_HOLD = 0,
+ MUTED = 1,
+ NORMAL = 2,
+ EC_ENABLED = 3,
+ LAST_MODE = 3,
+ };
+ int mMode;
+ AudioStream *mChain;
+ int mEventQueue;
+ volatile int mDtmfEvent;
+
+ int mSampleCount;
+ int mDeviceSocket;
+ AudioTrack mTrack;
+ AudioRecord mRecord;
+
+ bool networkLoop();
+ bool deviceLoop();
+
+ class NetworkThread : public Thread
+ {
+ public:
+ NetworkThread(AudioGroup *group) : Thread(false), mGroup(group) {}
+
+ bool start()
+ {
+ if (run("Network", ANDROID_PRIORITY_AUDIO) != NO_ERROR) {
+ LOGE("cannot start network thread");
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ AudioGroup *mGroup;
+ bool threadLoop()
+ {
+ return mGroup->networkLoop();
+ }
+ };
+ sp<NetworkThread> mNetworkThread;
+
+ class DeviceThread : public Thread
+ {
+ public:
+ DeviceThread(AudioGroup *group) : Thread(false), mGroup(group) {}
+
+ bool start()
+ {
+ char c;
+ while (recv(mGroup->mDeviceSocket, &c, 1, MSG_DONTWAIT) == 1);
+
+ if (run("Device", ANDROID_PRIORITY_AUDIO) != NO_ERROR) {
+ LOGE("cannot start device thread");
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ AudioGroup *mGroup;
+ bool threadLoop()
+ {
+ return mGroup->deviceLoop();
+ }
+ };
+ sp<DeviceThread> mDeviceThread;
+};
+
+AudioGroup::AudioGroup()
+{
+ mMode = ON_HOLD;
+ mChain = NULL;
+ mEventQueue = -1;
+ mDtmfEvent = -1;
+ mDeviceSocket = -1;
+ mNetworkThread = new NetworkThread(this);
+ mDeviceThread = new DeviceThread(this);
+}
+
+AudioGroup::~AudioGroup()
+{
+ mNetworkThread->requestExitAndWait();
+ mDeviceThread->requestExitAndWait();
+ mTrack.stop();
+ mRecord.stop();
+ close(mEventQueue);
+ close(mDeviceSocket);
+ while (mChain) {
+ AudioStream *next = mChain->mNext;
+ delete mChain;
+ mChain = next;
+ }
+ LOGD("group[%d] is dead", mDeviceSocket);
+}
+
+bool AudioGroup::set(int sampleRate, int sampleCount)
+{
+ mEventQueue = epoll_create(2);
+ if (mEventQueue == -1) {
+ LOGE("epoll_create: %s", strerror(errno));
+ return false;
+ }
+
+ mSampleCount = sampleCount;
+
+ // Find out the frame count for AudioTrack and AudioRecord.
+ int output = 0;
+ int input = 0;
+ if (AudioTrack::getMinFrameCount(&output, AudioSystem::VOICE_CALL,
+ sampleRate) != NO_ERROR || output <= 0 ||
+ AudioRecord::getMinFrameCount(&input, sampleRate,
+ AudioSystem::PCM_16_BIT, 1) != NO_ERROR || input <= 0) {
+ LOGE("cannot compute frame count");
+ return false;
+ }
+ LOGD("reported frame count: output %d, input %d", output, input);
+
+ output = (output + sampleCount - 1) / sampleCount * sampleCount;
+ input = (input + sampleCount - 1) / sampleCount * sampleCount;
+ if (input < output * 2) {
+ input = output * 2;
+ }
+ LOGD("adjusted frame count: output %d, input %d", output, input);
+
+ // Initialize AudioTrack and AudioRecord.
+ if (mTrack.set(AudioSystem::VOICE_CALL, sampleRate, AudioSystem::PCM_16_BIT,
+ AudioSystem::CHANNEL_OUT_MONO, output) != NO_ERROR ||
+ mRecord.set(AUDIO_SOURCE_MIC, sampleRate, AudioSystem::PCM_16_BIT,
+ AudioSystem::CHANNEL_IN_MONO, input) != NO_ERROR) {
+ LOGE("cannot initialize audio device");
+ return false;
+ }
+ LOGD("latency: output %d, input %d", mTrack.latency(), mRecord.latency());
+
+ // TODO: initialize echo canceler here.
+
+ // Create device socket.
+ int pair[2];
+ if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)) {
+ LOGE("socketpair: %s", strerror(errno));
+ return false;
+ }
+ mDeviceSocket = pair[0];
+
+ // Create device stream.
+ mChain = new AudioStream;
+ if (!mChain->set(AudioStream::NORMAL, pair[1], NULL, NULL,
+ sampleRate, sampleCount, -1, -1)) {
+ close(pair[1]);
+ LOGE("cannot initialize device stream");
+ return false;
+ }
+
+ // Give device socket a reasonable timeout and buffer size.
+ timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 1000 * sampleCount / sampleRate * 500;
+ if (setsockopt(pair[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) ||
+ setsockopt(pair[0], SOL_SOCKET, SO_RCVBUF, &output, sizeof(output)) ||
+ setsockopt(pair[1], SOL_SOCKET, SO_SNDBUF, &output, sizeof(output))) {
+ LOGE("setsockopt: %s", strerror(errno));
+ return false;
+ }
+
+ // Add device stream into event queue.
+ epoll_event event;
+ event.events = EPOLLIN;
+ event.data.ptr = mChain;
+ if (epoll_ctl(mEventQueue, EPOLL_CTL_ADD, pair[1], &event)) {
+ LOGE("epoll_ctl: %s", strerror(errno));
+ return false;
+ }
+
+ // Anything else?
+ LOGD("stream[%d] joins group[%d]", pair[1], pair[0]);
+ return true;
+}
+
+bool AudioGroup::setMode(int mode)
+{
+ if (mode < 0 || mode > LAST_MODE) {
+ return false;
+ }
+ if (mMode == mode) {
+ return true;
+ }
+
+ LOGD("group[%d] switches from mode %d to %d", mDeviceSocket, mMode, mode);
+ mMode = mode;
+
+ mDeviceThread->requestExitAndWait();
+ if (mode == ON_HOLD) {
+ mTrack.stop();
+ mRecord.stop();
+ return true;
+ }
+
+ mTrack.start();
+ if (mode == MUTED) {
+ mRecord.stop();
+ } else {
+ mRecord.start();
+ }
+
+ if (!mDeviceThread->start()) {
+ mTrack.stop();
+ mRecord.stop();
+ return false;
+ }
+ return true;
+}
+
+bool AudioGroup::sendDtmf(int event)
+{
+ if (event < 0 || event > 15) {
+ return false;
+ }
+
+ // DTMF is rarely used, so we try to make it as lightweight as possible.
+ // Using volatile might be dodgy, but using a pipe or pthread primitives
+ // or stop-set-restart threads seems too heavy. Will investigate later.
+ timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 100000000;
+ for (int i = 0; mDtmfEvent != -1 && i < 20; ++i) {
+ nanosleep(&ts, NULL);
+ }
+ if (mDtmfEvent != -1) {
+ return false;
+ }
+ mDtmfEvent = event;
+ nanosleep(&ts, NULL);
+ return true;
+}
+
+bool AudioGroup::add(AudioStream *stream)
+{
+ mNetworkThread->requestExitAndWait();
+
+ epoll_event event;
+ event.events = EPOLLIN;
+ event.data.ptr = stream;
+ if (epoll_ctl(mEventQueue, EPOLL_CTL_ADD, stream->mSocket, &event)) {
+ LOGE("epoll_ctl: %s", strerror(errno));
+ return false;
+ }
+
+ stream->mNext = mChain->mNext;
+ mChain->mNext = stream;
+ if (!mNetworkThread->start()) {
+ // Only take over the stream when succeeded.
+ mChain->mNext = stream->mNext;
+ return false;
+ }
+
+ LOGD("stream[%d] joins group[%d]", stream->mSocket, mDeviceSocket);
+ return true;
+}
+
+bool AudioGroup::remove(int socket)
+{
+ mNetworkThread->requestExitAndWait();
+
+ for (AudioStream *stream = mChain; stream->mNext; stream = stream->mNext) {
+ AudioStream *target = stream->mNext;
+ if (target->mSocket == socket) {
+ if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, socket, NULL)) {
+ LOGE("epoll_ctl: %s", strerror(errno));
+ return false;
+ }
+ stream->mNext = target->mNext;
+ LOGD("stream[%d] leaves group[%d]", socket, mDeviceSocket);
+ delete target;
+ break;
+ }
+ }
+
+ // Do not start network thread if there is only one stream.
+ if (!mChain->mNext || !mNetworkThread->start()) {
+ return false;
+ }
+ return true;
+}
+
+bool AudioGroup::networkLoop()
+{
+ int tick = elapsedRealtime();
+ int deadline = tick + 10;
+ int count = 0;
+
+ for (AudioStream *stream = mChain; stream; stream = stream->mNext) {
+ if (!stream->mTick || tick - stream->mTick >= 0) {
+ stream->encode(tick, mChain);
+ }
+ if (deadline - stream->mTick > 0) {
+ deadline = stream->mTick;
+ }
+ ++count;
+ }
+
+ if (mDtmfEvent != -1) {
+ int event = mDtmfEvent;
+ for (AudioStream *stream = mChain; stream; stream = stream->mNext) {
+ stream->sendDtmf(event);
+ }
+ mDtmfEvent = -1;
+ }
+
+ deadline -= tick;
+ if (deadline < 1) {
+ deadline = 1;
+ }
+
+ epoll_event events[count];
+ count = epoll_wait(mEventQueue, events, count, deadline);
+ if (count == -1) {
+ LOGE("epoll_wait: %s", strerror(errno));
+ return false;
+ }
+ for (int i = 0; i < count; ++i) {
+ ((AudioStream *)events[i].data.ptr)->decode(tick);
+ }
+
+ return true;
+}
+
+bool AudioGroup::deviceLoop()
+{
+ int16_t output[mSampleCount];
+
+ if (recv(mDeviceSocket, output, sizeof(output), 0) <= 0) {
+ memset(output, 0, sizeof(output));
+ }
+ if (mTrack.write(output, sizeof(output)) != (int)sizeof(output)) {
+ LOGE("cannot write to AudioTrack");
+ return false;
+ }
+
+ if (mMode != MUTED) {
+ uint32_t frameCount = mRecord.frameCount();
+ AudioRecord::Buffer input;
+ input.frameCount = frameCount;
+
+ if (mRecord.obtainBuffer(&input, -1) != NO_ERROR) {
+ LOGE("cannot read from AudioRecord");
+ return false;
+ }
+
+ if (input.frameCount < (uint32_t)mSampleCount) {
+ input.frameCount = 0;
+ } else {
+ if (mMode == NORMAL) {
+ send(mDeviceSocket, input.i8, sizeof(output), MSG_DONTWAIT);
+ } else {
+ // TODO: Echo canceller runs here.
+ send(mDeviceSocket, input.i8, sizeof(output), MSG_DONTWAIT);
+ }
+ if (input.frameCount < frameCount) {
+ input.frameCount = mSampleCount;
+ }
+ }
+
+ mRecord.releaseBuffer(&input);
+ }
+
+ return true;
+}
+
+//------------------------------------------------------------------------------
+
+static jfieldID gNative;
+static jfieldID gMode;
+
+void add(JNIEnv *env, jobject thiz, jint mode,
+ jint socket, jstring jRemoteAddress, jint remotePort,
+ jstring jCodecName, jint sampleRate, jint sampleCount,
+ jint codecType, jint dtmfType)
+{
+ const char *codecName = NULL;
+ AudioStream *stream = NULL;
+ AudioGroup *group = NULL;
+
+ // Sanity check.
+ sockaddr_storage remote;
+ if (parse(env, jRemoteAddress, remotePort, &remote) < 0) {
+ // Exception already thrown.
+ goto error;
+ }
+ if (sampleRate < 0 || sampleCount < 0 || codecType < 0 || codecType > 127) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+ goto error;
+ }
+ if (!jCodecName) {
+ jniThrowNullPointerException(env, "codecName");
+ goto error;
+ }
+ codecName = env->GetStringUTFChars(jCodecName, NULL);
+ if (!codecName) {
+ // Exception already thrown.
+ goto error;
+ }
+
+ // Create audio stream.
+ stream = new AudioStream;
+ if (!stream->set(mode, socket, &remote, codecName, sampleRate, sampleCount,
+ codecType, dtmfType)) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot initialize audio stream");
+ env->ReleaseStringUTFChars(jCodecName, codecName);
+ goto error;
+ }
+ env->ReleaseStringUTFChars(jCodecName, codecName);
+ socket = -1;
+
+ // Create audio group.
+ group = (AudioGroup *)env->GetIntField(thiz, gNative);
+ if (!group) {
+ int mode = env->GetIntField(thiz, gMode);
+ group = new AudioGroup;
+ if (!group->set(8000, 256) || !group->setMode(mode)) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot initialize audio group");
+ goto error;
+ }
+ }
+
+ // Add audio stream into audio group.
+ if (!group->add(stream)) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot add audio stream");
+ goto error;
+ }
+
+ // Succeed.
+ env->SetIntField(thiz, gNative, (int)group);
+ return;
+
+error:
+ delete group;
+ delete stream;
+ close(socket);
+ env->SetIntField(thiz, gNative, NULL);
+}
+
+void remove(JNIEnv *env, jobject thiz, jint socket)
+{
+ AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
+ if (group) {
+ if (socket == -1 || !group->remove(socket)) {
+ delete group;
+ env->SetIntField(thiz, gNative, NULL);
+ }
+ }
+}
+
+void setMode(JNIEnv *env, jobject thiz, jint mode)
+{
+ AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
+ if (group && !group->setMode(mode)) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+ return;
+ }
+ env->SetIntField(thiz, gMode, mode);
+}
+
+void sendDtmf(JNIEnv *env, jobject thiz, jint event)
+{
+ AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
+ if (group && !group->sendDtmf(event)) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+ }
+}
+
+JNINativeMethod gMethods[] = {
+ {"add", "(IILjava/lang/String;ILjava/lang/String;IIII)V", (void *)add},
+ {"remove", "(I)V", (void *)remove},
+ {"setMode", "(I)V", (void *)setMode},
+ {"sendDtmf", "(I)V", (void *)sendDtmf},
+};
+
+} // namespace
+
+int registerAudioGroup(JNIEnv *env)
+{
+ gRandom = open("/dev/urandom", O_RDONLY);
+ if (gRandom == -1) {
+ LOGE("urandom: %s", strerror(errno));
+ return -1;
+ }
+
+ jclass clazz;
+ if ((clazz = env->FindClass("android/net/rtp/AudioGroup")) == NULL ||
+ (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL ||
+ (gMode = env->GetFieldID(clazz, "mMode", "I")) == NULL ||
+ env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
+ LOGE("JNI registration failed");
+ return -1;
+ }
+ return 0;
+}
diff --git a/voip/jni/rtp/RtpStream.cpp b/voip/jni/rtp/RtpStream.cpp
new file mode 100644
index 0000000..33b88e4
--- /dev/null
+++ b/voip/jni/rtp/RtpStream.cpp
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#define LOG_TAG "RtpStream"
+#include <utils/Log.h>
+
+#include "jni.h"
+#include "JNIHelp.h"
+
+extern int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss);
+
+namespace {
+
+jfieldID gNative;
+
+jint create(JNIEnv *env, jobject thiz, jstring jAddress)
+{
+ env->SetIntField(thiz, gNative, -1);
+
+ sockaddr_storage ss;
+ if (parse(env, jAddress, 0, &ss) < 0) {
+ // Exception already thrown.
+ return -1;
+ }
+
+ int socket = ::socket(ss.ss_family, SOCK_DGRAM, 0);
+ socklen_t len = sizeof(ss);
+ if (socket == -1 || bind(socket, (sockaddr *)&ss, sizeof(ss)) != 0 ||
+ getsockname(socket, (sockaddr *)&ss, &len) != 0) {
+ jniThrowException(env, "java/net/SocketException", strerror(errno));
+ ::close(socket);
+ return -1;
+ }
+
+ uint16_t *p = (ss.ss_family == AF_INET) ?
+ &((sockaddr_in *)&ss)->sin_port : &((sockaddr_in6 *)&ss)->sin6_port;
+ uint16_t port = ntohs(*p);
+ if ((port & 1) == 0) {
+ env->SetIntField(thiz, gNative, socket);
+ return port;
+ }
+ ::close(socket);
+
+ socket = ::socket(ss.ss_family, SOCK_DGRAM, 0);
+ if (socket != -1) {
+ uint16_t delta = port << 1;
+ ++port;
+
+ for (int i = 0; i < 1000; ++i) {
+ do {
+ port += delta;
+ } while (port < 1024);
+ *p = htons(port);
+
+ if (bind(socket, (sockaddr *)&ss, sizeof(ss)) == 0) {
+ env->SetIntField(thiz, gNative, socket);
+ return port;
+ }
+ }
+ }
+
+ jniThrowException(env, "java/net/SocketException", strerror(errno));
+ ::close(socket);
+ return -1;
+}
+
+jint dup(JNIEnv *env, jobject thiz)
+{
+ int socket1 = env->GetIntField(thiz, gNative);
+ int socket2 = ::dup(socket1);
+ if (socket2 == -1) {
+ jniThrowException(env, "java/lang/IllegalStateException", strerror(errno));
+ }
+ LOGD("dup %d to %d", socket1, socket2);
+ return socket2;
+}
+
+void close(JNIEnv *env, jobject thiz)
+{
+ int socket = env->GetIntField(thiz, gNative);
+ ::close(socket);
+ env->SetIntField(thiz, gNative, -1);
+ LOGD("close %d", socket);
+}
+
+JNINativeMethod gMethods[] = {
+ {"create", "(Ljava/lang/String;)I", (void *)create},
+ {"dup", "()I", (void *)dup},
+ {"close", "()V", (void *)close},
+};
+
+} // namespace
+
+int registerRtpStream(JNIEnv *env)
+{
+ jclass clazz;
+ if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL ||
+ (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL ||
+ env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
+ LOGE("JNI registration failed");
+ return -1;
+ }
+ return 0;
+}
diff --git a/voip/jni/rtp/rtp_jni.cpp b/voip/jni/rtp/rtp_jni.cpp
new file mode 100644
index 0000000..9f4bff9
--- /dev/null
+++ b/voip/jni/rtp/rtp_jni.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+
+#include "jni.h"
+
+extern int registerRtpStream(JNIEnv *env);
+extern int registerAudioGroup(JNIEnv *env);
+
+__attribute__((visibility("default"))) jint JNI_OnLoad(JavaVM *vm, void *unused)
+{
+ JNIEnv *env = NULL;
+ if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK ||
+ registerRtpStream(env) < 0 || registerAudioGroup(env) < 0) {
+ return -1;
+ }
+ return JNI_VERSION_1_4;
+}
diff --git a/voip/jni/rtp/util.cpp b/voip/jni/rtp/util.cpp
new file mode 100644
index 0000000..1d702fc
--- /dev/null
+++ b/voip/jni/rtp/util.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "jni.h"
+#include "JNIHelp.h"
+
+int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss)
+{
+ if (!jAddress) {
+ jniThrowNullPointerException(env, "address");
+ return -1;
+ }
+ if (port < 0 || port > 65535) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", "port");
+ return -1;
+ }
+ const char *address = env->GetStringUTFChars(jAddress, NULL);
+ if (!address) {
+ // Exception already thrown.
+ return -1;
+ }
+ memset(ss, 0, sizeof(*ss));
+
+ sockaddr_in *sin = (sockaddr_in *)ss;
+ if (inet_pton(AF_INET, address, &(sin->sin_addr)) > 0) {
+ sin->sin_family = AF_INET;
+ sin->sin_port = htons(port);
+ env->ReleaseStringUTFChars(jAddress, address);
+ return 0;
+ }
+
+ sockaddr_in6 *sin6 = (sockaddr_in6 *)ss;
+ if (inet_pton(AF_INET6, address, &(sin6->sin6_addr)) > 0) {
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_port = htons(port);
+ env->ReleaseStringUTFChars(jAddress, address);
+ return 0;
+ }
+
+ env->ReleaseStringUTFChars(jAddress, address);
+ jniThrowException(env, "java/lang/IllegalArgumentException", "address");
+ return -1;
+}