summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--java/android/net/rtp/AudioCodec.java146
-rw-r--r--java/android/net/rtp/AudioGroup.java206
-rw-r--r--java/android/net/rtp/AudioStream.java167
-rw-r--r--java/android/net/rtp/RtpStream.java195
-rw-r--r--java/android/net/rtp/package.html28
-rw-r--r--java/android/net/sip/ISipService.aidl43
-rw-r--r--java/android/net/sip/ISipSession.aidl148
-rw-r--r--java/android/net/sip/ISipSessionListener.aidl133
-rw-r--r--java/android/net/sip/SimpleSessionDescription.java613
-rw-r--r--java/android/net/sip/SipAudioCall.java1143
-rw-r--r--java/android/net/sip/SipErrorCode.java101
-rw-r--r--java/android/net/sip/SipException.java37
-rw-r--r--java/android/net/sip/SipManager.java622
-rw-r--r--java/android/net/sip/SipProfile.aidl19
-rw-r--r--java/android/net/sip/SipProfile.java502
-rw-r--r--java/android/net/sip/SipRegistrationListener.java48
-rw-r--r--java/android/net/sip/SipSession.java574
-rw-r--r--java/android/net/sip/SipSessionAdapter.java68
-rw-r--r--java/android/net/sip/package.html45
-rw-r--r--java/com/android/server/sip/SipHelper.java537
-rw-r--r--java/com/android/server/sip/SipService.java1262
-rw-r--r--java/com/android/server/sip/SipSessionGroup.java1863
-rw-r--r--java/com/android/server/sip/SipSessionListenerProxy.java265
-rw-r--r--java/com/android/server/sip/SipWakeLock.java73
-rw-r--r--java/com/android/server/sip/SipWakeupTimer.java336
-rw-r--r--jni/rtp/AmrCodec.cpp272
-rw-r--r--jni/rtp/Android.mk59
-rw-r--r--jni/rtp/AudioCodec.cpp51
-rw-r--r--jni/rtp/AudioCodec.h38
-rw-r--r--jni/rtp/AudioGroup.cpp1073
-rw-r--r--jni/rtp/EchoSuppressor.cpp196
-rw-r--r--jni/rtp/EchoSuppressor.h58
-rw-r--r--jni/rtp/G711Codec.cpp144
-rw-r--r--jni/rtp/GsmCodec.cpp78
-rw-r--r--jni/rtp/RtpStream.cpp113
-rw-r--r--jni/rtp/rtp_jni.cpp32
-rw-r--r--jni/rtp/util.cpp61
37 files changed, 11349 insertions, 0 deletions
diff --git a/java/android/net/rtp/AudioCodec.java b/java/android/net/rtp/AudioCodec.java
new file mode 100644
index 0000000..85255c8
--- /dev/null
+++ b/java/android/net/rtp/AudioCodec.java
@@ -0,0 +1,146 @@
+/*
+ * 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.Arrays;
+
+/**
+ * This class defines a collection of audio codecs to be used with
+ * {@link AudioStream}s. Their parameters are designed to be exchanged using
+ * Session Description Protocol (SDP). Most of the values listed here can be
+ * found in RFC 3551, while others are described in separated standards.
+ *
+ * <p>Few simple configurations are defined as public static instances for the
+ * convenience of direct uses. More complicated ones could be obtained using
+ * {@link #getCodec(int, String, String)}. For example, one can use the
+ * following snippet to create a mode-1-only AMR codec.</p>
+ * <pre>
+ * AudioCodec codec = AudioCodec.getCodec(100, "AMR/8000", "mode-set=1");
+ * </pre>
+ *
+ * @see AudioStream
+ */
+public class AudioCodec {
+ /**
+ * The RTP payload type of the encoding.
+ */
+ public final int type;
+
+ /**
+ * The encoding parameters to be used in the corresponding SDP attribute.
+ */
+ public final String rtpmap;
+
+ /**
+ * The format parameters to be used in the corresponding SDP attribute.
+ */
+ public final String fmtp;
+
+ /**
+ * G.711 u-law audio codec.
+ */
+ public static final AudioCodec PCMU = new AudioCodec(0, "PCMU/8000", null);
+
+ /**
+ * G.711 a-law audio codec.
+ */
+ public static final AudioCodec PCMA = new AudioCodec(8, "PCMA/8000", null);
+
+ /**
+ * GSM Full-Rate audio codec, also known as GSM-FR, GSM 06.10, GSM, or
+ * simply FR.
+ */
+ public static final AudioCodec GSM = new AudioCodec(3, "GSM/8000", null);
+
+ /**
+ * GSM Enhanced Full-Rate audio codec, also known as GSM-EFR, GSM 06.60, or
+ * simply EFR.
+ */
+ public static final AudioCodec GSM_EFR = new AudioCodec(96, "GSM-EFR/8000", null);
+
+ /**
+ * Adaptive Multi-Rate narrowband audio codec, also known as AMR or AMR-NB.
+ * Currently CRC, robust sorting, and interleaving are not supported. See
+ * more details about these features in RFC 4867.
+ */
+ public static final AudioCodec AMR = new AudioCodec(97, "AMR/8000", null);
+
+ private static final AudioCodec[] sCodecs = {GSM_EFR, AMR, GSM, PCMU, PCMA};
+
+ private AudioCodec(int type, String rtpmap, String fmtp) {
+ this.type = type;
+ this.rtpmap = rtpmap;
+ this.fmtp = fmtp;
+ }
+
+ /**
+ * Returns system supported audio codecs.
+ */
+ public static AudioCodec[] getCodecs() {
+ return Arrays.copyOf(sCodecs, sCodecs.length);
+ }
+
+ /**
+ * Creates an AudioCodec according to the given configuration.
+ *
+ * @param type The payload type of the encoding defined in RTP/AVP.
+ * @param rtpmap The encoding parameters specified in the corresponding SDP
+ * attribute, or null if it is not available.
+ * @param fmtp The format parameters specified in the corresponding SDP
+ * attribute, or null if it is not available.
+ * @return The configured AudioCodec or {@code null} if it is not supported.
+ */
+ public static AudioCodec getCodec(int type, String rtpmap, String fmtp) {
+ if (type < 0 || type > 127) {
+ return null;
+ }
+
+ AudioCodec hint = null;
+ if (rtpmap != null) {
+ String clue = rtpmap.trim().toUpperCase();
+ for (AudioCodec codec : sCodecs) {
+ if (clue.startsWith(codec.rtpmap)) {
+ String channels = clue.substring(codec.rtpmap.length());
+ if (channels.length() == 0 || channels.equals("/1")) {
+ hint = codec;
+ }
+ break;
+ }
+ }
+ } else if (type < 96) {
+ for (AudioCodec codec : sCodecs) {
+ if (type == codec.type) {
+ hint = codec;
+ rtpmap = codec.rtpmap;
+ break;
+ }
+ }
+ }
+
+ if (hint == null) {
+ return null;
+ }
+ if (hint == AMR && fmtp != null) {
+ String clue = fmtp.toLowerCase();
+ if (clue.contains("crc=1") || clue.contains("robust-sorting=1") ||
+ clue.contains("interleaving=")) {
+ return null;
+ }
+ }
+ return new AudioCodec(type, rtpmap, fmtp);
+ }
+}
diff --git a/java/android/net/rtp/AudioGroup.java b/java/android/net/rtp/AudioGroup.java
new file mode 100644
index 0000000..8faeb88
--- /dev/null
+++ b/java/android/net/rtp/AudioGroup.java
@@ -0,0 +1,206 @@
+/*
+ * 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 android.media.AudioManager;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * An AudioGroup is an audio hub for the speaker, the microphone, and
+ * {@link AudioStream}s. Each of these components can be logically turned on
+ * or off by calling {@link #setMode(int)} or {@link RtpStream#setMode(int)}.
+ * The AudioGroup will go through these components and process them one by one
+ * within its execution loop. The loop consists of four steps. First, for each
+ * AudioStream not in {@link RtpStream#MODE_SEND_ONLY}, decodes its incoming
+ * packets and stores in its buffer. Then, if the microphone is enabled,
+ * processes the recorded audio and stores in its buffer. Third, if the speaker
+ * is enabled, mixes all AudioStream buffers and plays back. Finally, for each
+ * AudioStream not in {@link RtpStream#MODE_RECEIVE_ONLY}, mixes all other
+ * buffers and sends back the encoded packets. An AudioGroup does nothing if
+ * there is no AudioStream in it.
+ *
+ * <p>Few things must be noticed before using these classes. The performance is
+ * highly related to the system load and the network bandwidth. Usually a
+ * simpler {@link AudioCodec} costs fewer CPU cycles but requires more network
+ * bandwidth, and vise versa. Using two AudioStreams at the same time doubles
+ * not only the load but also the bandwidth. The condition varies from one
+ * device to another, and developers should choose the right combination in
+ * order to get the best result.</p>
+ *
+ * <p>It is sometimes useful to keep multiple AudioGroups at the same time. For
+ * example, a Voice over IP (VoIP) application might want to put a conference
+ * call on hold in order to make a new call but still allow people in the
+ * conference call talking to each other. This can be done easily using two
+ * AudioGroups, but there are some limitations. Since the speaker and the
+ * microphone are globally shared resources, only one AudioGroup at a time is
+ * allowed to run in a mode other than {@link #MODE_ON_HOLD}. The others will
+ * be unable to acquire these resources and fail silently.</p>
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission. Developers
+ * should set the audio mode to {@link AudioManager#MODE_IN_COMMUNICATION}
+ * using {@link AudioManager#setMode(int)} and change it back when none of
+ * the AudioGroups is in use.</p>
+ *
+ * @see AudioStream
+ */
+public class AudioGroup {
+ /**
+ * This mode is similar to {@link #MODE_NORMAL} except the speaker and
+ * the microphone are both disabled.
+ */
+ public static final int MODE_ON_HOLD = 0;
+
+ /**
+ * This mode is similar to {@link #MODE_NORMAL} except the microphone is
+ * disabled.
+ */
+ public static final int MODE_MUTED = 1;
+
+ /**
+ * This mode indicates that the speaker, the microphone, and all
+ * {@link AudioStream}s in the group are enabled. First, the packets
+ * received from the streams are decoded and mixed with the audio recorded
+ * from the microphone. Then, the results are played back to the speaker,
+ * encoded and sent back to each stream.
+ */
+ public static final int MODE_NORMAL = 2;
+
+ /**
+ * This mode is similar to {@link #MODE_NORMAL} except the echo suppression
+ * is enabled. It should be only used when the speaker phone is on.
+ */
+ public static final int MODE_ECHO_SUPPRESSION = 3;
+
+ private static final int MODE_LAST = 3;
+
+ private final Map<AudioStream, Integer> mStreams;
+ private int mMode = MODE_ON_HOLD;
+
+ private int mNative;
+ static {
+ System.loadLibrary("rtp_jni");
+ }
+
+ /**
+ * Creates an empty AudioGroup.
+ */
+ public AudioGroup() {
+ mStreams = new HashMap<AudioStream, Integer>();
+ }
+
+ /**
+ * Returns the {@link AudioStream}s in this group.
+ */
+ public AudioStream[] getStreams() {
+ synchronized (this) {
+ return mStreams.keySet().toArray(new AudioStream[mStreams.size()]);
+ }
+ }
+
+ /**
+ * Returns the current mode.
+ */
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * Changes the current mode. It must be one of {@link #MODE_ON_HOLD},
+ * {@link #MODE_MUTED}, {@link #MODE_NORMAL}, and
+ * {@link #MODE_ECHO_SUPPRESSION}.
+ *
+ * @param mode The mode to change to.
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ public void setMode(int mode) {
+ if (mode < 0 || mode > MODE_LAST) {
+ throw new IllegalArgumentException("Invalid mode");
+ }
+ synchronized (this) {
+ nativeSetMode(mode);
+ mMode = mode;
+ }
+ }
+
+ private native void nativeSetMode(int mode);
+
+ // Package-private method used by AudioStream.join().
+ synchronized void add(AudioStream stream) {
+ if (!mStreams.containsKey(stream)) {
+ try {
+ AudioCodec codec = stream.getCodec();
+ String codecSpec = String.format(Locale.US, "%d %s %s", codec.type,
+ codec.rtpmap, codec.fmtp);
+ int id = nativeAdd(stream.getMode(), stream.getSocket(),
+ stream.getRemoteAddress().getHostAddress(),
+ stream.getRemotePort(), codecSpec, stream.getDtmfType());
+ mStreams.put(stream, id);
+ } catch (NullPointerException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private native int nativeAdd(int mode, int socket, String remoteAddress,
+ int remotePort, String codecSpec, int dtmfType);
+
+ // Package-private method used by AudioStream.join().
+ synchronized void remove(AudioStream stream) {
+ Integer id = mStreams.remove(stream);
+ if (id != null) {
+ nativeRemove(id);
+ }
+ }
+
+ private native void nativeRemove(int id);
+
+ /**
+ * 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 void sendDtmf(int event) {
+ if (event < 0 || event > 15) {
+ throw new IllegalArgumentException("Invalid event");
+ }
+ synchronized (this) {
+ nativeSendDtmf(event);
+ }
+ }
+
+ private native void nativeSendDtmf(int event);
+
+ /**
+ * Removes every {@link AudioStream} in this group.
+ */
+ public void clear() {
+ for (AudioStream stream : getStreams()) {
+ stream.join(null);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ nativeRemove(0);
+ super.finalize();
+ }
+}
diff --git a/java/android/net/rtp/AudioStream.java b/java/android/net/rtp/AudioStream.java
new file mode 100644
index 0000000..5cd1abc
--- /dev/null
+++ b/java/android/net/rtp/AudioStream.java
@@ -0,0 +1,167 @@
+/*
+ * 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;
+
+/**
+ * An AudioStream is a {@link RtpStream} which carrys audio payloads over
+ * Real-time Transport Protocol (RTP). Two different classes are developed in
+ * order to support various usages such as audio conferencing. An AudioStream
+ * represents a remote endpoint which consists of a network mapping and a
+ * configured {@link AudioCodec}. On the other side, An {@link AudioGroup}
+ * represents a local endpoint which mixes all the AudioStreams and optionally
+ * interacts with the speaker and the microphone at the same time. The simplest
+ * usage includes one for each endpoints. For other combinations, developers
+ * should be aware of the limitations described in {@link AudioGroup}.
+ *
+ * <p>An AudioStream becomes busy when it joins an AudioGroup. In this case most
+ * of the setter methods are disabled. This is designed to ease the task of
+ * managing native resources. One can always make an AudioStream leave its
+ * AudioGroup by calling {@link #join(AudioGroup)} with {@code null} and put it
+ * back after the modification is done.</p>
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#INTERNET} permission.</p>
+ *
+ * @see RtpStream
+ * @see AudioGroup
+ */
+public class AudioStream extends RtpStream {
+ private AudioCodec mCodec;
+ 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 has already joined an
+ * {@link AudioGroup}.
+ */
+ @Override
+ public final boolean isBusy() {
+ return mGroup != null;
+ }
+
+ /**
+ * Returns the joined {@link AudioGroup}.
+ */
+ public AudioGroup getGroup() {
+ 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) {
+ synchronized (this) {
+ if (mGroup == group) {
+ return;
+ }
+ if (mGroup != null) {
+ mGroup.remove(this);
+ mGroup = null;
+ }
+ if (group != null) {
+ group.add(this);
+ mGroup = group;
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link AudioCodec}, or {@code null} if it is not set.
+ *
+ * @see #setCodec(AudioCodec)
+ */
+ public AudioCodec getCodec() {
+ return mCodec;
+ }
+
+ /**
+ * Sets the {@link AudioCodec}.
+ *
+ * @param codec The AudioCodec to be used.
+ * @throws IllegalArgumentException if its type is used by DTMF.
+ * @throws IllegalStateException if the stream is busy.
+ */
+ public void setCodec(AudioCodec codec) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ if (codec.type == mDtmfType) {
+ throw new IllegalArgumentException("The type is used by DTMF");
+ }
+ mCodec = codec;
+ }
+
+ /**
+ * Returns the RTP payload type for dual-tone multi-frequency (DTMF) digits,
+ * or {@code -1} if it is not enabled.
+ *
+ * @see #setDtmfType(int)
+ */
+ public int getDtmfType() {
+ return mDtmfType;
+ }
+
+ /**
+ * 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 type. 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 (mCodec != null && type == mCodec.type) {
+ throw new IllegalArgumentException("The type is used by codec");
+ }
+ }
+ mDtmfType = type;
+ }
+}
diff --git a/java/android/net/rtp/RtpStream.java b/java/android/net/rtp/RtpStream.java
new file mode 100644
index 0000000..b9d75cd
--- /dev/null
+++ b/java/android/net/rtp/RtpStream.java
@@ -0,0 +1,195 @@
+/*
+ * 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 the base class of streams which send and receive network
+ * packets with media payloads over Real-time Transport Protocol (RTP).
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#INTERNET} permission.</p>
+ */
+public class RtpStream {
+ /**
+ * This mode indicates that the stream sends and receives packets at the
+ * same time. This is the initial mode for new streams.
+ */
+ public static final int MODE_NORMAL = 0;
+
+ /**
+ * This mode indicates that the stream only sends packets.
+ */
+ public static final int MODE_SEND_ONLY = 1;
+
+ /**
+ * This mode indicates that the stream only receives packets.
+ */
+ public static final int MODE_RECEIVE_ONLY = 2;
+
+ private static final int MODE_LAST = 2;
+
+ private final InetAddress mLocalAddress;
+ private final int mLocalPort;
+
+ private InetAddress mRemoteAddress;
+ private int mRemotePort = -1;
+ private int mMode = MODE_NORMAL;
+
+ private int mSocket = -1;
+ 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. In this case most of the
+ * setter methods are disabled. This method is intended to be overridden
+ * by subclasses.
+ */
+ public boolean isBusy() {
+ return false;
+ }
+
+ /**
+ * Returns the current mode.
+ */
+ 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 < 0 || mode > MODE_LAST) {
+ throw new IllegalArgumentException("Invalid mode");
+ }
+ mMode = mode;
+ }
+
+ /**
+ * Associates with a remote host. This defines the destination of the
+ * outgoing packets.
+ *
+ * @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;
+ }
+
+ int getSocket() {
+ return mSocket;
+ }
+
+ /**
+ * Releases allocated resources. The stream becomes inoperable after calling
+ * this method.
+ *
+ * @throws IllegalStateException if the stream is busy.
+ * @see #isBusy()
+ */
+ public void release() {
+ synchronized (this) {
+ if (isBusy()) {
+ throw new IllegalStateException("Busy");
+ }
+ close();
+ }
+ }
+
+ private native void close();
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+}
diff --git a/java/android/net/rtp/package.html b/java/android/net/rtp/package.html
new file mode 100644
index 0000000..4506b09
--- /dev/null
+++ b/java/android/net/rtp/package.html
@@ -0,0 +1,28 @@
+<html>
+<body>
+<p>Provides APIs for RTP (Real-time Transport Protocol), allowing applications to manage on-demand
+or interactive data streaming. In particular, apps that provide VOIP, push-to-talk, conferencing,
+and audio streaming can use these APIs to initiate sessions and transmit or receive data streams
+over any available network.</p>
+
+<p>To support audio conferencing and similar usages, you need to instantiate two classes as
+endpoints for the stream:</p>
+
+<ul>
+<li>{@link android.net.rtp.AudioStream} specifies a remote endpoint and consists of network mapping
+and a configured {@link android.net.rtp.AudioCodec}.</li>
+
+<li>{@link android.net.rtp.AudioGroup} represents the local endpoint for one or more {@link
+android.net.rtp.AudioStream}s. The {@link android.net.rtp.AudioGroup} mixes all the {@link
+android.net.rtp.AudioStream}s and optionally interacts with the device speaker and the microphone at
+the same time.</li>
+</ul>
+
+<p>The simplest usage involves a single remote endpoint and local endpoint. For more complex usages,
+refer to the limitations described for {@link android.net.rtp.AudioGroup}.</p>
+
+<p class="note"><strong>Note:</strong> To use the RTP APIs, you must request the {@link
+android.Manifest.permission#INTERNET} and {@link
+android.Manifest.permission#RECORD_AUDIO} permissions in your manifest file.</p>
+</body>
+</html> \ No newline at end of file
diff --git a/java/android/net/sip/ISipService.aidl b/java/android/net/sip/ISipService.aidl
new file mode 100644
index 0000000..3250bf9
--- /dev/null
+++ b/java/android/net/sip/ISipService.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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.app.PendingIntent;
+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,
+ in PendingIntent incomingCallPendingIntent,
+ 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/java/android/net/sip/ISipSession.aidl b/java/android/net/sip/ISipSession.aidl
new file mode 100644
index 0000000..2d515db
--- /dev/null
+++ b/java/android/net/sip/ISipSession.aidl
@@ -0,0 +1,148 @@
+/*
+ * 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.SipProfile;
+
+/**
+ * A SIP session that is associated with a SIP dialog or a transaction that is
+ * 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}.
+ *
+ * @return the session state
+ */
+ int 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
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds
+ * @see ISipSessionListener
+ */
+ void makeCall(in SipProfile callee, String sessionDescription, int timeout);
+
+ /**
+ * 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
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds
+ */
+ void answerCall(String sessionDescription, int timeout);
+
+ /**
+ * 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
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds
+ */
+ void changeCall(String sessionDescription, int timeout);
+}
diff --git a/java/android/net/sip/ISipSessionListener.aidl b/java/android/net/sip/ISipSessionListener.aidl
new file mode 100644
index 0000000..690700c
--- /dev/null
+++ b/java/android/net/sip/ISipSessionListener.aidl
@@ -0,0 +1,133 @@
+/*
+ * 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,
+ String 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,
+ String 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 the call is being transferred to a new one.
+ *
+ * @param newSession the new session that the call will be transferred to
+ * @param sessionDescription the new peer's session description
+ */
+ void onCallTransferring(in ISipSession newSession, String sessionDescription);
+
+ /**
+ * Called when an error occurs during session initialization and
+ * termination.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ void onError(in ISipSession session, int errorCode, String errorMessage);
+
+ /**
+ * Called when an error occurs during session modification negotiation.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ void onCallChangeFailed(in ISipSession session, int errorCode,
+ 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 errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ void onRegistrationFailed(in ISipSession session, int errorCode,
+ 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/java/android/net/sip/SimpleSessionDescription.java b/java/android/net/sip/SimpleSessionDescription.java
new file mode 100644
index 0000000..9fcd21d
--- /dev/null
+++ b/java/android/net/sip/SimpleSessionDescription.java
@@ -0,0 +1,613 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * An object used to manipulate messages of Session Description Protocol (SDP).
+ * It is mainly designed for the uses of Session Initiation Protocol (SIP).
+ * Therefore, it only handles connection addresses ("c="), bandwidth limits,
+ * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this
+ * implementation does not support multicast sessions.
+ *
+ * <p>Here is an example code to create a session description.</p>
+ * <pre>
+ * SimpleSessionDescription description = new SimpleSessionDescription(
+ * System.currentTimeMillis(), "1.2.3.4");
+ * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP");
+ * media.setRtpPayload(0, "PCMU/8000", null);
+ * media.setRtpPayload(8, "PCMA/8000", null);
+ * media.setRtpPayload(127, "telephone-event/8000", "0-15");
+ * media.setAttribute("sendrecv", "");
+ * </pre>
+ * <p>Invoking <code>description.encode()</code> will produce a result like the
+ * one below.</p>
+ * <pre>
+ * v=0
+ * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4
+ * s=-
+ * c=IN IP4 1.2.3.4
+ * t=0 0
+ * m=audio 56789 RTP/AVP 0 8 127
+ * a=rtpmap:0 PCMU/8000
+ * a=rtpmap:8 PCMA/8000
+ * a=rtpmap:127 telephone-event/8000
+ * a=fmtp:127 0-15
+ * a=sendrecv
+ * </pre>
+ * @hide
+ */
+public class SimpleSessionDescription {
+ private final Fields mFields = new Fields("voscbtka");
+ private final ArrayList<Media> mMedia = new ArrayList<Media>();
+
+ /**
+ * Creates a minimal session description from the given session ID and
+ * unicast address. The address is used in the origin field ("o=") and the
+ * connection field ("c="). See {@link SimpleSessionDescription} for an
+ * example of its usage.
+ */
+ public SimpleSessionDescription(long sessionId, String address) {
+ address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address;
+ mFields.parse("v=0");
+ mFields.parse(String.format(Locale.US, "o=- %d %d %s", sessionId,
+ System.currentTimeMillis(), address));
+ mFields.parse("s=-");
+ mFields.parse("t=0 0");
+ mFields.parse("c=" + address);
+ }
+
+ /**
+ * Creates a session description from the given message.
+ *
+ * @throws IllegalArgumentException if message is invalid.
+ */
+ public SimpleSessionDescription(String message) {
+ String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+");
+ Fields fields = mFields;
+
+ for (String line : lines) {
+ try {
+ if (line.charAt(1) != '=') {
+ throw new IllegalArgumentException();
+ }
+ if (line.charAt(0) == 'm') {
+ String[] parts = line.substring(2).split(" ", 4);
+ String[] ports = parts[1].split("/", 2);
+ Media media = newMedia(parts[0], Integer.parseInt(ports[0]),
+ (ports.length < 2) ? 1 : Integer.parseInt(ports[1]),
+ parts[2]);
+ for (String format : parts[3].split(" ")) {
+ media.setFormat(format, null);
+ }
+ fields = media;
+ } else {
+ fields.parse(line);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid SDP: " + line);
+ }
+ }
+ }
+
+ /**
+ * Creates a new media description in this session description.
+ *
+ * @param type The media type, e.g. {@code "audio"}.
+ * @param port The first transport port used by this media.
+ * @param portCount The number of contiguous ports used by this media.
+ * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
+ */
+ public Media newMedia(String type, int port, int portCount,
+ String protocol) {
+ Media media = new Media(type, port, portCount, protocol);
+ mMedia.add(media);
+ return media;
+ }
+
+ /**
+ * Returns all the media descriptions in this session description.
+ */
+ public Media[] getMedia() {
+ return mMedia.toArray(new Media[mMedia.size()]);
+ }
+
+ /**
+ * Encodes the session description and all its media descriptions in a
+ * string. Note that the result might be incomplete if a required field
+ * has never been added before.
+ */
+ public String encode() {
+ StringBuilder buffer = new StringBuilder();
+ mFields.write(buffer);
+ for (Media media : mMedia) {
+ media.write(buffer);
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the connection address or {@code null} if it is not present.
+ */
+ public String getAddress() {
+ return mFields.getAddress();
+ }
+
+ /**
+ * Sets the connection address. The field will be removed if the address
+ * is {@code null}.
+ */
+ public void setAddress(String address) {
+ mFields.setAddress(address);
+ }
+
+ /**
+ * Returns the encryption method or {@code null} if it is not present.
+ */
+ public String getEncryptionMethod() {
+ return mFields.getEncryptionMethod();
+ }
+
+ /**
+ * Returns the encryption key or {@code null} if it is not present.
+ */
+ public String getEncryptionKey() {
+ return mFields.getEncryptionKey();
+ }
+
+ /**
+ * Sets the encryption method and the encryption key. The field will be
+ * removed if the method is {@code null}.
+ */
+ public void setEncryption(String method, String key) {
+ mFields.setEncryption(method, key);
+ }
+
+ /**
+ * Returns the types of the bandwidth limits.
+ */
+ public String[] getBandwidthTypes() {
+ return mFields.getBandwidthTypes();
+ }
+
+ /**
+ * Returns the bandwidth limit of the given type or {@code -1} if it is not
+ * present.
+ */
+ public int getBandwidth(String type) {
+ return mFields.getBandwidth(type);
+ }
+
+ /**
+ * Sets the bandwith limit for the given type. The field will be removed if
+ * the value is negative.
+ */
+ public void setBandwidth(String type, int value) {
+ mFields.setBandwidth(type, value);
+ }
+
+ /**
+ * Returns the names of all the attributes.
+ */
+ public String[] getAttributeNames() {
+ return mFields.getAttributeNames();
+ }
+
+ /**
+ * Returns the attribute of the given name or {@code null} if it is not
+ * present.
+ */
+ public String getAttribute(String name) {
+ return mFields.getAttribute(name);
+ }
+
+ /**
+ * Sets the attribute for the given name. The field will be removed if
+ * the value is {@code null}. To set a binary attribute, use an empty
+ * string as the value.
+ */
+ public void setAttribute(String name, String value) {
+ mFields.setAttribute(name, value);
+ }
+
+ /**
+ * This class represents a media description of a session description. It
+ * can only be created by {@link SimpleSessionDescription#newMedia}. Since
+ * the syntax is more restricted for RTP based protocols, two sets of access
+ * methods are implemented. See {@link SimpleSessionDescription} for an
+ * example of its usage.
+ */
+ public static class Media extends Fields {
+ private final String mType;
+ private final int mPort;
+ private final int mPortCount;
+ private final String mProtocol;
+ private ArrayList<String> mFormats = new ArrayList<String>();
+
+ private Media(String type, int port, int portCount, String protocol) {
+ super("icbka");
+ mType = type;
+ mPort = port;
+ mPortCount = portCount;
+ mProtocol = protocol;
+ }
+
+ /**
+ * Returns the media type.
+ */
+ public String getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the first transport port used by this media.
+ */
+ public int getPort() {
+ return mPort;
+ }
+
+ /**
+ * Returns the number of contiguous ports used by this media.
+ */
+ public int getPortCount() {
+ return mPortCount;
+ }
+
+ /**
+ * Returns the transport protocol.
+ */
+ public String getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * Returns the media formats.
+ */
+ public String[] getFormats() {
+ return mFormats.toArray(new String[mFormats.size()]);
+ }
+
+ /**
+ * Returns the {@code fmtp} attribute of the given format or
+ * {@code null} if it is not present.
+ */
+ public String getFmtp(String format) {
+ return super.get("a=fmtp:" + format, ' ');
+ }
+
+ /**
+ * Sets a format and its {@code fmtp} attribute. If the attribute is
+ * {@code null}, the corresponding field will be removed.
+ */
+ public void setFormat(String format, String fmtp) {
+ mFormats.remove(format);
+ mFormats.add(format);
+ super.set("a=rtpmap:" + format, ' ', null);
+ super.set("a=fmtp:" + format, ' ', fmtp);
+ }
+
+ /**
+ * Removes a format and its {@code fmtp} attribute.
+ */
+ public void removeFormat(String format) {
+ mFormats.remove(format);
+ super.set("a=rtpmap:" + format, ' ', null);
+ super.set("a=fmtp:" + format, ' ', null);
+ }
+
+ /**
+ * Returns the RTP payload types.
+ */
+ public int[] getRtpPayloadTypes() {
+ int[] types = new int[mFormats.size()];
+ int length = 0;
+ for (String format : mFormats) {
+ try {
+ types[length] = Integer.parseInt(format);
+ ++length;
+ } catch (NumberFormatException e) { }
+ }
+ return Arrays.copyOf(types, length);
+ }
+
+ /**
+ * Returns the {@code rtpmap} attribute of the given RTP payload type
+ * or {@code null} if it is not present.
+ */
+ public String getRtpmap(int type) {
+ return super.get("a=rtpmap:" + type, ' ');
+ }
+
+ /**
+ * Returns the {@code fmtp} attribute of the given RTP payload type or
+ * {@code null} if it is not present.
+ */
+ public String getFmtp(int type) {
+ return super.get("a=fmtp:" + type, ' ');
+ }
+
+ /**
+ * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp}
+ * attributes. If any of the attributes is {@code null}, the
+ * corresponding field will be removed. See
+ * {@link SimpleSessionDescription} for an example of its usage.
+ */
+ public void setRtpPayload(int type, String rtpmap, String fmtp) {
+ String format = String.valueOf(type);
+ mFormats.remove(format);
+ mFormats.add(format);
+ super.set("a=rtpmap:" + format, ' ', rtpmap);
+ super.set("a=fmtp:" + format, ' ', fmtp);
+ }
+
+ /**
+ * Removes a RTP payload and its {@code rtpmap} and {@code fmtp}
+ * attributes.
+ */
+ public void removeRtpPayload(int type) {
+ removeFormat(String.valueOf(type));
+ }
+
+ private void write(StringBuilder buffer) {
+ buffer.append("m=").append(mType).append(' ').append(mPort);
+ if (mPortCount != 1) {
+ buffer.append('/').append(mPortCount);
+ }
+ buffer.append(' ').append(mProtocol);
+ for (String format : mFormats) {
+ buffer.append(' ').append(format);
+ }
+ buffer.append("\r\n");
+ super.write(buffer);
+ }
+ }
+
+ /**
+ * This class acts as a set of fields, and the size of the set is expected
+ * to be small. Therefore, it uses a simple list instead of maps. Each field
+ * has three parts: a key, a delimiter, and a value. Delimiters are special
+ * because they are not included in binary attributes. As a result, the
+ * private methods, which are the building blocks of this class, all take
+ * the delimiter as an argument.
+ */
+ private static class Fields {
+ private final String mOrder;
+ private final ArrayList<String> mLines = new ArrayList<String>();
+
+ Fields(String order) {
+ mOrder = order;
+ }
+
+ /**
+ * Returns the connection address or {@code null} if it is not present.
+ */
+ public String getAddress() {
+ String address = get("c", '=');
+ if (address == null) {
+ return null;
+ }
+ String[] parts = address.split(" ");
+ if (parts.length != 3) {
+ return null;
+ }
+ int slash = parts[2].indexOf('/');
+ return (slash < 0) ? parts[2] : parts[2].substring(0, slash);
+ }
+
+ /**
+ * Sets the connection address. The field will be removed if the address
+ * is {@code null}.
+ */
+ public void setAddress(String address) {
+ if (address != null) {
+ address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") +
+ address;
+ }
+ set("c", '=', address);
+ }
+
+ /**
+ * Returns the encryption method or {@code null} if it is not present.
+ */
+ public String getEncryptionMethod() {
+ String encryption = get("k", '=');
+ if (encryption == null) {
+ return null;
+ }
+ int colon = encryption.indexOf(':');
+ return (colon == -1) ? encryption : encryption.substring(0, colon);
+ }
+
+ /**
+ * Returns the encryption key or {@code null} if it is not present.
+ */
+ public String getEncryptionKey() {
+ String encryption = get("k", '=');
+ if (encryption == null) {
+ return null;
+ }
+ int colon = encryption.indexOf(':');
+ return (colon == -1) ? null : encryption.substring(0, colon + 1);
+ }
+
+ /**
+ * Sets the encryption method and the encryption key. The field will be
+ * removed if the method is {@code null}.
+ */
+ public void setEncryption(String method, String key) {
+ set("k", '=', (method == null || key == null) ?
+ method : method + ':' + key);
+ }
+
+ /**
+ * Returns the types of the bandwidth limits.
+ */
+ public String[] getBandwidthTypes() {
+ return cut("b=", ':');
+ }
+
+ /**
+ * Returns the bandwidth limit of the given type or {@code -1} if it is
+ * not present.
+ */
+ public int getBandwidth(String type) {
+ String value = get("b=" + type, ':');
+ if (value != null) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) { }
+ setBandwidth(type, -1);
+ }
+ return -1;
+ }
+
+ /**
+ * Sets the bandwith limit for the given type. The field will be removed
+ * if the value is negative.
+ */
+ public void setBandwidth(String type, int value) {
+ set("b=" + type, ':', (value < 0) ? null : String.valueOf(value));
+ }
+
+ /**
+ * Returns the names of all the attributes.
+ */
+ public String[] getAttributeNames() {
+ return cut("a=", ':');
+ }
+
+ /**
+ * Returns the attribute of the given name or {@code null} if it is not
+ * present.
+ */
+ public String getAttribute(String name) {
+ return get("a=" + name, ':');
+ }
+
+ /**
+ * Sets the attribute for the given name. The field will be removed if
+ * the value is {@code null}. To set a binary attribute, use an empty
+ * string as the value.
+ */
+ public void setAttribute(String name, String value) {
+ set("a=" + name, ':', value);
+ }
+
+ private void write(StringBuilder buffer) {
+ for (int i = 0; i < mOrder.length(); ++i) {
+ char type = mOrder.charAt(i);
+ for (String line : mLines) {
+ if (line.charAt(0) == type) {
+ buffer.append(line).append("\r\n");
+ }
+ }
+ }
+ }
+
+ /**
+ * Invokes {@link #set} after splitting the line into three parts.
+ */
+ private void parse(String line) {
+ char type = line.charAt(0);
+ if (mOrder.indexOf(type) == -1) {
+ return;
+ }
+ char delimiter = '=';
+ if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) {
+ delimiter = ' ';
+ } else if (type == 'b' || type == 'a') {
+ delimiter = ':';
+ }
+ int i = line.indexOf(delimiter);
+ if (i == -1) {
+ set(line, delimiter, "");
+ } else {
+ set(line.substring(0, i), delimiter, line.substring(i + 1));
+ }
+ }
+
+ /**
+ * Finds the key with the given prefix and returns its suffix.
+ */
+ private String[] cut(String prefix, char delimiter) {
+ String[] names = new String[mLines.size()];
+ int length = 0;
+ for (String line : mLines) {
+ if (line.startsWith(prefix)) {
+ int i = line.indexOf(delimiter);
+ if (i == -1) {
+ i = line.length();
+ }
+ names[length] = line.substring(prefix.length(), i);
+ ++length;
+ }
+ }
+ return Arrays.copyOf(names, length);
+ }
+
+ /**
+ * Returns the index of the key.
+ */
+ private int find(String key, char delimiter) {
+ int length = key.length();
+ for (int i = mLines.size() - 1; i >= 0; --i) {
+ String line = mLines.get(i);
+ if (line.startsWith(key) && (line.length() == length ||
+ line.charAt(length) == delimiter)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Sets the key with the value or removes the key if the value is
+ * {@code null}.
+ */
+ private void set(String key, char delimiter, String value) {
+ int index = find(key, delimiter);
+ if (value != null) {
+ if (value.length() != 0) {
+ key = key + delimiter + value;
+ }
+ if (index == -1) {
+ mLines.add(key);
+ } else {
+ mLines.set(index, key);
+ }
+ } else if (index != -1) {
+ mLines.remove(index);
+ }
+ }
+
+ /**
+ * Returns the value of the key.
+ */
+ private String get(String key, char delimiter) {
+ int index = find(key, delimiter);
+ if (index == -1) {
+ return null;
+ }
+ String line = mLines.get(index);
+ int length = key.length();
+ return (line.length() == length) ? "" : line.substring(length + 1);
+ }
+ }
+}
diff --git a/java/android/net/sip/SipAudioCall.java b/java/android/net/sip/SipAudioCall.java
new file mode 100644
index 0000000..ea943e9
--- /dev/null
+++ b/java/android/net/sip/SipAudioCall.java
@@ -0,0 +1,1143 @@
+/*
+ * 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.media.AudioManager;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.net.sip.SimpleSessionDescription.Media;
+import android.net.wifi.WifiManager;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager},
+ * using {@link SipManager#makeAudioCall makeAudioCall()} and {@link SipManager#takeAudioCall
+ * takeAudioCall()}.
+ *
+ * <p class="note"><strong>Note:</strong> Using this class require the
+ * {@link android.Manifest.permission#INTERNET} and
+ * {@link android.Manifest.permission#USE_SIP} permissions. In addition, {@link
+ * #startAudio} requires the
+ * {@link android.Manifest.permission#RECORD_AUDIO},
+ * {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and
+ * {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode
+ * setSpeakerMode()} requires the
+ * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipAudioCall {
+ private static final String LOG_TAG = SipAudioCall.class.getSimpleName();
+ private static final boolean DBG = true;
+ private static final boolean RELEASE_SOCKET = true;
+ private static final boolean DONT_RELEASE_SOCKET = false;
+ private static final int SESSION_TIMEOUT = 5; // in seconds
+ private static final int TRANSFER_TIMEOUT = 15; // in seconds
+
+ /** Listener for events relating to a SIP call, such as when a call is being
+ * recieved ("on ringing") or a call is outgoing ("on calling").
+ * <p>Many of these events are also received by {@link SipSession.Listener}.</p>
+ */
+ public static class Listener {
+ /**
+ * Called when the call object is ready to make another call.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that is ready to make another call
+ */
+ public void onReadyToCall(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when a request is sent out to initiate a new call.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onCalling(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when a new call comes in.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ * @param caller the SIP profile of the caller
+ */
+ public void onRinging(SipAudioCall call, SipProfile caller) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when a RINGING response is received for the INVITE request
+ * sent. The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onRingingBack(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when the session is established.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onCallEstablished(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when the session is terminated.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onCallEnded(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when the peer is busy during session initialization.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onCallBusy(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when the call is on hold.
+ * The default implementation calls {@link #onChanged}.
+ *
+ * @param call the call object that carries out the audio call
+ */
+ public void onCallHeld(SipAudioCall call) {
+ onChanged(call);
+ }
+
+ /**
+ * Called when an error occurs. The default implementation is no op.
+ *
+ * @param call the call object that carries out the audio call
+ * @param errorCode error code of this error
+ * @param errorMessage error message
+ * @see SipErrorCode
+ */
+ public void onError(SipAudioCall call, int errorCode,
+ String errorMessage) {
+ // no-op
+ }
+
+ /**
+ * Called when an event occurs and the corresponding callback is not
+ * overridden. The default implementation is no op. Error events are
+ * not re-directed to this callback and are handled in {@link #onError}.
+ */
+ public void onChanged(SipAudioCall call) {
+ // no-op
+ }
+ }
+
+ private Context mContext;
+ private SipProfile mLocalProfile;
+ private SipAudioCall.Listener mListener;
+ private SipSession mSipSession;
+ private SipSession mTransferringSession;
+
+ private long mSessionId = System.currentTimeMillis();
+ private String mPeerSd;
+
+ private AudioStream mAudioStream;
+ private AudioGroup mAudioGroup;
+
+ private boolean mInCall = false;
+ private boolean mMuted = false;
+ private boolean mHold = false;
+
+ private WifiManager mWm;
+ private WifiManager.WifiLock mWifiHighPerfLock;
+
+ private int mErrorCode = SipErrorCode.NO_ERROR;
+ private String mErrorMessage;
+
+ /**
+ * Creates a call object with the local SIP profile.
+ * @param context the context for accessing system services such as
+ * ringtone, audio, WIFI etc
+ */
+ public SipAudioCall(Context context, SipProfile localProfile) {
+ mContext = context;
+ mLocalProfile = localProfile;
+ mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ /**
+ * Sets the listener to listen to the audio call events. The method calls
+ * {@link #setListener setListener(listener, false)}.
+ *
+ * @param listener to listen to the audio call events of this object
+ * @see #setListener(Listener, boolean)
+ */
+ public void setListener(SipAudioCall.Listener listener) {
+ setListener(listener, false);
+ }
+
+ /**
+ * 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
+ */
+ public void setListener(SipAudioCall.Listener listener,
+ boolean callbackImmediately) {
+ mListener = listener;
+ try {
+ if ((listener == null) || !callbackImmediately) {
+ // do nothing
+ } else if (mErrorCode != SipErrorCode.NO_ERROR) {
+ listener.onError(this, mErrorCode, mErrorMessage);
+ } else if (mInCall) {
+ if (mHold) {
+ listener.onCallHeld(this);
+ } else {
+ listener.onCallEstablished(this);
+ }
+ } else {
+ int state = getState();
+ switch (state) {
+ case SipSession.State.READY_TO_CALL:
+ listener.onReadyToCall(this);
+ break;
+ case SipSession.State.INCOMING_CALL:
+ listener.onRinging(this, getPeerProfile());
+ break;
+ case SipSession.State.OUTGOING_CALL:
+ listener.onCalling(this);
+ break;
+ case SipSession.State.OUTGOING_CALL_RING_BACK:
+ listener.onRingingBack(this);
+ break;
+ }
+ }
+ } catch (Throwable t) {
+ loge("setListener()", t);
+ }
+ }
+
+ /**
+ * Checks if the call is established.
+ *
+ * @return true if the call is established
+ */
+ public boolean isInCall() {
+ synchronized (this) {
+ return mInCall;
+ }
+ }
+
+ /**
+ * Checks if the call is on hold.
+ *
+ * @return true if the call is on hold
+ */
+ public boolean isOnHold() {
+ synchronized (this) {
+ return mHold;
+ }
+ }
+
+ /**
+ * Closes this object. This object is not usable after being closed.
+ */
+ public void close() {
+ close(true);
+ }
+
+ private synchronized void close(boolean closeRtp) {
+ if (closeRtp) stopCall(RELEASE_SOCKET);
+
+ mInCall = false;
+ mHold = false;
+ mSessionId = System.currentTimeMillis();
+ mErrorCode = SipErrorCode.NO_ERROR;
+ mErrorMessage = null;
+
+ if (mSipSession != null) {
+ mSipSession.setListener(null);
+ mSipSession = null;
+ }
+ }
+
+ /**
+ * Gets the local SIP profile.
+ *
+ * @return the local SIP profile
+ */
+ public SipProfile getLocalProfile() {
+ synchronized (this) {
+ return mLocalProfile;
+ }
+ }
+
+ /**
+ * Gets the peer's SIP profile.
+ *
+ * @return the peer's SIP profile
+ */
+ public SipProfile getPeerProfile() {
+ synchronized (this) {
+ return (mSipSession == null) ? null : mSipSession.getPeerProfile();
+ }
+ }
+
+ /**
+ * Gets the state of the {@link SipSession} that carries this call.
+ * The value returned must be one of the states in {@link SipSession.State}.
+ *
+ * @return the session state
+ */
+ public int getState() {
+ synchronized (this) {
+ if (mSipSession == null) return SipSession.State.READY_TO_CALL;
+ return mSipSession.getState();
+ }
+ }
+
+
+ /**
+ * Gets the {@link SipSession} that carries this call.
+ *
+ * @return the session object that carries this call
+ * @hide
+ */
+ public SipSession getSipSession() {
+ synchronized (this) {
+ return mSipSession;
+ }
+ }
+
+ private synchronized void transferToNewSession() {
+ if (mTransferringSession == null) return;
+ SipSession origin = mSipSession;
+ mSipSession = mTransferringSession;
+ mTransferringSession = null;
+
+ // stop the replaced call.
+ if (mAudioStream != null) {
+ mAudioStream.join(null);
+ } else {
+ try {
+ mAudioStream = new AudioStream(InetAddress.getByName(
+ getLocalIp()));
+ } catch (Throwable t) {
+ loge("transferToNewSession():", t);
+ }
+ }
+ if (origin != null) origin.endCall();
+ startAudio();
+ }
+
+ private SipSession.Listener createListener() {
+ return new SipSession.Listener() {
+ @Override
+ public void onCalling(SipSession session) {
+ if (DBG) log("onCalling: session=" + session);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCalling(SipAudioCall.this);
+ } catch (Throwable t) {
+ loge("onCalling():", t);
+ }
+ }
+ }
+
+ @Override
+ public void onRingingBack(SipSession session) {
+ if (DBG) log("onRingingBackk: " + session);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onRingingBack(SipAudioCall.this);
+ } catch (Throwable t) {
+ loge("onRingingBack():", t);
+ }
+ }
+ }
+
+ @Override
+ public void onRinging(SipSession session,
+ SipProfile peerProfile, String sessionDescription) {
+ // this callback is triggered only for reinvite.
+ synchronized (SipAudioCall.this) {
+ if ((mSipSession == null) || !mInCall
+ || !session.getCallId().equals(
+ mSipSession.getCallId())) {
+ // should not happen
+ session.endCall();
+ return;
+ }
+
+ // session changing request
+ try {
+ String answer = createAnswer(sessionDescription).encode();
+ mSipSession.answerCall(answer, SESSION_TIMEOUT);
+ } catch (Throwable e) {
+ loge("onRinging():", e);
+ session.endCall();
+ }
+ }
+ }
+
+ @Override
+ public void onCallEstablished(SipSession session,
+ String sessionDescription) {
+ mPeerSd = sessionDescription;
+ if (DBG) log("onCallEstablished(): " + mPeerSd);
+
+ // TODO: how to notify the UI that the remote party is changed
+ if ((mTransferringSession != null)
+ && (session == mTransferringSession)) {
+ transferToNewSession();
+ return;
+ }
+
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ if (mHold) {
+ listener.onCallHeld(SipAudioCall.this);
+ } else {
+ listener.onCallEstablished(SipAudioCall.this);
+ }
+ } catch (Throwable t) {
+ loge("onCallEstablished(): ", t);
+ }
+ }
+ }
+
+ @Override
+ public void onCallEnded(SipSession session) {
+ if (DBG) log("onCallEnded: " + session + " mSipSession:" + mSipSession);
+ // reset the trasnferring session if it is the one.
+ if (session == mTransferringSession) {
+ mTransferringSession = null;
+ return;
+ }
+ // or ignore the event if the original session is being
+ // transferred to the new one.
+ if ((mTransferringSession != null) ||
+ (session != mSipSession)) return;
+
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCallEnded(SipAudioCall.this);
+ } catch (Throwable t) {
+ loge("onCallEnded(): ", t);
+ }
+ }
+ close();
+ }
+
+ @Override
+ public void onCallBusy(SipSession session) {
+ if (DBG) log("onCallBusy: " + session);
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onCallBusy(SipAudioCall.this);
+ } catch (Throwable t) {
+ loge("onCallBusy(): ", t);
+ }
+ }
+ close(false);
+ }
+
+ @Override
+ public void onCallChangeFailed(SipSession session, int errorCode,
+ String message) {
+ if (DBG) log("onCallChangedFailed: " + message);
+ mErrorCode = errorCode;
+ mErrorMessage = message;
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onError(SipAudioCall.this, mErrorCode,
+ message);
+ } catch (Throwable t) {
+ loge("onCallBusy():", t);
+ }
+ }
+ }
+
+ @Override
+ public void onError(SipSession session, int errorCode,
+ String message) {
+ SipAudioCall.this.onError(errorCode, message);
+ }
+
+ @Override
+ public void onRegistering(SipSession session) {
+ // irrelevant
+ }
+
+ @Override
+ public void onRegistrationTimeout(SipSession session) {
+ // irrelevant
+ }
+
+ @Override
+ public void onRegistrationFailed(SipSession session, int errorCode,
+ String message) {
+ // irrelevant
+ }
+
+ @Override
+ public void onRegistrationDone(SipSession session, int duration) {
+ // irrelevant
+ }
+
+ @Override
+ public void onCallTransferring(SipSession newSession,
+ String sessionDescription) {
+ if (DBG) log("onCallTransferring: mSipSession="
+ + mSipSession + " newSession=" + newSession);
+ mTransferringSession = newSession;
+ try {
+ if (sessionDescription == null) {
+ newSession.makeCall(newSession.getPeerProfile(),
+ createOffer().encode(), TRANSFER_TIMEOUT);
+ } else {
+ String answer = createAnswer(sessionDescription).encode();
+ newSession.answerCall(answer, SESSION_TIMEOUT);
+ }
+ } catch (Throwable e) {
+ loge("onCallTransferring()", e);
+ newSession.endCall();
+ }
+ }
+ };
+ }
+
+ private void onError(int errorCode, String message) {
+ if (DBG) log("onError: "
+ + SipErrorCode.toString(errorCode) + ": " + message);
+ mErrorCode = errorCode;
+ mErrorMessage = message;
+ Listener listener = mListener;
+ if (listener != null) {
+ try {
+ listener.onError(this, errorCode, message);
+ } catch (Throwable t) {
+ loge("onError():", t);
+ }
+ }
+ synchronized (this) {
+ if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST)
+ || !isInCall()) {
+ close(true);
+ }
+ }
+ }
+
+ /**
+ * Attaches an incoming call to this call object.
+ *
+ * @param session the session that receives the incoming call
+ * @param sessionDescription the session description of the incoming call
+ * @throws SipException if the SIP service fails to attach this object to
+ * the session or VOIP API is not supported by the device
+ * @see SipManager#isVoipSupported
+ */
+ public void attachCall(SipSession session, String sessionDescription)
+ throws SipException {
+ if (!SipManager.isVoipSupported(mContext)) {
+ throw new SipException("VOIP API is not supported");
+ }
+
+ synchronized (this) {
+ mSipSession = session;
+ mPeerSd = sessionDescription;
+ if (DBG) log("attachCall(): " + mPeerSd);
+ try {
+ session.setListener(createListener());
+ } catch (Throwable e) {
+ loge("attachCall()", e);
+ throwSipException(e);
+ }
+ }
+ }
+
+ /**
+ * Initiates an audio call to the specified profile. The attempt will be
+ * timed out if the call is not established within {@code timeout} seconds
+ * and {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @param peerProfile the SIP profile to make the call to
+ * @param sipSession the {@link SipSession} for carrying out the call
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @see Listener#onError
+ * @throws SipException if the SIP service fails to create a session for the
+ * call or VOIP API is not supported by the device
+ * @see SipManager#isVoipSupported
+ */
+ public void makeCall(SipProfile peerProfile, SipSession sipSession,
+ int timeout) throws SipException {
+ if (DBG) log("makeCall: " + peerProfile + " session=" + sipSession + " timeout=" + timeout);
+ if (!SipManager.isVoipSupported(mContext)) {
+ throw new SipException("VOIP API is not supported");
+ }
+
+ synchronized (this) {
+ mSipSession = sipSession;
+ try {
+ mAudioStream = new AudioStream(InetAddress.getByName(
+ getLocalIp()));
+ sipSession.setListener(createListener());
+ sipSession.makeCall(peerProfile, createOffer().encode(),
+ timeout);
+ } catch (IOException e) {
+ loge("makeCall:", e);
+ throw new SipException("makeCall()", e);
+ }
+ }
+ }
+
+ /**
+ * Ends a call.
+ * @throws SipException if the SIP service fails to end the call
+ */
+ public void endCall() throws SipException {
+ if (DBG) log("endCall: mSipSession" + mSipSession);
+ synchronized (this) {
+ stopCall(RELEASE_SOCKET);
+ mInCall = false;
+
+ // perform the above local ops first and then network op
+ if (mSipSession != null) mSipSession.endCall();
+ }
+ }
+
+ /**
+ * Puts a call on hold. When succeeds, {@link Listener#onCallHeld} is
+ * called. The attempt will be timed out if the call is not established
+ * within {@code timeout} seconds and
+ * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @see Listener#onError
+ * @throws SipException if the SIP service fails to hold the call
+ */
+ public void holdCall(int timeout) throws SipException {
+ if (DBG) log("holdCall: mSipSession" + mSipSession + " timeout=" + timeout);
+ synchronized (this) {
+ if (mHold) return;
+ if (mSipSession == null) {
+ loge("holdCall:");
+ throw new SipException("Not in a call to hold call");
+ }
+ mSipSession.changeCall(createHoldOffer().encode(), timeout);
+ mHold = true;
+ setAudioGroupMode();
+ }
+ }
+
+ /**
+ * Answers a call. The attempt will be timed out if the call is not
+ * established within {@code timeout} seconds and
+ * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @see Listener#onError
+ * @throws SipException if the SIP service fails to answer the call
+ */
+ public void answerCall(int timeout) throws SipException {
+ if (DBG) log("answerCall: mSipSession" + mSipSession + " timeout=" + timeout);
+ synchronized (this) {
+ if (mSipSession == null) {
+ throw new SipException("No call to answer");
+ }
+ try {
+ mAudioStream = new AudioStream(InetAddress.getByName(
+ getLocalIp()));
+ mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout);
+ } catch (IOException e) {
+ loge("answerCall:", e);
+ throw new SipException("answerCall()", e);
+ }
+ }
+ }
+
+ /**
+ * Continues a call that's on hold. When succeeds,
+ * {@link Listener#onCallEstablished} is called. The attempt will be timed
+ * out if the call is not established within {@code timeout} seconds and
+ * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @see Listener#onError
+ * @throws SipException if the SIP service fails to unhold the call
+ */
+ public void continueCall(int timeout) throws SipException {
+ if (DBG) log("continueCall: mSipSession" + mSipSession + " timeout=" + timeout);
+ synchronized (this) {
+ if (!mHold) return;
+ mSipSession.changeCall(createContinueOffer().encode(), timeout);
+ mHold = false;
+ setAudioGroupMode();
+ }
+ }
+
+ private SimpleSessionDescription createOffer() {
+ SimpleSessionDescription offer =
+ new SimpleSessionDescription(mSessionId, getLocalIp());
+ AudioCodec[] codecs = AudioCodec.getCodecs();
+ Media media = offer.newMedia(
+ "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+ for (AudioCodec codec : AudioCodec.getCodecs()) {
+ media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+ }
+ media.setRtpPayload(127, "telephone-event/8000", "0-15");
+ if (DBG) log("createOffer: offer=" + offer);
+ return offer;
+ }
+
+ private SimpleSessionDescription createAnswer(String offerSd) {
+ if (TextUtils.isEmpty(offerSd)) return createOffer();
+ SimpleSessionDescription offer =
+ new SimpleSessionDescription(offerSd);
+ SimpleSessionDescription answer =
+ new SimpleSessionDescription(mSessionId, getLocalIp());
+ AudioCodec codec = null;
+ for (Media media : offer.getMedia()) {
+ if ((codec == null) && (media.getPort() > 0)
+ && "audio".equals(media.getType())
+ && "RTP/AVP".equals(media.getProtocol())) {
+ // Find the first audio codec we supported.
+ for (int type : media.getRtpPayloadTypes()) {
+ codec = AudioCodec.getCodec(type, media.getRtpmap(type),
+ media.getFmtp(type));
+ if (codec != null) {
+ break;
+ }
+ }
+ if (codec != null) {
+ Media reply = answer.newMedia(
+ "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+ reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+
+ // Check if DTMF is supported in the same media.
+ for (int type : media.getRtpPayloadTypes()) {
+ String rtpmap = media.getRtpmap(type);
+ if ((type != codec.type) && (rtpmap != null)
+ && rtpmap.startsWith("telephone-event")) {
+ reply.setRtpPayload(
+ type, rtpmap, media.getFmtp(type));
+ }
+ }
+
+ // Handle recvonly and sendonly.
+ if (media.getAttribute("recvonly") != null) {
+ answer.setAttribute("sendonly", "");
+ } else if(media.getAttribute("sendonly") != null) {
+ answer.setAttribute("recvonly", "");
+ } else if(offer.getAttribute("recvonly") != null) {
+ answer.setAttribute("sendonly", "");
+ } else if(offer.getAttribute("sendonly") != null) {
+ answer.setAttribute("recvonly", "");
+ }
+ continue;
+ }
+ }
+ // Reject the media.
+ Media reply = answer.newMedia(
+ media.getType(), 0, 1, media.getProtocol());
+ for (String format : media.getFormats()) {
+ reply.setFormat(format, null);
+ }
+ }
+ if (codec == null) {
+ loge("createAnswer: no suitable codes");
+ throw new IllegalStateException("Reject SDP: no suitable codecs");
+ }
+ if (DBG) log("createAnswer: answer=" + answer);
+ return answer;
+ }
+
+ private SimpleSessionDescription createHoldOffer() {
+ SimpleSessionDescription offer = createContinueOffer();
+ offer.setAttribute("sendonly", "");
+ if (DBG) log("createHoldOffer: offer=" + offer);
+ return offer;
+ }
+
+ private SimpleSessionDescription createContinueOffer() {
+ if (DBG) log("createContinueOffer");
+ SimpleSessionDescription offer =
+ new SimpleSessionDescription(mSessionId, getLocalIp());
+ Media media = offer.newMedia(
+ "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+ AudioCodec codec = mAudioStream.getCodec();
+ media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+ int dtmfType = mAudioStream.getDtmfType();
+ if (dtmfType != -1) {
+ media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15");
+ }
+ return offer;
+ }
+
+ private void grabWifiHighPerfLock() {
+ if (mWifiHighPerfLock == null) {
+ if (DBG) log("grabWifiHighPerfLock:");
+ mWifiHighPerfLock = ((WifiManager)
+ mContext.getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOG_TAG);
+ mWifiHighPerfLock.acquire();
+ }
+ }
+
+ private void releaseWifiHighPerfLock() {
+ if (mWifiHighPerfLock != null) {
+ if (DBG) log("releaseWifiHighPerfLock:");
+ mWifiHighPerfLock.release();
+ mWifiHighPerfLock = null;
+ }
+ }
+
+ private boolean isWifiOn() {
+ return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
+ }
+
+ /** Toggles mute. */
+ public void toggleMute() {
+ synchronized (this) {
+ mMuted = !mMuted;
+ setAudioGroupMode();
+ }
+ }
+
+ /**
+ * Checks if the call is muted.
+ *
+ * @return true if the call is muted
+ */
+ public boolean isMuted() {
+ synchronized (this) {
+ return mMuted;
+ }
+ }
+
+ /**
+ * Puts the device to speaker mode.
+ * <p class="note"><strong>Note:</strong> Requires the
+ * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
+ *
+ * @param speakerMode set true to enable speaker mode; false to disable
+ */
+ public void setSpeakerMode(boolean speakerMode) {
+ synchronized (this) {
+ ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+ .setSpeakerphoneOn(speakerMode);
+ setAudioGroupMode();
+ }
+ }
+
+ private boolean isSpeakerOn() {
+ return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+ .isSpeakerphoneOn();
+ }
+
+ /**
+ * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
+ * event 0--9 maps to decimal
+ * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
+ * flash to 16. Currently, event flash is not supported.
+ *
+ * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
+ * inputs.
+ */
+ public void sendDtmf(int code) {
+ sendDtmf(code, null);
+ }
+
+ /**
+ * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
+ * event 0--9 maps to decimal
+ * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
+ * flash to 16. Currently, event flash is not supported.
+ *
+ * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
+ * inputs.
+ * @param result the result message to send when done
+ */
+ public void sendDtmf(int code, Message result) {
+ synchronized (this) {
+ AudioGroup audioGroup = getAudioGroup();
+ if ((audioGroup != null) && (mSipSession != null)
+ && (SipSession.State.IN_CALL == getState())) {
+ if (DBG) log("sendDtmf: code=" + code + " result=" + result);
+ audioGroup.sendDtmf(code);
+ }
+ if (result != null) result.sendToTarget();
+ }
+ }
+
+ /**
+ * 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
+ * @hide
+ */
+ public AudioStream getAudioStream() {
+ synchronized (this) {
+ return mAudioStream;
+ }
+ }
+
+ /**
+ * 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. If a group object is set by
+ * {@link #setAudioGroup(AudioGroup)}, then this method returns that object.
+ *
+ * @return the {@link AudioGroup} object or null if the RTP stream has not
+ * yet been set up
+ * @see #getAudioStream
+ * @hide
+ */
+ public AudioGroup getAudioGroup() {
+ synchronized (this) {
+ if (mAudioGroup != null) return mAudioGroup;
+ return ((mAudioStream == null) ? null : mAudioStream.getGroup());
+ }
+ }
+
+ /**
+ * Sets the {@link AudioGroup} object which the {@link AudioStream} object
+ * joins. If {@code audioGroup} is null, then the {@code AudioGroup} object
+ * will be dynamically created when needed. Note that the mode of the
+ * {@code AudioGroup} is not changed according to the audio settings (i.e.,
+ * hold, mute, speaker phone) of this object. This is mainly used to merge
+ * multiple {@code SipAudioCall} objects to form a conference call. The
+ * settings of the first object (that merges others) override others'.
+ *
+ * @see #getAudioStream
+ * @hide
+ */
+ public void setAudioGroup(AudioGroup group) {
+ synchronized (this) {
+ if (DBG) log("setAudioGroup: group=" + group);
+ if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) {
+ mAudioStream.join(group);
+ }
+ mAudioGroup = group;
+ }
+ }
+
+ /**
+ * Starts the audio for the established call. This method should be called
+ * after {@link Listener#onCallEstablished} is called.
+ * <p class="note"><strong>Note:</strong> Requires the
+ * {@link android.Manifest.permission#RECORD_AUDIO},
+ * {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
+ * {@link android.Manifest.permission#WAKE_LOCK} permissions.</p>
+ */
+ public void startAudio() {
+ try {
+ startAudioInternal();
+ } catch (UnknownHostException e) {
+ onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage());
+ } catch (Throwable e) {
+ onError(SipErrorCode.CLIENT_ERROR, e.getMessage());
+ }
+ }
+
+ private synchronized void startAudioInternal() throws UnknownHostException {
+ if (DBG) loge("startAudioInternal: mPeerSd=" + mPeerSd);
+ if (mPeerSd == null) {
+ throw new IllegalStateException("mPeerSd = null");
+ }
+
+ stopCall(DONT_RELEASE_SOCKET);
+ mInCall = true;
+
+ // Run exact the same logic in createAnswer() to setup mAudioStream.
+ SimpleSessionDescription offer =
+ new SimpleSessionDescription(mPeerSd);
+ AudioStream stream = mAudioStream;
+ AudioCodec codec = null;
+ for (Media media : offer.getMedia()) {
+ if ((codec == null) && (media.getPort() > 0)
+ && "audio".equals(media.getType())
+ && "RTP/AVP".equals(media.getProtocol())) {
+ // Find the first audio codec we supported.
+ for (int type : media.getRtpPayloadTypes()) {
+ codec = AudioCodec.getCodec(
+ type, media.getRtpmap(type), media.getFmtp(type));
+ if (codec != null) {
+ break;
+ }
+ }
+
+ if (codec != null) {
+ // Associate with the remote host.
+ String address = media.getAddress();
+ if (address == null) {
+ address = offer.getAddress();
+ }
+ stream.associate(InetAddress.getByName(address),
+ media.getPort());
+
+ stream.setDtmfType(-1);
+ stream.setCodec(codec);
+ // Check if DTMF is supported in the same media.
+ for (int type : media.getRtpPayloadTypes()) {
+ String rtpmap = media.getRtpmap(type);
+ if ((type != codec.type) && (rtpmap != null)
+ && rtpmap.startsWith("telephone-event")) {
+ stream.setDtmfType(type);
+ }
+ }
+
+ // Handle recvonly and sendonly.
+ if (mHold) {
+ stream.setMode(RtpStream.MODE_NORMAL);
+ } else if (media.getAttribute("recvonly") != null) {
+ stream.setMode(RtpStream.MODE_SEND_ONLY);
+ } else if(media.getAttribute("sendonly") != null) {
+ stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+ } else if(offer.getAttribute("recvonly") != null) {
+ stream.setMode(RtpStream.MODE_SEND_ONLY);
+ } else if(offer.getAttribute("sendonly") != null) {
+ stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+ } else {
+ stream.setMode(RtpStream.MODE_NORMAL);
+ }
+ break;
+ }
+ }
+ }
+ if (codec == null) {
+ throw new IllegalStateException("Reject SDP: no suitable codecs");
+ }
+
+ if (isWifiOn()) grabWifiHighPerfLock();
+
+ // AudioGroup logic:
+ AudioGroup audioGroup = getAudioGroup();
+ if (mHold) {
+ // don't create an AudioGroup here; doing so will fail if
+ // there's another AudioGroup out there that's active
+ } else {
+ if (audioGroup == null) audioGroup = new AudioGroup();
+ stream.join(audioGroup);
+ }
+ setAudioGroupMode();
+ }
+
+ // set audio group mode based on current audio configuration
+ private void setAudioGroupMode() {
+ AudioGroup audioGroup = getAudioGroup();
+ if (DBG) log("setAudioGroupMode: audioGroup=" + audioGroup);
+ if (audioGroup != null) {
+ if (mHold) {
+ audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+ } else if (mMuted) {
+ audioGroup.setMode(AudioGroup.MODE_MUTED);
+ } else if (isSpeakerOn()) {
+ audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
+ } else {
+ audioGroup.setMode(AudioGroup.MODE_NORMAL);
+ }
+ }
+ }
+
+ private void stopCall(boolean releaseSocket) {
+ if (DBG) log("stopCall: releaseSocket=" + releaseSocket);
+ releaseWifiHighPerfLock();
+ if (mAudioStream != null) {
+ mAudioStream.join(null);
+
+ if (releaseSocket) {
+ mAudioStream.release();
+ mAudioStream = null;
+ }
+ }
+ }
+
+ private String getLocalIp() {
+ return mSipSession.getLocalIp();
+ }
+
+ private void throwSipException(Throwable throwable) throws SipException {
+ if (throwable instanceof SipException) {
+ throw (SipException) throwable;
+ } else {
+ throw new SipException("", throwable);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(LOG_TAG, s);
+ }
+
+ private void loge(String s) {
+ Rlog.e(LOG_TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(LOG_TAG, s, t);
+ }
+}
diff --git a/java/android/net/sip/SipErrorCode.java b/java/android/net/sip/SipErrorCode.java
new file mode 100644
index 0000000..509728f
--- /dev/null
+++ b/java/android/net/sip/SipErrorCode.java
@@ -0,0 +1,101 @@
+/*
+ * 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 error codes returned during SIP actions. For example, during
+ * {@link SipRegistrationListener#onRegistrationFailed onRegistrationFailed()},
+ * {@link SipSession.Listener#onError onError()},
+ * {@link SipSession.Listener#onCallChangeFailed onCallChangeFailed()} and
+ * {@link SipSession.Listener#onRegistrationFailed onRegistrationFailed()}.
+ */
+public class SipErrorCode {
+ /** Not an error. */
+ public static final int NO_ERROR = 0;
+
+ /** When some socket error occurs. */
+ public static final int SOCKET_ERROR = -1;
+
+ /** When server responds with an error. */
+ public static final int SERVER_ERROR = -2;
+
+ /** When transaction is terminated unexpectedly. */
+ public static final int TRANSACTION_TERMINTED = -3;
+
+ /** When some error occurs on the device, possibly due to a bug. */
+ public static final int CLIENT_ERROR = -4;
+
+ /** When the transaction gets timed out. */
+ public static final int TIME_OUT = -5;
+
+ /** When the remote URI is not valid. */
+ public static final int INVALID_REMOTE_URI = -6;
+
+ /** When the peer is not reachable. */
+ public static final int PEER_NOT_REACHABLE = -7;
+
+ /** When invalid credentials are provided. */
+ public static final int INVALID_CREDENTIALS = -8;
+
+ /** The client is in a transaction and cannot initiate a new one. */
+ public static final int IN_PROGRESS = -9;
+
+ /** When data connection is lost. */
+ public static final int DATA_CONNECTION_LOST = -10;
+
+ /** Cross-domain authentication required. */
+ public static final int CROSS_DOMAIN_AUTHENTICATION = -11;
+
+ /** When the server is not reachable. */
+ public static final int SERVER_UNREACHABLE = -12;
+
+ public static String toString(int errorCode) {
+ switch (errorCode) {
+ case NO_ERROR:
+ return "NO_ERROR";
+ case SOCKET_ERROR:
+ return "SOCKET_ERROR";
+ case SERVER_ERROR:
+ return "SERVER_ERROR";
+ case TRANSACTION_TERMINTED:
+ return "TRANSACTION_TERMINTED";
+ case CLIENT_ERROR:
+ return "CLIENT_ERROR";
+ case TIME_OUT:
+ return "TIME_OUT";
+ case INVALID_REMOTE_URI:
+ return "INVALID_REMOTE_URI";
+ case PEER_NOT_REACHABLE:
+ return "PEER_NOT_REACHABLE";
+ case INVALID_CREDENTIALS:
+ return "INVALID_CREDENTIALS";
+ case IN_PROGRESS:
+ return "IN_PROGRESS";
+ case DATA_CONNECTION_LOST:
+ return "DATA_CONNECTION_LOST";
+ case CROSS_DOMAIN_AUTHENTICATION:
+ return "CROSS_DOMAIN_AUTHENTICATION";
+ case SERVER_UNREACHABLE:
+ return "SERVER_UNREACHABLE";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private SipErrorCode() {
+ }
+}
diff --git a/java/android/net/sip/SipException.java b/java/android/net/sip/SipException.java
new file mode 100644
index 0000000..0339395
--- /dev/null
+++ b/java/android/net/sip/SipException.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Indicates a general SIP-related exception.
+ */
+public class SipException extends Exception {
+ public SipException() {
+ }
+
+ public SipException(String message) {
+ super(message);
+ }
+
+ public SipException(String message, Throwable cause) {
+ // we want to eliminate the dependency on javax.sip.SipException
+ super(message, ((cause instanceof javax.sip.SipException)
+ && (cause.getCause() != null))
+ ? cause.getCause()
+ : cause);
+ }
+}
diff --git a/java/android/net/sip/SipManager.java b/java/android/net/sip/SipManager.java
new file mode 100644
index 0000000..a94232a
--- /dev/null
+++ b/java/android/net/sip/SipManager.java
@@ -0,0 +1,622 @@
+/*
+ * 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.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.Rlog;
+
+import java.text.ParseException;
+
+/**
+ * Provides APIs for SIP tasks, such as initiating SIP connections, and provides access to related
+ * SIP services. This class is the starting point for any SIP actions. You can acquire an instance
+ * of it with {@link #newInstance newInstance()}.</p>
+ * <p>The APIs in this class allows you to:</p>
+ * <ul>
+ * <li>Create a {@link SipSession} to get ready for making calls or listen for incoming calls. See
+ * {@link #createSipSession createSipSession()} and {@link #getSessionFor getSessionFor()}.</li>
+ * <li>Initiate and receive generic SIP calls or audio-only SIP calls. Generic SIP calls may
+ * be video, audio, or other, and are initiated with {@link #open open()}. Audio-only SIP calls
+ * should be handled with a {@link SipAudioCall}, which you can acquire with {@link
+ * #makeAudioCall makeAudioCall()} and {@link #takeAudioCall takeAudioCall()}.</li>
+ * <li>Register and unregister with a SIP service provider, with
+ * {@link #register register()} and {@link #unregister unregister()}.</li>
+ * <li>Verify session connectivity, with {@link #isOpened isOpened()} and
+ * {@link #isRegistered isRegistered()}.</li>
+ * </ul>
+ * <p class="note"><strong>Note:</strong> Not all Android-powered devices support VOIP calls using
+ * SIP. You should always call {@link android.net.sip.SipManager#isVoipSupported
+ * isVoipSupported()} to verify that the device supports VOIP calling and {@link
+ * android.net.sip.SipManager#isApiSupported isApiSupported()} to verify that the device supports
+ * the SIP APIs. Your application must also request the {@link
+ * android.Manifest.permission#INTERNET} and {@link android.Manifest.permission#USE_SIP}
+ * permissions.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipManager {
+ /**
+ * The result code to be sent back with the incoming call
+ * {@link PendingIntent}.
+ * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+ */
+ public static final int INCOMING_CALL_RESULT_CODE = 101;
+
+ /**
+ * Key to retrieve the call ID from an incoming call intent.
+ * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+ */
+ public static final String EXTRA_CALL_ID = "android:sipCallID";
+
+ /**
+ * Key to retrieve the offered session description from an incoming call
+ * intent.
+ * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+ */
+ public static final String EXTRA_OFFER_SD = "android:sipOfferSD";
+
+ /**
+ * Action to broadcast when SipService is up.
+ * Internal use only.
+ * @hide
+ */
+ public static final String ACTION_SIP_SERVICE_UP =
+ "android.net.sip.SIP_SERVICE_UP";
+ /**
+ * Action string for the incoming call intent for the Phone app.
+ * Internal use only.
+ * @hide
+ */
+ public static final String ACTION_SIP_INCOMING_CALL =
+ "com.android.phone.SIP_INCOMING_CALL";
+ /**
+ * Action string for the add-phone intent.
+ * Internal use only.
+ * @hide
+ */
+ public static final String ACTION_SIP_ADD_PHONE =
+ "com.android.phone.SIP_ADD_PHONE";
+ /**
+ * Action string for the remove-phone intent.
+ * Internal use only.
+ * @hide
+ */
+ public static final String ACTION_SIP_REMOVE_PHONE =
+ "com.android.phone.SIP_REMOVE_PHONE";
+ /**
+ * Part of the ACTION_SIP_ADD_PHONE and ACTION_SIP_REMOVE_PHONE intents.
+ * Internal use only.
+ * @hide
+ */
+ public static final String EXTRA_LOCAL_URI = "android:localSipUri";
+
+ private static final String TAG = "SipManager";
+
+ private ISipService mSipService;
+ private Context mContext;
+
+ /**
+ * Creates a manager instance. Returns null if SIP API is not supported.
+ *
+ * @param context application context for creating the manager object
+ * @return the manager instance or null if SIP API is not supported
+ */
+ public static SipManager newInstance(Context context) {
+ return (isApiSupported(context) ? new SipManager(context) : null);
+ }
+
+ /**
+ * Returns true if the SIP API is supported by the system.
+ */
+ public static boolean isApiSupported(Context context) {
+ return context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_SIP);
+ }
+
+ /**
+ * Returns true if the system supports SIP-based VOIP API.
+ */
+ public static boolean isVoipSupported(Context context) {
+ return context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_SIP_VOIP) && isApiSupported(context);
+ }
+
+ /**
+ * Returns true if SIP is only available on WIFI.
+ */
+ public static boolean isSipWifiOnly(Context context) {
+ return context.getResources().getBoolean(
+ com.android.internal.R.bool.config_sip_wifi_only);
+ }
+
+ private SipManager(Context context) {
+ mContext = context;
+ createSipService();
+ }
+
+ private void createSipService() {
+ IBinder b = ServiceManager.getService(Context.SIP_SERVICE);
+ mSipService = ISipService.Stub.asInterface(b);
+ }
+
+ /**
+ * Opens the profile for making generic SIP calls. The caller may make subsequent calls
+ * through {@link #makeAudioCall}. If one also wants to receive calls on the
+ * profile, use
+ * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)}
+ * instead.
+ *
+ * @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 generic SIP calls. The caller may
+ * make subsequent calls through {@link #makeAudioCall}. If the
+ * auto-registration option is enabled in the profile, the SIP service
+ * will register the profile to the corresponding SIP provider periodically
+ * in order to receive calls from the provider. When the SIP service
+ * receives a new call, it will send out an intent with the provided action
+ * string. The intent contains a call ID extra and an offer session
+ * description string extra. Use {@link #getCallId} and
+ * {@link #getOfferSessionDescription} to retrieve those extras.
+ *
+ * @param localProfile the SIP profile to receive incoming calls for
+ * @param incomingCallPendingIntent When an incoming call is received, the
+ * SIP service will call
+ * {@link PendingIntent#send(Context, int, Intent)} to send back the
+ * intent to the caller with {@link #INCOMING_CALL_RESULT_CODE} as the
+ * result code and the intent to fill in the call ID and session
+ * description information. It cannot be null.
+ * @param listener to listen to registration events; can be null
+ * @see #getCallId
+ * @see #getOfferSessionDescription
+ * @see #takeAudioCall
+ * @throws NullPointerException if {@code incomingCallPendingIntent} is null
+ * @throws SipException if the profile contains incorrect settings or
+ * calling the SIP service results in an error
+ * @see #isIncomingCallIntent
+ * @see #getCallId
+ * @see #getOfferSessionDescription
+ */
+ public void open(SipProfile localProfile,
+ PendingIntent incomingCallPendingIntent,
+ SipRegistrationListener listener) throws SipException {
+ if (incomingCallPendingIntent == null) {
+ throw new NullPointerException(
+ "incomingCallPendingIntent cannot be null");
+ }
+ try {
+ mSipService.open3(localProfile, incomingCallPendingIntent,
+ createRelay(listener, localProfile.getUriString()));
+ } 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, PendingIntent, SipRegistrationListener)}).
+ *
+ * @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, localProfileUri));
+ } 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 opened in the SIP service for
+ * making and/or receiving 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 SIP service has successfully registered the profile to the
+ * SIP provider (specified in the profile) for receiving calls. Returning
+ * true from this method also implies the profile is opened
+ * ({@link #isOpened}).
+ *
+ * @param localProfileUri the URI of the profile in question
+ * @return true if the profile is registered to the SIP provider; false if
+ * the profile has not been opened in the SIP service or the SIP
+ * service has not yet successfully registered the profile to the SIP
+ * provider
+ * @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. The attempt will be timed
+ * out if the call is not established within {@code timeout} seconds and
+ * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @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
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @return a {@link SipAudioCall} object
+ * @throws SipException if calling the SIP service results in an error or
+ * VOIP API is not supported by the device
+ * @see SipAudioCall.Listener#onError
+ * @see #isVoipSupported
+ */
+ public SipAudioCall makeAudioCall(SipProfile localProfile,
+ SipProfile peerProfile, SipAudioCall.Listener listener, int timeout)
+ throws SipException {
+ if (!isVoipSupported(mContext)) {
+ throw new SipException("VOIP API is not supported");
+ }
+ SipAudioCall call = new SipAudioCall(mContext, localProfile);
+ call.setListener(listener);
+ SipSession s = createSipSession(localProfile, null);
+ call.makeCall(peerProfile, s, timeout);
+ return call;
+ }
+
+ /**
+ * Creates a {@link SipAudioCall} to make an audio call. The attempt will be
+ * timed out if the call is not established within {@code timeout} seconds
+ * and
+ * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+ * will be called.
+ *
+ * @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
+ * @param timeout the timeout value in seconds. Default value (defined by
+ * SIP protocol) is used if {@code timeout} is zero or negative.
+ * @return a {@link SipAudioCall} object
+ * @throws SipException if calling the SIP service results in an error or
+ * VOIP API is not supported by the device
+ * @see SipAudioCall.Listener#onError
+ * @see #isVoipSupported
+ */
+ public SipAudioCall makeAudioCall(String localProfileUri,
+ String peerProfileUri, SipAudioCall.Listener listener, int timeout)
+ throws SipException {
+ if (!isVoipSupported(mContext)) {
+ throw new SipException("VOIP API is not supported");
+ }
+ try {
+ return makeAudioCall(
+ new SipProfile.Builder(localProfileUri).build(),
+ new SipProfile.Builder(peerProfileUri).build(), listener,
+ timeout);
+ } catch (ParseException e) {
+ throw new SipException("build SipProfile", e);
+ }
+ }
+
+ /**
+ * Creates a {@link SipAudioCall} to take an incoming call. Before the call
+ * is returned, the listener will receive a
+ * {@link SipAudioCall.Listener#onRinging}
+ * callback.
+ *
+ * @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(Intent incomingCallIntent,
+ SipAudioCall.Listener listener) throws SipException {
+ if (incomingCallIntent == null) {
+ throw new SipException("Cannot retrieve session with null intent");
+ }
+
+ String callId = getCallId(incomingCallIntent);
+ if (callId == null) {
+ throw new SipException("Call ID missing in incoming call intent");
+ }
+
+ String offerSd = getOfferSessionDescription(incomingCallIntent);
+ if (offerSd == null) {
+ throw new SipException("Session description missing in incoming "
+ + "call intent");
+ }
+
+ try {
+ ISipSession session = mSipService.getPendingSession(callId);
+ if (session == null) {
+ throw new SipException("No pending session for the call");
+ }
+ SipAudioCall call = new SipAudioCall(
+ mContext, session.getLocalProfile());
+ call.attachCall(new SipSession(session), offerSd);
+ 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);
+ String 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(EXTRA_CALL_ID);
+ }
+
+ /**
+ * 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 String getOfferSessionDescription(Intent incomingCallIntent) {
+ return incomingCallIntent.getStringExtra(EXTRA_OFFER_SD);
+ }
+
+ /**
+ * Creates an incoming call broadcast intent.
+ *
+ * @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 callId,
+ String sessionDescription) {
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_CALL_ID, callId);
+ intent.putExtra(EXTRA_OFFER_SD, sessionDescription);
+ return intent;
+ }
+
+ /**
+ * Manually registers the profile to the corresponding SIP provider for
+ * receiving calls.
+ * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)} is
+ * still needed to be called at least once in order for the SIP service to
+ * notify the caller with the {@link android.app.PendingIntent} when an incoming call is
+ * received.
+ *
+ * @param localProfile the SIP profile to register with
+ * @param expiryTime registration expiration time (in seconds)
+ * @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, localProfile.getUriString()));
+ if (session == null) {
+ throw new SipException(
+ "SipService.createSession() returns null");
+ }
+ session.register(expiryTime);
+ } catch (RemoteException e) {
+ throw new SipException("register()", e);
+ }
+ }
+
+ /**
+ * Manually unregisters the profile from the corresponding SIP provider for
+ * stop receiving further calls. This may interference with the auto
+ * registration process in the SIP service if the auto-registration option
+ * in the profile is enabled.
+ *
+ * @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, localProfile.getUriString()));
+ if (session == null) {
+ throw new SipException(
+ "SipService.createSession() returns null");
+ }
+ session.unregister();
+ } catch (RemoteException e) {
+ throw new SipException("unregister()", e);
+ }
+ }
+
+ /**
+ * Gets the {@link SipSession} that handles the incoming call. For audio
+ * calls, consider to use {@link SipAudioCall} to handle the incoming call.
+ * See {@link #takeAudioCall}. 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 SipSession getSessionFor(Intent incomingCallIntent)
+ throws SipException {
+ try {
+ String callId = getCallId(incomingCallIntent);
+ ISipSession s = mSipService.getPendingSession(callId);
+ return ((s == null) ? null : new SipSession(s));
+ } catch (RemoteException e) {
+ throw new SipException("getSessionFor()", e);
+ }
+ }
+
+ private static ISipSessionListener createRelay(
+ SipRegistrationListener listener, String uri) {
+ return ((listener == null) ? null : new ListenerRelay(listener, uri));
+ }
+
+ /**
+ * Creates a {@link SipSession} with the specified profile. Use other
+ * methods, if applicable, instead of interacting with {@link SipSession}
+ * directly.
+ *
+ * @param localProfile the SIP profile the session is associated with
+ * @param listener to listen to SIP session events
+ */
+ public SipSession createSipSession(SipProfile localProfile,
+ SipSession.Listener listener) throws SipException {
+ try {
+ ISipSession s = mSipService.createSession(localProfile, null);
+ if (s == null) {
+ throw new SipException(
+ "Failed to create SipSession; network unavailable?");
+ }
+ return new SipSession(s, 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 new SipProfile[0];
+ }
+ }
+
+ private static class ListenerRelay extends SipSessionAdapter {
+ private SipRegistrationListener mListener;
+ private String mUri;
+
+ // listener must not be null
+ public ListenerRelay(SipRegistrationListener listener, String uri) {
+ mListener = listener;
+ mUri = uri;
+ }
+
+ private String getUri(ISipSession session) {
+ try {
+ return ((session == null)
+ ? mUri
+ : session.getLocalProfile().getUriString());
+ } catch (Throwable e) {
+ // SipService died? SIP stack died?
+ Rlog.e(TAG, "getUri(): ", e);
+ return null;
+ }
+ }
+
+ @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, int errorCode,
+ String message) {
+ mListener.onRegistrationFailed(getUri(session), errorCode, message);
+ }
+
+ @Override
+ public void onRegistrationTimeout(ISipSession session) {
+ mListener.onRegistrationFailed(getUri(session),
+ SipErrorCode.TIME_OUT, "registration timed out");
+ }
+ }
+}
diff --git a/java/android/net/sip/SipProfile.aidl b/java/android/net/sip/SipProfile.aidl
new file mode 100644
index 0000000..3b6f68f
--- /dev/null
+++ b/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/java/android/net/sip/SipProfile.java b/java/android/net/sip/SipProfile.java
new file mode 100644
index 0000000..0ef754c
--- /dev/null
+++ b/java/android/net/sip/SipProfile.java
@@ -0,0 +1,502 @@
+/*
+ * 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.ObjectStreamException;
+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;
+
+/**
+ * Defines a SIP profile, including a SIP account, domain and server information.
+ * <p>You can create a {@link SipProfile} using {@link
+ * SipProfile.Builder}. You can also retrieve one from a {@link SipSession}, using {@link
+ * SipSession#getLocalProfile} and {@link SipSession#getPeerProfile}.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipProfile implements Parcelable, Serializable, Cloneable {
+ private static final long serialVersionUID = 1L;
+ private static final int DEFAULT_PORT = 5060;
+ private static final String TCP = "TCP";
+ private static final String UDP = "UDP";
+ private Address mAddress;
+ private String mProxyAddress;
+ private String mPassword;
+ private String mDomain;
+ private String mProtocol = UDP;
+ private String mProfileName;
+ private String mAuthUserName;
+ private int mPort = DEFAULT_PORT;
+ private boolean mSendKeepAlive = false;
+ private boolean mAutoRegistration = true;
+ private transient int mCallingUid = 0;
+
+ 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];
+ }
+ };
+
+ /**
+ * Helper class for creating 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);
+ }
+ }
+
+ /**
+ * Creates a builder based on the given profile.
+ */
+ public Builder(SipProfile profile) {
+ if (profile == null) throw new NullPointerException();
+ try {
+ mProfile = (SipProfile) profile.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("should not occur", e);
+ }
+ mProfile.mAddress = null;
+ mUri = profile.getUri();
+ mUri.setUserPassword(profile.getPassword());
+ mDisplayName = profile.getDisplayName();
+ mProxyAddress = profile.getProxyAddress();
+ mProfile.mPort = profile.getPort();
+ }
+
+ /**
+ * 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} 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 username used for authentication.
+ *
+ * @param name authentication username of the profile
+ * @return this builder object
+ */
+ public Builder setAuthUserName(String name) {
+ mProfile.mAuthUserName = name;
+ return this;
+ }
+
+ /**
+ * 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 IllegalArgumentException if the port number is out of range
+ */
+ public Builder setPort(int port) throws IllegalArgumentException {
+ if ((port > 65535) || (port < 1000)) {
+ throw new IllegalArgumentException("incorrect port arugment: " + port);
+ }
+ mProfile.mPort = 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 IllegalArgumentException if the protocol is not recognized
+ */
+ public Builder setProtocol(String protocol)
+ throws IllegalArgumentException {
+ if (protocol == null) {
+ throw new NullPointerException("protocol cannot be null");
+ }
+ protocol = protocol.toUpperCase();
+ if (!protocol.equals(UDP) && !protocol.equals(TCP)) {
+ throw new IllegalArgumentException(
+ "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 {
+ if (!TextUtils.isEmpty(mProxyAddress)) {
+ SipURI uri = (SipURI)
+ mAddressFactory.createURI(fix(mProxyAddress));
+ mProfile.mProxyAddress = uri.getHost();
+ } else {
+ if (!mProfile.mProtocol.equals(UDP)) {
+ mUri.setTransportParam(mProfile.mProtocol);
+ }
+ if (mProfile.mPort != DEFAULT_PORT) {
+ mUri.setPort(mProfile.mPort);
+ }
+ }
+ mProfile.mAddress = mAddressFactory.createAddress(
+ mDisplayName, mUri);
+ } catch (InvalidArgumentException e) {
+ throw new RuntimeException(e);
+ } 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;
+ mCallingUid = in.readInt();
+ mPort = in.readInt();
+ mAuthUserName = in.readString();
+ }
+
+ @Override
+ 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);
+ out.writeInt(mCallingUid);
+ out.writeInt(mPort);
+ out.writeString(mAuthUserName);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Gets the SIP URI of this profile.
+ *
+ * @return the SIP URI of this profile
+ * @hide
+ */
+ 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() {
+ // We need to return the sip uri domain instead of
+ // the SIP URI with transport, port information if
+ // the outbound proxy address exists.
+ if (!TextUtils.isEmpty(mProxyAddress)) {
+ return "sip:" + getUserName() + "@" + mDomain;
+ }
+ return getUri().toString();
+ }
+
+ /**
+ * Gets the SIP address of this profile.
+ *
+ * @return the SIP address of this profile
+ * @hide
+ */
+ 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 username for authentication. If it is null, then the username
+ * is used in authentication instead.
+ *
+ * @return the authentication username
+ * @see #getUserName
+ */
+ public String getAuthUserName() {
+ return mAuthUserName;
+ }
+
+ /**
+ * 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() {
+ return mPort;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Sets the calling process's Uid in the sip service.
+ * @hide
+ */
+ public void setCallingUid(int uid) {
+ mCallingUid = uid;
+ }
+
+ /**
+ * Gets the calling process's Uid in the sip settings.
+ * @hide
+ */
+ public int getCallingUid() {
+ return mCallingUid;
+ }
+
+ private Object readResolve() throws ObjectStreamException {
+ // For compatibility.
+ if (mPort == 0) mPort = DEFAULT_PORT;
+ return this;
+ }
+}
diff --git a/java/android/net/sip/SipRegistrationListener.java b/java/android/net/sip/SipRegistrationListener.java
new file mode 100644
index 0000000..9968cc7
--- /dev/null
+++ b/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 for SIP registration events.
+ */
+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 the registration succeeded.
+ *
+ * @param localProfileUri the URI string of the SIP profile to register with
+ * @param expiryTime duration in seconds before the registration expires
+ */
+ void onRegistrationDone(String localProfileUri, long expiryTime);
+
+ /**
+ * Called when the registration failed.
+ *
+ * @param localProfileUri the URI string of the SIP profile to register with
+ * @param errorCode error code of this error
+ * @param errorMessage error message
+ * @see SipErrorCode
+ */
+ void onRegistrationFailed(String localProfileUri, int errorCode,
+ String errorMessage);
+}
diff --git a/java/android/net/sip/SipSession.java b/java/android/net/sip/SipSession.java
new file mode 100644
index 0000000..edbc66f
--- /dev/null
+++ b/java/android/net/sip/SipSession.java
@@ -0,0 +1,574 @@
+/*
+ * 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.RemoteException;
+import android.telephony.Rlog;
+
+/**
+ * Represents a SIP session that is associated with a SIP dialog or a standalone
+ * transaction not within a dialog.
+ * <p>You can get a {@link SipSession} from {@link SipManager} with {@link
+ * SipManager#createSipSession createSipSession()} (when initiating calls) or {@link
+ * SipManager#getSessionFor getSessionFor()} (when receiving calls).</p>
+ */
+public final class SipSession {
+ private static final String TAG = "SipSession";
+
+ /**
+ * Defines SIP session states, such as "registering", "outgoing call", and "in call".
+ */
+ public static class State {
+ /** When session is ready to initiate a call or transaction. */
+ public static final int READY_TO_CALL = 0;
+
+ /** When the registration request is sent out. */
+ public static final int REGISTERING = 1;
+
+ /** When the unregistration request is sent out. */
+ public static final int DEREGISTERING = 2;
+
+ /** When an INVITE request is received. */
+ public static final int INCOMING_CALL = 3;
+
+ /** When an OK response is sent for the INVITE request received. */
+ public static final int INCOMING_CALL_ANSWERING = 4;
+
+ /** When an INVITE request is sent. */
+ public static final int OUTGOING_CALL = 5;
+
+ /** When a RINGING response is received for the INVITE request sent. */
+ public static final int OUTGOING_CALL_RING_BACK = 6;
+
+ /** When a CANCEL request is sent for the INVITE request sent. */
+ public static final int OUTGOING_CALL_CANCELING = 7;
+
+ /** When a call is established. */
+ public static final int IN_CALL = 8;
+
+ /** When an OPTIONS request is sent. */
+ public static final int PINGING = 9;
+
+ /** When ending a call. @hide */
+ public static final int ENDING_CALL = 10;
+
+ /** Not defined. */
+ public static final int NOT_DEFINED = 101;
+
+ /**
+ * Converts the state to string.
+ */
+ public static String toString(int state) {
+ switch (state) {
+ case READY_TO_CALL:
+ return "READY_TO_CALL";
+ case REGISTERING:
+ return "REGISTERING";
+ case DEREGISTERING:
+ return "DEREGISTERING";
+ case INCOMING_CALL:
+ return "INCOMING_CALL";
+ case INCOMING_CALL_ANSWERING:
+ return "INCOMING_CALL_ANSWERING";
+ case OUTGOING_CALL:
+ return "OUTGOING_CALL";
+ case OUTGOING_CALL_RING_BACK:
+ return "OUTGOING_CALL_RING_BACK";
+ case OUTGOING_CALL_CANCELING:
+ return "OUTGOING_CALL_CANCELING";
+ case IN_CALL:
+ return "IN_CALL";
+ case PINGING:
+ return "PINGING";
+ default:
+ return "NOT_DEFINED";
+ }
+ }
+
+ private State() {
+ }
+ }
+
+ /**
+ * Listener for events relating to a SIP session, such as when a session is being registered
+ * ("on registering") or a call is outgoing ("on calling").
+ * <p>Many of these events are also received by {@link SipAudioCall.Listener}.</p>
+ */
+ public static class Listener {
+ /**
+ * Called when an INVITE request is sent to initiate a new call.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ public void onCalling(SipSession 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
+ */
+ public void onRinging(SipSession session, SipProfile caller,
+ String sessionDescription) {
+ }
+
+ /**
+ * Called when a RINGING response is received for the INVITE request sent
+ *
+ * @param session the session object that carries out the transaction
+ */
+ public void onRingingBack(SipSession 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
+ */
+ public void onCallEstablished(SipSession session,
+ String sessionDescription) {
+ }
+
+ /**
+ * Called when the session is terminated.
+ *
+ * @param session the session object that is associated with the dialog
+ */
+ public void onCallEnded(SipSession session) {
+ }
+
+ /**
+ * Called when the peer is busy during session initialization.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ public void onCallBusy(SipSession session) {
+ }
+
+ /**
+ * Called when the call is being transferred to a new one.
+ *
+ * @hide
+ * @param newSession the new session that the call will be transferred to
+ * @param sessionDescription the new peer's session description
+ */
+ public void onCallTransferring(SipSession newSession,
+ String sessionDescription) {
+ }
+
+ /**
+ * Called when an error occurs during session initialization and
+ * termination.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ public void onError(SipSession session, int errorCode,
+ String errorMessage) {
+ }
+
+ /**
+ * Called when an error occurs during session modification negotiation.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ public void onCallChangeFailed(SipSession session, int errorCode,
+ String errorMessage) {
+ }
+
+ /**
+ * Called when a registration request is sent.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ public void onRegistering(SipSession 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
+ */
+ public void onRegistrationDone(SipSession session, int duration) {
+ }
+
+ /**
+ * Called when the registration fails.
+ *
+ * @param session the session object that carries out the transaction
+ * @param errorCode error code defined in {@link SipErrorCode}
+ * @param errorMessage error message
+ */
+ public void onRegistrationFailed(SipSession session, int errorCode,
+ String errorMessage) {
+ }
+
+ /**
+ * Called when the registration gets timed out.
+ *
+ * @param session the session object that carries out the transaction
+ */
+ public void onRegistrationTimeout(SipSession session) {
+ }
+ }
+
+ private final ISipSession mSession;
+ private Listener mListener;
+
+ SipSession(ISipSession realSession) {
+ mSession = realSession;
+ if (realSession != null) {
+ try {
+ realSession.setListener(createListener());
+ } catch (RemoteException e) {
+ loge("SipSession.setListener:", e);
+ }
+ }
+ }
+
+ SipSession(ISipSession realSession, Listener listener) {
+ this(realSession);
+ setListener(listener);
+ }
+
+ /**
+ * Gets the IP address of the local host on which this SIP session runs.
+ *
+ * @return the IP address of the local host
+ */
+ public String getLocalIp() {
+ try {
+ return mSession.getLocalIp();
+ } catch (RemoteException e) {
+ loge("getLocalIp:", e);
+ return "127.0.0.1";
+ }
+ }
+
+ /**
+ * Gets the SIP profile that this session is associated with.
+ *
+ * @return the SIP profile that this session is associated with
+ */
+ public SipProfile getLocalProfile() {
+ try {
+ return mSession.getLocalProfile();
+ } catch (RemoteException e) {
+ loge("getLocalProfile:", e);
+ return null;
+ }
+ }
+
+ /**
+ * 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
+ */
+ public SipProfile getPeerProfile() {
+ try {
+ return mSession.getPeerProfile();
+ } catch (RemoteException e) {
+ loge("getPeerProfile:", e);
+ return null;
+ }
+ }
+
+ /**
+ * Gets the session state. The value returned must be one of the states in
+ * {@link State}.
+ *
+ * @return the session state
+ */
+ public int getState() {
+ try {
+ return mSession.getState();
+ } catch (RemoteException e) {
+ loge("getState:", e);
+ return State.NOT_DEFINED;
+ }
+ }
+
+ /**
+ * Checks if the session is in a call.
+ *
+ * @return true if the session is in a call
+ */
+ public boolean isInCall() {
+ try {
+ return mSession.isInCall();
+ } catch (RemoteException e) {
+ loge("isInCall:", e);
+ return false;
+ }
+ }
+
+ /**
+ * Gets the call ID of the session.
+ *
+ * @return the call ID
+ */
+ public String getCallId() {
+ try {
+ return mSession.getCallId();
+ } catch (RemoteException e) {
+ loge("getCallId:", e);
+ return null;
+ }
+ }
+
+
+ /**
+ * Sets the listener to listen to the session events. A {@code SipSession}
+ * 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
+ */
+ public void setListener(Listener listener) {
+ mListener = 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 State#READY_TO_CALL}.
+ *
+ * @param duration duration in second before the registration expires
+ * @see Listener
+ */
+ public void register(int duration) {
+ try {
+ mSession.register(duration);
+ } catch (RemoteException e) {
+ loge("register:", e);
+ }
+ }
+
+ /**
+ * 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 State#READY_TO_CALL}.
+ *
+ * @see Listener
+ */
+ public void unregister() {
+ try {
+ mSession.unregister();
+ } catch (RemoteException e) {
+ loge("unregister:", e);
+ }
+ }
+
+ /**
+ * 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 State#READY_TO_CALL}.
+ *
+ * @param callee the SIP profile to make the call to
+ * @param sessionDescription the session description of this call
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds. Default value (defined
+ * by SIP protocol) is used if {@code timeout} is zero or negative.
+ * @see Listener
+ */
+ public void makeCall(SipProfile callee, String sessionDescription,
+ int timeout) {
+ try {
+ mSession.makeCall(callee, sessionDescription, timeout);
+ } catch (RemoteException e) {
+ loge("makeCall:", e);
+ }
+ }
+
+ /**
+ * Answers an incoming call with the specified session description. The
+ * method is only valid to call when the session state is in
+ * {@link State#INCOMING_CALL}.
+ *
+ * @param sessionDescription the session description to answer this call
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds. Default value (defined
+ * by SIP protocol) is used if {@code timeout} is zero or negative.
+ */
+ public void answerCall(String sessionDescription, int timeout) {
+ try {
+ mSession.answerCall(sessionDescription, timeout);
+ } catch (RemoteException e) {
+ loge("answerCall:", e);
+ }
+ }
+
+ /**
+ * 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 State#IN_CALL},
+ * {@link State#INCOMING_CALL},
+ * {@link State#OUTGOING_CALL} or
+ * {@link State#OUTGOING_CALL_RING_BACK}.
+ */
+ public void endCall() {
+ try {
+ mSession.endCall();
+ } catch (RemoteException e) {
+ loge("endCall:", e);
+ }
+ }
+
+ /**
+ * Changes the session description during a call. The method is only valid
+ * to call when the session state is in {@link State#IN_CALL}.
+ *
+ * @param sessionDescription the new session description
+ * @param timeout the session will be timed out if the call is not
+ * established within {@code timeout} seconds. Default value (defined
+ * by SIP protocol) is used if {@code timeout} is zero or negative.
+ */
+ public void changeCall(String sessionDescription, int timeout) {
+ try {
+ mSession.changeCall(sessionDescription, timeout);
+ } catch (RemoteException e) {
+ loge("changeCall:", e);
+ }
+ }
+
+ ISipSession getRealSession() {
+ return mSession;
+ }
+
+ private ISipSessionListener createListener() {
+ return new ISipSessionListener.Stub() {
+ @Override
+ public void onCalling(ISipSession session) {
+ if (mListener != null) {
+ mListener.onCalling(SipSession.this);
+ }
+ }
+
+ @Override
+ public void onRinging(ISipSession session, SipProfile caller,
+ String sessionDescription) {
+ if (mListener != null) {
+ mListener.onRinging(SipSession.this, caller,
+ sessionDescription);
+ }
+ }
+
+ @Override
+ public void onRingingBack(ISipSession session) {
+ if (mListener != null) {
+ mListener.onRingingBack(SipSession.this);
+ }
+ }
+
+ @Override
+ public void onCallEstablished(ISipSession session,
+ String sessionDescription) {
+ if (mListener != null) {
+ mListener.onCallEstablished(SipSession.this,
+ sessionDescription);
+ }
+ }
+
+ @Override
+ public void onCallEnded(ISipSession session) {
+ if (mListener != null) {
+ mListener.onCallEnded(SipSession.this);
+ }
+ }
+
+ @Override
+ public void onCallBusy(ISipSession session) {
+ if (mListener != null) {
+ mListener.onCallBusy(SipSession.this);
+ }
+ }
+
+ @Override
+ public void onCallTransferring(ISipSession session,
+ String sessionDescription) {
+ if (mListener != null) {
+ mListener.onCallTransferring(
+ new SipSession(session, SipSession.this.mListener),
+ sessionDescription);
+
+ }
+ }
+
+ @Override
+ public void onCallChangeFailed(ISipSession session, int errorCode,
+ String message) {
+ if (mListener != null) {
+ mListener.onCallChangeFailed(SipSession.this, errorCode,
+ message);
+ }
+ }
+
+ @Override
+ public void onError(ISipSession session, int errorCode, String message) {
+ if (mListener != null) {
+ mListener.onError(SipSession.this, errorCode, message);
+ }
+ }
+
+ @Override
+ public void onRegistering(ISipSession session) {
+ if (mListener != null) {
+ mListener.onRegistering(SipSession.this);
+ }
+ }
+
+ @Override
+ public void onRegistrationDone(ISipSession session, int duration) {
+ if (mListener != null) {
+ mListener.onRegistrationDone(SipSession.this, duration);
+ }
+ }
+
+ @Override
+ public void onRegistrationFailed(ISipSession session, int errorCode,
+ String message) {
+ if (mListener != null) {
+ mListener.onRegistrationFailed(SipSession.this, errorCode,
+ message);
+ }
+ }
+
+ @Override
+ public void onRegistrationTimeout(ISipSession session) {
+ if (mListener != null) {
+ mListener.onRegistrationTimeout(SipSession.this);
+ }
+ }
+ };
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(TAG, s, t);
+ }
+}
diff --git a/java/android/net/sip/SipSessionAdapter.java b/java/android/net/sip/SipSessionAdapter.java
new file mode 100644
index 0000000..f538983
--- /dev/null
+++ b/java/android/net/sip/SipSessionAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * 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,
+ String sessionDescription) {
+ }
+
+ public void onRingingBack(ISipSession session) {
+ }
+
+ public void onCallEstablished(ISipSession session,
+ String sessionDescription) {
+ }
+
+ public void onCallEnded(ISipSession session) {
+ }
+
+ public void onCallBusy(ISipSession session) {
+ }
+
+ public void onCallTransferring(ISipSession session,
+ String sessionDescription) {
+ }
+
+ public void onCallChangeFailed(ISipSession session, int errorCode,
+ String message) {
+ }
+
+ public void onError(ISipSession session, int errorCode, String message) {
+ }
+
+ public void onRegistering(ISipSession session) {
+ }
+
+ public void onRegistrationDone(ISipSession session, int duration) {
+ }
+
+ public void onRegistrationFailed(ISipSession session, int errorCode,
+ String message) {
+ }
+
+ public void onRegistrationTimeout(ISipSession session) {
+ }
+}
diff --git a/java/android/net/sip/package.html b/java/android/net/sip/package.html
new file mode 100644
index 0000000..3c4cc23
--- /dev/null
+++ b/java/android/net/sip/package.html
@@ -0,0 +1,45 @@
+<HTML>
+<BODY>
+<p>Provides access to Session Initiation Protocol (SIP) functionality, such as
+making and answering VOIP calls using SIP.</p>
+
+<p>For more information, see the
+<a href="{@docRoot}guide/topics/connectivity/sip.html">Session Initiation Protocol</a>
+developer guide.</p>
+{@more}
+
+<p>To get started, you need to get an instance of the {@link android.net.sip.SipManager} by
+calling {@link android.net.sip.SipManager#newInstance newInstance()}.</p>
+
+<p>With the {@link android.net.sip.SipManager}, you can initiate SIP audio calls with {@link
+android.net.sip.SipManager#makeAudioCall makeAudioCall()} and {@link
+android.net.sip.SipManager#takeAudioCall takeAudioCall()}. Both methods require
+a {@link android.net.sip.SipAudioCall.Listener} that receives callbacks when the state of the
+call changes, such as when the call is ringing, established, or ended.</p>
+
+<p>Both {@link android.net.sip.SipManager#makeAudioCall makeAudioCall()} also requires two
+{@link android.net.sip.SipProfile} objects, representing the local device and the peer
+device. You can create a {@link android.net.sip.SipProfile} using the {@link
+android.net.sip.SipProfile.Builder} subclass.</p>
+
+<p>Once you have a {@link android.net.sip.SipAudioCall}, you can perform SIP audio call actions with
+the instance, such as make a call, answer a call, mute a call, turn on speaker mode, send DTMF
+tones, and more.</p>
+
+<p>If you want to create generic SIP connections (such as for video calls or other), you can
+create a SIP connection from the {@link android.net.sip.SipManager}, using {@link
+android.net.sip.SipManager#open open()}. If you only want to create audio SIP calls, though, you
+should use the {@link android.net.sip.SipAudioCall} class, as described above.</p>
+
+<p class="note"><strong>Note:</strong>
+Not all Android-powered devices support VOIP functionality with SIP. Before performing any SIP
+activity, you should call {@link android.net.sip.SipManager#isVoipSupported isVoipSupported()}
+to verify that the device supports VOIP calling and {@link
+android.net.sip.SipManager#isApiSupported isApiSupported()} to verify that the device supports the
+SIP APIs.
+Your application must also request the {@link android.Manifest.permission#INTERNET} and {@link
+android.Manifest.permission#USE_SIP} permissions in order to use the SIP APIs.
+</p>
+
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/java/com/android/server/sip/SipHelper.java b/java/com/android/server/sip/SipHelper.java
new file mode 100644
index 0000000..c708be8
--- /dev/null
+++ b/java/com/android/server/sip/SipHelper.java
@@ -0,0 +1,537 @@
+/*
+ * 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 com.android.server.sip;
+
+import gov.nist.javax.sip.SipStackExt;
+import gov.nist.javax.sip.clientauthutils.AccountManager;
+import gov.nist.javax.sip.clientauthutils.AuthenticationHelper;
+import gov.nist.javax.sip.header.extensions.ReferencesHeader;
+import gov.nist.javax.sip.header.extensions.ReferredByHeader;
+import gov.nist.javax.sip.header.extensions.ReplacesHeader;
+
+import android.net.sip.SipProfile;
+import android.telephony.Rlog;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.EventObject;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.sip.ClientTransaction;
+import javax.sip.Dialog;
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.PeerUnavailableException;
+import javax.sip.RequestEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.SipFactory;
+import javax.sip.SipProvider;
+import javax.sip.SipStack;
+import javax.sip.Transaction;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.TransactionState;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.header.CSeqHeader;
+import javax.sip.header.CallIdHeader;
+import javax.sip.header.ContactHeader;
+import javax.sip.header.FromHeader;
+import javax.sip.header.Header;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.MaxForwardsHeader;
+import javax.sip.header.ToHeader;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Message;
+import javax.sip.message.MessageFactory;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/**
+ * Helper class for holding SIP stack related classes and for various low-level
+ * SIP tasks like sending messages.
+ */
+class SipHelper {
+ private static final String TAG = SipHelper.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean DBG_PING = false;
+
+ private SipStack mSipStack;
+ private SipProvider mSipProvider;
+ private AddressFactory mAddressFactory;
+ private HeaderFactory mHeaderFactory;
+ private MessageFactory mMessageFactory;
+
+ public SipHelper(SipStack sipStack, SipProvider sipProvider)
+ throws PeerUnavailableException {
+ mSipStack = sipStack;
+ mSipProvider = sipProvider;
+
+ SipFactory sipFactory = SipFactory.getInstance();
+ mAddressFactory = sipFactory.createAddressFactory();
+ mHeaderFactory = sipFactory.createHeaderFactory();
+ mMessageFactory = sipFactory.createMessageFactory();
+ }
+
+ private FromHeader createFromHeader(SipProfile profile, String tag)
+ throws ParseException {
+ return mHeaderFactory.createFromHeader(profile.getSipAddress(), tag);
+ }
+
+ private ToHeader createToHeader(SipProfile profile) throws ParseException {
+ return createToHeader(profile, null);
+ }
+
+ private ToHeader createToHeader(SipProfile profile, String tag)
+ throws ParseException {
+ return mHeaderFactory.createToHeader(profile.getSipAddress(), tag);
+ }
+
+ private CallIdHeader createCallIdHeader() {
+ return mSipProvider.getNewCallId();
+ }
+
+ private CSeqHeader createCSeqHeader(String method)
+ throws ParseException, InvalidArgumentException {
+ long sequence = (long) (Math.random() * 10000);
+ return mHeaderFactory.createCSeqHeader(sequence, method);
+ }
+
+ private MaxForwardsHeader createMaxForwardsHeader()
+ throws InvalidArgumentException {
+ return mHeaderFactory.createMaxForwardsHeader(70);
+ }
+
+ private MaxForwardsHeader createMaxForwardsHeader(int max)
+ throws InvalidArgumentException {
+ return mHeaderFactory.createMaxForwardsHeader(max);
+ }
+
+ private ListeningPoint getListeningPoint() throws SipException {
+ ListeningPoint lp = mSipProvider.getListeningPoint(ListeningPoint.UDP);
+ if (lp == null) lp = mSipProvider.getListeningPoint(ListeningPoint.TCP);
+ if (lp == null) {
+ ListeningPoint[] lps = mSipProvider.getListeningPoints();
+ if ((lps != null) && (lps.length > 0)) lp = lps[0];
+ }
+ if (lp == null) {
+ throw new SipException("no listening point is available");
+ }
+ return lp;
+ }
+
+ private List<ViaHeader> createViaHeaders()
+ throws ParseException, SipException {
+ List<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(1);
+ ListeningPoint lp = getListeningPoint();
+ ViaHeader viaHeader = mHeaderFactory.createViaHeader(lp.getIPAddress(),
+ lp.getPort(), lp.getTransport(), null);
+ viaHeader.setRPort();
+ viaHeaders.add(viaHeader);
+ return viaHeaders;
+ }
+
+ private ContactHeader createContactHeader(SipProfile profile)
+ throws ParseException, SipException {
+ return createContactHeader(profile, null, 0);
+ }
+
+ private ContactHeader createContactHeader(SipProfile profile,
+ String ip, int port) throws ParseException,
+ SipException {
+ SipURI contactURI = (ip == null)
+ ? createSipUri(profile.getUserName(), profile.getProtocol(),
+ getListeningPoint())
+ : createSipUri(profile.getUserName(), profile.getProtocol(),
+ ip, port);
+
+ Address contactAddress = mAddressFactory.createAddress(contactURI);
+ contactAddress.setDisplayName(profile.getDisplayName());
+
+ return mHeaderFactory.createContactHeader(contactAddress);
+ }
+
+ private ContactHeader createWildcardContactHeader() {
+ ContactHeader contactHeader = mHeaderFactory.createContactHeader();
+ contactHeader.setWildCard();
+ return contactHeader;
+ }
+
+ private SipURI createSipUri(String username, String transport,
+ ListeningPoint lp) throws ParseException {
+ return createSipUri(username, transport, lp.getIPAddress(), lp.getPort());
+ }
+
+ private SipURI createSipUri(String username, String transport,
+ String ip, int port) throws ParseException {
+ SipURI uri = mAddressFactory.createSipURI(username, ip);
+ try {
+ uri.setPort(port);
+ uri.setTransportParam(transport);
+ } catch (InvalidArgumentException e) {
+ throw new RuntimeException(e);
+ }
+ return uri;
+ }
+
+ public ClientTransaction sendOptions(SipProfile caller, SipProfile callee,
+ String tag) throws SipException {
+ try {
+ Request request = (caller == callee)
+ ? createRequest(Request.OPTIONS, caller, tag)
+ : createRequest(Request.OPTIONS, caller, callee, tag);
+
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ clientTransaction.sendRequest();
+ return clientTransaction;
+ } catch (Exception e) {
+ throw new SipException("sendOptions()", e);
+ }
+ }
+
+ public ClientTransaction sendRegister(SipProfile userProfile, String tag,
+ int expiry) throws SipException {
+ try {
+ Request request = createRequest(Request.REGISTER, userProfile, tag);
+ if (expiry == 0) {
+ // remove all previous registrations by wildcard
+ // rfc3261#section-10.2.2
+ request.addHeader(createWildcardContactHeader());
+ } else {
+ request.addHeader(createContactHeader(userProfile));
+ }
+ request.addHeader(mHeaderFactory.createExpiresHeader(expiry));
+
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ clientTransaction.sendRequest();
+ return clientTransaction;
+ } catch (ParseException e) {
+ throw new SipException("sendRegister()", e);
+ }
+ }
+
+ private Request createRequest(String requestType, SipProfile userProfile,
+ String tag) throws ParseException, SipException {
+ FromHeader fromHeader = createFromHeader(userProfile, tag);
+ ToHeader toHeader = createToHeader(userProfile);
+
+ String replaceStr = Pattern.quote(userProfile.getUserName() + "@");
+ SipURI requestURI = mAddressFactory.createSipURI(
+ userProfile.getUriString().replaceFirst(replaceStr, ""));
+
+ List<ViaHeader> viaHeaders = createViaHeaders();
+ CallIdHeader callIdHeader = createCallIdHeader();
+ CSeqHeader cSeqHeader = createCSeqHeader(requestType);
+ MaxForwardsHeader maxForwards = createMaxForwardsHeader();
+ Request request = mMessageFactory.createRequest(requestURI,
+ requestType, callIdHeader, cSeqHeader, fromHeader,
+ toHeader, viaHeaders, maxForwards);
+ Header userAgentHeader = mHeaderFactory.createHeader("User-Agent",
+ "SIPAUA/0.1.001");
+ request.addHeader(userAgentHeader);
+ return request;
+ }
+
+ public ClientTransaction handleChallenge(ResponseEvent responseEvent,
+ AccountManager accountManager) throws SipException {
+ AuthenticationHelper authenticationHelper =
+ ((SipStackExt) mSipStack).getAuthenticationHelper(
+ accountManager, mHeaderFactory);
+ ClientTransaction tid = responseEvent.getClientTransaction();
+ ClientTransaction ct = authenticationHelper.handleChallenge(
+ responseEvent.getResponse(), tid, mSipProvider, 5);
+ if (DBG) log("send request with challenge response: "
+ + ct.getRequest());
+ ct.sendRequest();
+ return ct;
+ }
+
+ private Request createRequest(String requestType, SipProfile caller,
+ SipProfile callee, String tag) throws ParseException, SipException {
+ FromHeader fromHeader = createFromHeader(caller, tag);
+ ToHeader toHeader = createToHeader(callee);
+ SipURI requestURI = callee.getUri();
+ List<ViaHeader> viaHeaders = createViaHeaders();
+ CallIdHeader callIdHeader = createCallIdHeader();
+ CSeqHeader cSeqHeader = createCSeqHeader(requestType);
+ MaxForwardsHeader maxForwards = createMaxForwardsHeader();
+
+ Request request = mMessageFactory.createRequest(requestURI,
+ requestType, callIdHeader, cSeqHeader, fromHeader,
+ toHeader, viaHeaders, maxForwards);
+
+ request.addHeader(createContactHeader(caller));
+ return request;
+ }
+
+ public ClientTransaction sendInvite(SipProfile caller, SipProfile callee,
+ String sessionDescription, String tag, ReferredByHeader referredBy,
+ String replaces) throws SipException {
+ try {
+ Request request = createRequest(Request.INVITE, caller, callee, tag);
+ if (referredBy != null) request.addHeader(referredBy);
+ if (replaces != null) {
+ request.addHeader(mHeaderFactory.createHeader(
+ ReplacesHeader.NAME, replaces));
+ }
+ request.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ if (DBG) log("send INVITE: " + request);
+ clientTransaction.sendRequest();
+ return clientTransaction;
+ } catch (ParseException e) {
+ throw new SipException("sendInvite()", e);
+ }
+ }
+
+ public ClientTransaction sendReinvite(Dialog dialog,
+ String sessionDescription) throws SipException {
+ try {
+ Request request = dialog.createRequest(Request.INVITE);
+ request.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+
+ // Adding rport argument in the request could fix some SIP servers
+ // in resolving the initiator's NAT port mapping for relaying the
+ // response message from the other end.
+
+ ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
+ if (viaHeader != null) viaHeader.setRPort();
+
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ if (DBG) log("send RE-INVITE: " + request);
+ dialog.sendRequest(clientTransaction);
+ return clientTransaction;
+ } catch (ParseException e) {
+ throw new SipException("sendReinvite()", e);
+ }
+ }
+
+ public ServerTransaction getServerTransaction(RequestEvent event)
+ throws SipException {
+ ServerTransaction transaction = event.getServerTransaction();
+ if (transaction == null) {
+ Request request = event.getRequest();
+ return mSipProvider.getNewServerTransaction(request);
+ } else {
+ return transaction;
+ }
+ }
+
+ /**
+ * @param event the INVITE request event
+ */
+ public ServerTransaction sendRinging(RequestEvent event, String tag)
+ throws SipException {
+ try {
+ Request request = event.getRequest();
+ ServerTransaction transaction = getServerTransaction(event);
+
+ Response response = mMessageFactory.createResponse(Response.RINGING,
+ request);
+
+ ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
+ toHeader.setTag(tag);
+ response.addHeader(toHeader);
+ if (DBG) log("send RINGING: " + response);
+ transaction.sendResponse(response);
+ return transaction;
+ } catch (ParseException e) {
+ throw new SipException("sendRinging()", e);
+ }
+ }
+
+ /**
+ * @param event the INVITE request event
+ */
+ public ServerTransaction sendInviteOk(RequestEvent event,
+ SipProfile localProfile, String sessionDescription,
+ ServerTransaction inviteTransaction, String externalIp,
+ int externalPort) throws SipException {
+ try {
+ Request request = event.getRequest();
+ Response response = mMessageFactory.createResponse(Response.OK,
+ request);
+ response.addHeader(createContactHeader(localProfile, externalIp,
+ externalPort));
+ response.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+
+ if (inviteTransaction == null) {
+ inviteTransaction = getServerTransaction(event);
+ }
+
+ if (inviteTransaction.getState() != TransactionState.COMPLETED) {
+ if (DBG) log("send OK: " + response);
+ inviteTransaction.sendResponse(response);
+ }
+
+ return inviteTransaction;
+ } catch (ParseException e) {
+ throw new SipException("sendInviteOk()", e);
+ }
+ }
+
+ public void sendInviteBusyHere(RequestEvent event,
+ ServerTransaction inviteTransaction) throws SipException {
+ try {
+ Request request = event.getRequest();
+ Response response = mMessageFactory.createResponse(
+ Response.BUSY_HERE, request);
+
+ if (inviteTransaction == null) {
+ inviteTransaction = getServerTransaction(event);
+ }
+
+ if (inviteTransaction.getState() != TransactionState.COMPLETED) {
+ if (DBG) log("send BUSY HERE: " + response);
+ inviteTransaction.sendResponse(response);
+ }
+ } catch (ParseException e) {
+ throw new SipException("sendInviteBusyHere()", e);
+ }
+ }
+
+ /**
+ * @param event the INVITE ACK request event
+ */
+ public void sendInviteAck(ResponseEvent event, Dialog dialog)
+ throws SipException {
+ Response response = event.getResponse();
+ long cseq = ((CSeqHeader) response.getHeader(CSeqHeader.NAME))
+ .getSeqNumber();
+ Request ack = dialog.createAck(cseq);
+ if (DBG) log("send ACK: " + ack);
+ dialog.sendAck(ack);
+ }
+
+ public void sendBye(Dialog dialog) throws SipException {
+ Request byeRequest = dialog.createRequest(Request.BYE);
+ if (DBG) log("send BYE: " + byeRequest);
+ dialog.sendRequest(mSipProvider.getNewClientTransaction(byeRequest));
+ }
+
+ public void sendCancel(ClientTransaction inviteTransaction)
+ throws SipException {
+ Request cancelRequest = inviteTransaction.createCancel();
+ if (DBG) log("send CANCEL: " + cancelRequest);
+ mSipProvider.getNewClientTransaction(cancelRequest).sendRequest();
+ }
+
+ public void sendResponse(RequestEvent event, int responseCode)
+ throws SipException {
+ try {
+ Request request = event.getRequest();
+ Response response = mMessageFactory.createResponse(
+ responseCode, request);
+ if (DBG && (!Request.OPTIONS.equals(request.getMethod())
+ || DBG_PING)) {
+ log("send response: " + response);
+ }
+ getServerTransaction(event).sendResponse(response);
+ } catch (ParseException e) {
+ throw new SipException("sendResponse()", e);
+ }
+ }
+
+ public void sendReferNotify(Dialog dialog, String content)
+ throws SipException {
+ try {
+ Request request = dialog.createRequest(Request.NOTIFY);
+ request.addHeader(mHeaderFactory.createSubscriptionStateHeader(
+ "active;expires=60"));
+ // set content here
+ request.setContent(content,
+ mHeaderFactory.createContentTypeHeader(
+ "message", "sipfrag"));
+ request.addHeader(mHeaderFactory.createEventHeader(
+ ReferencesHeader.REFER));
+ if (DBG) log("send NOTIFY: " + request);
+ dialog.sendRequest(mSipProvider.getNewClientTransaction(request));
+ } catch (ParseException e) {
+ throw new SipException("sendReferNotify()", e);
+ }
+ }
+
+ public void sendInviteRequestTerminated(Request inviteRequest,
+ ServerTransaction inviteTransaction) throws SipException {
+ try {
+ Response response = mMessageFactory.createResponse(
+ Response.REQUEST_TERMINATED, inviteRequest);
+ if (DBG) log("send response: " + response);
+ inviteTransaction.sendResponse(response);
+ } catch (ParseException e) {
+ throw new SipException("sendInviteRequestTerminated()", e);
+ }
+ }
+
+ public static String getCallId(EventObject event) {
+ if (event == null) return null;
+ if (event instanceof RequestEvent) {
+ return getCallId(((RequestEvent) event).getRequest());
+ } else if (event instanceof ResponseEvent) {
+ return getCallId(((ResponseEvent) event).getResponse());
+ } else if (event instanceof DialogTerminatedEvent) {
+ Dialog dialog = ((DialogTerminatedEvent) event).getDialog();
+ return getCallId(((DialogTerminatedEvent) event).getDialog());
+ } else if (event instanceof TransactionTerminatedEvent) {
+ TransactionTerminatedEvent e = (TransactionTerminatedEvent) event;
+ return getCallId(e.isServerTransaction()
+ ? e.getServerTransaction()
+ : e.getClientTransaction());
+ } else {
+ Object source = event.getSource();
+ if (source instanceof Transaction) {
+ return getCallId(((Transaction) source));
+ } else if (source instanceof Dialog) {
+ return getCallId((Dialog) source);
+ }
+ }
+ return "";
+ }
+
+ public static String getCallId(Transaction transaction) {
+ return ((transaction != null) ? getCallId(transaction.getRequest())
+ : "");
+ }
+
+ private static String getCallId(Message message) {
+ CallIdHeader callIdHeader =
+ (CallIdHeader) message.getHeader(CallIdHeader.NAME);
+ return callIdHeader.getCallId();
+ }
+
+ private static String getCallId(Dialog dialog) {
+ return dialog.getCallId().getCallId();
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+}
diff --git a/java/com/android/server/sip/SipService.java b/java/com/android/server/sip/SipService.java
new file mode 100644
index 0000000..80fe68c
--- /dev/null
+++ b/java/com/android/server/sip/SipService.java
@@ -0,0 +1,1262 @@
+/*
+ * 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 com.android.server.sip;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.sip.ISipService;
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipErrorCode;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSession;
+import android.net.sip.SipSessionAdapter;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.telephony.Rlog;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import javax.sip.SipException;
+
+/**
+ * @hide
+ */
+public final class SipService extends ISipService.Stub {
+ static final String TAG = "SipService";
+ static final boolean DBG = true;
+ private static final int EXPIRY_TIME = 3600;
+ private static final int SHORT_EXPIRY_TIME = 10;
+ private static final int MIN_EXPIRY_TIME = 60;
+ private static final int DEFAULT_KEEPALIVE_INTERVAL = 10; // in seconds
+ private static final int DEFAULT_MAX_KEEPALIVE_INTERVAL = 120; // in seconds
+
+ private Context mContext;
+ private String mLocalIp;
+ private int mNetworkType = -1;
+ private SipWakeupTimer mTimer;
+ private WifiManager.WifiLock mWifiLock;
+ private boolean mSipOnWifiOnly;
+
+ private SipKeepAliveProcessCallback mSipKeepAliveProcessCallback;
+
+ private MyExecutor mExecutor = new MyExecutor();
+
+ // SipProfile URI --> group
+ private Map<String, SipSessionGroupExt> mSipGroups =
+ new HashMap<String, SipSessionGroupExt>();
+
+ // session ID --> session
+ private Map<String, ISipSession> mPendingSessions =
+ new HashMap<String, ISipSession>();
+
+ private ConnectivityReceiver mConnectivityReceiver;
+ private SipWakeLock mMyWakeLock;
+ private int mKeepAliveInterval;
+ private int mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
+
+ /**
+ * Starts the SIP service. Do nothing if the SIP API is not supported on the
+ * device.
+ */
+ public static void start(Context context) {
+ if (SipManager.isApiSupported(context)) {
+ ServiceManager.addService("sip", new SipService(context));
+ context.sendBroadcast(new Intent(SipManager.ACTION_SIP_SERVICE_UP));
+ if (DBG) slog("start:");
+ }
+ }
+
+ private SipService(Context context) {
+ if (DBG) log("SipService: started!");
+ mContext = context;
+ mConnectivityReceiver = new ConnectivityReceiver();
+
+ mWifiLock = ((WifiManager)
+ context.getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
+ mWifiLock.setReferenceCounted(false);
+ mSipOnWifiOnly = SipManager.isSipWifiOnly(context);
+
+ mMyWakeLock = new SipWakeLock((PowerManager)
+ context.getSystemService(Context.POWER_SERVICE));
+
+ mTimer = new SipWakeupTimer(context, mExecutor);
+ }
+
+ @Override
+ public synchronized SipProfile[] getListOfProfiles() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ boolean isCallerRadio = isCallerRadio();
+ ArrayList<SipProfile> profiles = new ArrayList<SipProfile>();
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ if (isCallerRadio || isCallerCreator(group)) {
+ profiles.add(group.getLocalProfile());
+ }
+ }
+ return profiles.toArray(new SipProfile[profiles.size()]);
+ }
+
+ @Override
+ public synchronized void open(SipProfile localProfile) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ localProfile.setCallingUid(Binder.getCallingUid());
+ try {
+ createGroup(localProfile);
+ } catch (SipException e) {
+ loge("openToMakeCalls()", e);
+ // TODO: how to send the exception back
+ }
+ }
+
+ @Override
+ public synchronized void open3(SipProfile localProfile,
+ PendingIntent incomingCallPendingIntent,
+ ISipSessionListener listener) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ localProfile.setCallingUid(Binder.getCallingUid());
+ if (incomingCallPendingIntent == null) {
+ if (DBG) log("open3: incomingCallPendingIntent cannot be null; "
+ + "the profile is not opened");
+ return;
+ }
+ if (DBG) log("open3: " + localProfile.getUriString() + ": "
+ + incomingCallPendingIntent + ": " + listener);
+ try {
+ SipSessionGroupExt group = createGroup(localProfile,
+ incomingCallPendingIntent, listener);
+ if (localProfile.getAutoRegistration()) {
+ group.openToReceiveCalls();
+ updateWakeLocks();
+ }
+ } catch (SipException e) {
+ loge("open3:", e);
+ // TODO: how to send the exception back
+ }
+ }
+
+ private boolean isCallerCreator(SipSessionGroupExt group) {
+ SipProfile profile = group.getLocalProfile();
+ return (profile.getCallingUid() == Binder.getCallingUid());
+ }
+
+ private boolean isCallerCreatorOrRadio(SipSessionGroupExt group) {
+ return (isCallerRadio() || isCallerCreator(group));
+ }
+
+ private boolean isCallerRadio() {
+ return (Binder.getCallingUid() == Process.PHONE_UID);
+ }
+
+ @Override
+ public synchronized void close(String localProfileUri) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+ if (group == null) return;
+ if (!isCallerCreatorOrRadio(group)) {
+ if (DBG) log("only creator or radio can close this profile");
+ return;
+ }
+
+ group = mSipGroups.remove(localProfileUri);
+ notifyProfileRemoved(group.getLocalProfile());
+ group.close();
+
+ updateWakeLocks();
+ }
+
+ @Override
+ public synchronized boolean isOpened(String localProfileUri) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+ if (group == null) return false;
+ if (isCallerCreatorOrRadio(group)) {
+ return true;
+ } else {
+ if (DBG) log("only creator or radio can query on the profile");
+ return false;
+ }
+ }
+
+ @Override
+ public synchronized boolean isRegistered(String localProfileUri) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+ if (group == null) return false;
+ if (isCallerCreatorOrRadio(group)) {
+ return group.isRegistered();
+ } else {
+ if (DBG) log("only creator or radio can query on the profile");
+ return false;
+ }
+ }
+
+ @Override
+ public synchronized void setRegistrationListener(String localProfileUri,
+ ISipSessionListener listener) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+ if (group == null) return;
+ if (isCallerCreator(group)) {
+ group.setListener(listener);
+ } else {
+ if (DBG) log("only creator can set listener on the profile");
+ }
+ }
+
+ @Override
+ public synchronized ISipSession createSession(SipProfile localProfile,
+ ISipSessionListener listener) {
+ if (DBG) log("createSession: profile" + localProfile);
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ localProfile.setCallingUid(Binder.getCallingUid());
+ if (mNetworkType == -1) {
+ if (DBG) log("createSession: mNetworkType==-1 ret=null");
+ return null;
+ }
+ try {
+ SipSessionGroupExt group = createGroup(localProfile);
+ return group.createSession(listener);
+ } catch (SipException e) {
+ if (DBG) loge("createSession;", e);
+ return null;
+ }
+ }
+
+ @Override
+ public synchronized ISipSession getPendingSession(String callId) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.USE_SIP, null);
+ if (callId == null) return null;
+ return mPendingSessions.get(callId);
+ }
+
+ private String determineLocalIp() {
+ try {
+ DatagramSocket s = new DatagramSocket();
+ s.connect(InetAddress.getByName("192.168.1.1"), 80);
+ return s.getLocalAddress().getHostAddress();
+ } catch (IOException e) {
+ if (DBG) loge("determineLocalIp()", e);
+ // dont do anything; there should be a connectivity change going
+ return null;
+ }
+ }
+
+ private SipSessionGroupExt createGroup(SipProfile localProfile)
+ throws SipException {
+ String key = localProfile.getUriString();
+ SipSessionGroupExt group = mSipGroups.get(key);
+ if (group == null) {
+ group = new SipSessionGroupExt(localProfile, null, null);
+ mSipGroups.put(key, group);
+ notifyProfileAdded(localProfile);
+ } else if (!isCallerCreator(group)) {
+ throw new SipException("only creator can access the profile");
+ }
+ return group;
+ }
+
+ private SipSessionGroupExt createGroup(SipProfile localProfile,
+ PendingIntent incomingCallPendingIntent,
+ ISipSessionListener listener) throws SipException {
+ String key = localProfile.getUriString();
+ SipSessionGroupExt group = mSipGroups.get(key);
+ if (group != null) {
+ if (!isCallerCreator(group)) {
+ throw new SipException("only creator can access the profile");
+ }
+ group.setIncomingCallPendingIntent(incomingCallPendingIntent);
+ group.setListener(listener);
+ } else {
+ group = new SipSessionGroupExt(localProfile,
+ incomingCallPendingIntent, listener);
+ mSipGroups.put(key, group);
+ notifyProfileAdded(localProfile);
+ }
+ return group;
+ }
+
+ private void notifyProfileAdded(SipProfile localProfile) {
+ if (DBG) log("notify: profile added: " + localProfile);
+ Intent intent = new Intent(SipManager.ACTION_SIP_ADD_PHONE);
+ intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
+ mContext.sendBroadcast(intent);
+ if (mSipGroups.size() == 1) {
+ registerReceivers();
+ }
+ }
+
+ private void notifyProfileRemoved(SipProfile localProfile) {
+ if (DBG) log("notify: profile removed: " + localProfile);
+ Intent intent = new Intent(SipManager.ACTION_SIP_REMOVE_PHONE);
+ intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
+ mContext.sendBroadcast(intent);
+ if (mSipGroups.size() == 0) {
+ unregisterReceivers();
+ }
+ }
+
+ private void stopPortMappingMeasurement() {
+ if (mSipKeepAliveProcessCallback != null) {
+ mSipKeepAliveProcessCallback.stop();
+ mSipKeepAliveProcessCallback = null;
+ }
+ }
+
+ private void startPortMappingLifetimeMeasurement(
+ SipProfile localProfile) {
+ startPortMappingLifetimeMeasurement(localProfile,
+ DEFAULT_MAX_KEEPALIVE_INTERVAL);
+ }
+
+ private void startPortMappingLifetimeMeasurement(
+ SipProfile localProfile, int maxInterval) {
+ if ((mSipKeepAliveProcessCallback == null)
+ && (mKeepAliveInterval == -1)
+ && isBehindNAT(mLocalIp)) {
+ if (DBG) log("startPortMappingLifetimeMeasurement: profile="
+ + localProfile.getUriString());
+
+ int minInterval = mLastGoodKeepAliveInterval;
+ if (minInterval >= maxInterval) {
+ // If mLastGoodKeepAliveInterval also does not work, reset it
+ // to the default min
+ minInterval = mLastGoodKeepAliveInterval
+ = DEFAULT_KEEPALIVE_INTERVAL;
+ log(" reset min interval to " + minInterval);
+ }
+ mSipKeepAliveProcessCallback = new SipKeepAliveProcessCallback(
+ localProfile, minInterval, maxInterval);
+ mSipKeepAliveProcessCallback.start();
+ }
+ }
+
+ private void restartPortMappingLifetimeMeasurement(
+ SipProfile localProfile, int maxInterval) {
+ stopPortMappingMeasurement();
+ mKeepAliveInterval = -1;
+ startPortMappingLifetimeMeasurement(localProfile, maxInterval);
+ }
+
+ private synchronized void addPendingSession(ISipSession session) {
+ try {
+ cleanUpPendingSessions();
+ mPendingSessions.put(session.getCallId(), session);
+ if (DBG) log("#pending sess=" + mPendingSessions.size());
+ } catch (RemoteException e) {
+ // should not happen with a local call
+ loge("addPendingSession()", e);
+ }
+ }
+
+ private void cleanUpPendingSessions() throws RemoteException {
+ Map.Entry<String, ISipSession>[] entries =
+ mPendingSessions.entrySet().toArray(
+ new Map.Entry[mPendingSessions.size()]);
+ for (Map.Entry<String, ISipSession> entry : entries) {
+ if (entry.getValue().getState() != SipSession.State.INCOMING_CALL) {
+ mPendingSessions.remove(entry.getKey());
+ }
+ }
+ }
+
+ private synchronized boolean callingSelf(SipSessionGroupExt ringingGroup,
+ SipSessionGroup.SipSessionImpl ringingSession) {
+ String callId = ringingSession.getCallId();
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ if ((group != ringingGroup) && group.containsSession(callId)) {
+ if (DBG) log("call self: "
+ + ringingSession.getLocalProfile().getUriString()
+ + " -> " + group.getLocalProfile().getUriString());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private synchronized void onKeepAliveIntervalChanged() {
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ group.onKeepAliveIntervalChanged();
+ }
+ }
+
+ private int getKeepAliveInterval() {
+ return (mKeepAliveInterval < 0)
+ ? mLastGoodKeepAliveInterval
+ : mKeepAliveInterval;
+ }
+
+ private boolean isBehindNAT(String address) {
+ try {
+ // TODO: How is isBehindNAT used and why these constanst address:
+ // 10.x.x.x | 192.168.x.x | 172.16.x.x .. 172.19.x.x
+ byte[] d = InetAddress.getByName(address).getAddress();
+ if ((d[0] == 10) ||
+ (((0x000000FF & d[0]) == 172) &&
+ ((0x000000F0 & d[1]) == 16)) ||
+ (((0x000000FF & d[0]) == 192) &&
+ ((0x000000FF & d[1]) == 168))) {
+ return true;
+ }
+ } catch (UnknownHostException e) {
+ loge("isBehindAT()" + address, e);
+ }
+ return false;
+ }
+
+ private class SipSessionGroupExt extends SipSessionAdapter {
+ private static final String SSGE_TAG = "SipSessionGroupExt";
+ private static final boolean SSGE_DBG = true;
+ private SipSessionGroup mSipGroup;
+ private PendingIntent mIncomingCallPendingIntent;
+ private boolean mOpenedToReceiveCalls;
+
+ private SipAutoReg mAutoRegistration =
+ new SipAutoReg();
+
+ public SipSessionGroupExt(SipProfile localProfile,
+ PendingIntent incomingCallPendingIntent,
+ ISipSessionListener listener) throws SipException {
+ if (SSGE_DBG) log("SipSessionGroupExt: profile=" + localProfile);
+ mSipGroup = new SipSessionGroup(duplicate(localProfile),
+ localProfile.getPassword(), mTimer, mMyWakeLock);
+ mIncomingCallPendingIntent = incomingCallPendingIntent;
+ mAutoRegistration.setListener(listener);
+ }
+
+ public SipProfile getLocalProfile() {
+ return mSipGroup.getLocalProfile();
+ }
+
+ public boolean containsSession(String callId) {
+ return mSipGroup.containsSession(callId);
+ }
+
+ public void onKeepAliveIntervalChanged() {
+ mAutoRegistration.onKeepAliveIntervalChanged();
+ }
+
+ // TODO: remove this method once SipWakeupTimer can better handle variety
+ // of timeout values
+ void setWakeupTimer(SipWakeupTimer timer) {
+ mSipGroup.setWakeupTimer(timer);
+ }
+
+ private SipProfile duplicate(SipProfile p) {
+ try {
+ return new SipProfile.Builder(p).setPassword("*").build();
+ } catch (Exception e) {
+ loge("duplicate()", e);
+ throw new RuntimeException("duplicate profile", e);
+ }
+ }
+
+ public void setListener(ISipSessionListener listener) {
+ mAutoRegistration.setListener(listener);
+ }
+
+ public void setIncomingCallPendingIntent(PendingIntent pIntent) {
+ mIncomingCallPendingIntent = pIntent;
+ }
+
+ public void openToReceiveCalls() throws SipException {
+ mOpenedToReceiveCalls = true;
+ if (mNetworkType != -1) {
+ mSipGroup.openToReceiveCalls(this);
+ mAutoRegistration.start(mSipGroup);
+ }
+ if (SSGE_DBG) log("openToReceiveCalls: " + getUri() + ": "
+ + mIncomingCallPendingIntent);
+ }
+
+ public void onConnectivityChanged(boolean connected)
+ throws SipException {
+ if (SSGE_DBG) {
+ log("onConnectivityChanged: connected=" + connected + " uri="
+ + getUri() + ": " + mIncomingCallPendingIntent);
+ }
+ mSipGroup.onConnectivityChanged();
+ if (connected) {
+ mSipGroup.reset();
+ if (mOpenedToReceiveCalls) openToReceiveCalls();
+ } else {
+ mSipGroup.close();
+ mAutoRegistration.stop();
+ }
+ }
+
+ public void close() {
+ mOpenedToReceiveCalls = false;
+ mSipGroup.close();
+ mAutoRegistration.stop();
+ if (SSGE_DBG) log("close: " + getUri() + ": " + mIncomingCallPendingIntent);
+ }
+
+ public ISipSession createSession(ISipSessionListener listener) {
+ if (SSGE_DBG) log("createSession");
+ return mSipGroup.createSession(listener);
+ }
+
+ @Override
+ public void onRinging(ISipSession s, SipProfile caller,
+ String sessionDescription) {
+ SipSessionGroup.SipSessionImpl session =
+ (SipSessionGroup.SipSessionImpl) s;
+ synchronized (SipService.this) {
+ try {
+ if (!isRegistered() || callingSelf(this, session)) {
+ if (SSGE_DBG) log("onRinging: end notReg or self");
+ session.endCall();
+ return;
+ }
+
+ // send out incoming call broadcast
+ addPendingSession(session);
+ Intent intent = SipManager.createIncomingCallBroadcast(
+ session.getCallId(), sessionDescription);
+ if (SSGE_DBG) log("onRinging: uri=" + getUri() + ": "
+ + caller.getUri() + ": " + session.getCallId()
+ + " " + mIncomingCallPendingIntent);
+ mIncomingCallPendingIntent.send(mContext,
+ SipManager.INCOMING_CALL_RESULT_CODE, intent);
+ } catch (PendingIntent.CanceledException e) {
+ loge("onRinging: pendingIntent is canceled, drop incoming call", e);
+ session.endCall();
+ }
+ }
+ }
+
+ @Override
+ public void onError(ISipSession session, int errorCode,
+ String message) {
+ if (SSGE_DBG) log("onError: errorCode=" + errorCode + " desc="
+ + SipErrorCode.toString(errorCode) + ": " + message);
+ }
+
+ public boolean isOpenedToReceiveCalls() {
+ return mOpenedToReceiveCalls;
+ }
+
+ public boolean isRegistered() {
+ return mAutoRegistration.isRegistered();
+ }
+
+ private String getUri() {
+ return mSipGroup.getLocalProfileUri();
+ }
+
+ private void log(String s) {
+ Rlog.d(SSGE_TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(SSGE_TAG, s, t);
+ }
+
+ }
+
+ private class SipKeepAliveProcessCallback implements Runnable,
+ SipSessionGroup.KeepAliveProcessCallback {
+ private static final String SKAI_TAG = "SipKeepAliveProcessCallback";
+ private static final boolean SKAI_DBG = true;
+ private static final int MIN_INTERVAL = 5; // in seconds
+ private static final int PASS_THRESHOLD = 10;
+ private static final int NAT_MEASUREMENT_RETRY_INTERVAL = 120; // in seconds
+ private SipProfile mLocalProfile;
+ private SipSessionGroupExt mGroup;
+ private SipSessionGroup.SipSessionImpl mSession;
+ private int mMinInterval;
+ private int mMaxInterval;
+ private int mInterval;
+ private int mPassCount;
+
+ public SipKeepAliveProcessCallback(SipProfile localProfile,
+ int minInterval, int maxInterval) {
+ mMaxInterval = maxInterval;
+ mMinInterval = minInterval;
+ mLocalProfile = localProfile;
+ }
+
+ public void start() {
+ synchronized (SipService.this) {
+ if (mSession != null) {
+ return;
+ }
+
+ mInterval = (mMaxInterval + mMinInterval) / 2;
+ mPassCount = 0;
+
+ // Don't start measurement if the interval is too small
+ if (mInterval < DEFAULT_KEEPALIVE_INTERVAL || checkTermination()) {
+ if (SKAI_DBG) log("start: measurement aborted; interval=[" +
+ mMinInterval + "," + mMaxInterval + "]");
+ return;
+ }
+
+ try {
+ if (SKAI_DBG) log("start: interval=" + mInterval);
+
+ mGroup = new SipSessionGroupExt(mLocalProfile, null, null);
+ // TODO: remove this line once SipWakeupTimer can better handle
+ // variety of timeout values
+ mGroup.setWakeupTimer(new SipWakeupTimer(mContext, mExecutor));
+
+ mSession = (SipSessionGroup.SipSessionImpl)
+ mGroup.createSession(null);
+ mSession.startKeepAliveProcess(mInterval, this);
+ } catch (Throwable t) {
+ onError(SipErrorCode.CLIENT_ERROR, t.toString());
+ }
+ }
+ }
+
+ public void stop() {
+ synchronized (SipService.this) {
+ if (mSession != null) {
+ mSession.stopKeepAliveProcess();
+ mSession = null;
+ }
+ if (mGroup != null) {
+ mGroup.close();
+ mGroup = null;
+ }
+ mTimer.cancel(this);
+ if (SKAI_DBG) log("stop");
+ }
+ }
+
+ private void restart() {
+ synchronized (SipService.this) {
+ // Return immediately if the measurement process is stopped
+ if (mSession == null) return;
+
+ if (SKAI_DBG) log("restart: interval=" + mInterval);
+ try {
+ mSession.stopKeepAliveProcess();
+ mPassCount = 0;
+ mSession.startKeepAliveProcess(mInterval, this);
+ } catch (SipException e) {
+ loge("restart", e);
+ }
+ }
+ }
+
+ private boolean checkTermination() {
+ return ((mMaxInterval - mMinInterval) < MIN_INTERVAL);
+ }
+
+ // SipSessionGroup.KeepAliveProcessCallback
+ @Override
+ public void onResponse(boolean portChanged) {
+ synchronized (SipService.this) {
+ if (!portChanged) {
+ if (++mPassCount != PASS_THRESHOLD) return;
+ // update the interval, since the current interval is good to
+ // keep the port mapping.
+ if (mKeepAliveInterval > 0) {
+ mLastGoodKeepAliveInterval = mKeepAliveInterval;
+ }
+ mKeepAliveInterval = mMinInterval = mInterval;
+ if (SKAI_DBG) {
+ log("onResponse: portChanged=" + portChanged + " mKeepAliveInterval="
+ + mKeepAliveInterval);
+ }
+ onKeepAliveIntervalChanged();
+ } else {
+ // Since the rport is changed, shorten the interval.
+ mMaxInterval = mInterval;
+ }
+ if (checkTermination()) {
+ // update mKeepAliveInterval and stop measurement.
+ stop();
+ // If all the measurements failed, we still set it to
+ // mMinInterval; If mMinInterval still doesn't work, a new
+ // measurement with min interval=DEFAULT_KEEPALIVE_INTERVAL
+ // will be conducted.
+ mKeepAliveInterval = mMinInterval;
+ if (SKAI_DBG) {
+ log("onResponse: checkTermination mKeepAliveInterval="
+ + mKeepAliveInterval);
+ }
+ } else {
+ // calculate the new interval and continue.
+ mInterval = (mMaxInterval + mMinInterval) / 2;
+ if (SKAI_DBG) {
+ log("onResponse: mKeepAliveInterval=" + mKeepAliveInterval
+ + ", new mInterval=" + mInterval);
+ }
+ restart();
+ }
+ }
+ }
+
+ // SipSessionGroup.KeepAliveProcessCallback
+ @Override
+ public void onError(int errorCode, String description) {
+ if (SKAI_DBG) loge("onError: errorCode=" + errorCode + " desc=" + description);
+ restartLater();
+ }
+
+ // timeout handler
+ @Override
+ public void run() {
+ mTimer.cancel(this);
+ restart();
+ }
+
+ private void restartLater() {
+ synchronized (SipService.this) {
+ int interval = NAT_MEASUREMENT_RETRY_INTERVAL;
+ mTimer.cancel(this);
+ mTimer.set(interval * 1000, this);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(SKAI_TAG, s);
+ }
+
+ private void loge(String s) {
+ Rlog.d(SKAI_TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.d(SKAI_TAG, s, t);
+ }
+ }
+
+ private class SipAutoReg extends SipSessionAdapter
+ implements Runnable, SipSessionGroup.KeepAliveProcessCallback {
+ private String SAR_TAG;
+ private static final boolean SAR_DBG = true;
+ private static final int MIN_KEEPALIVE_SUCCESS_COUNT = 10;
+
+ private SipSessionGroup.SipSessionImpl mSession;
+ private SipSessionGroup.SipSessionImpl mKeepAliveSession;
+ private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
+ private int mBackoff = 1;
+ private boolean mRegistered;
+ private long mExpiryTime;
+ private int mErrorCode;
+ private String mErrorMessage;
+ private boolean mRunning = false;
+
+ private int mKeepAliveSuccessCount = 0;
+
+ public void start(SipSessionGroup group) {
+ if (!mRunning) {
+ mRunning = true;
+ mBackoff = 1;
+ mSession = (SipSessionGroup.SipSessionImpl)
+ group.createSession(this);
+ // return right away if no active network connection.
+ if (mSession == null) return;
+
+ // start unregistration to clear up old registration at server
+ // TODO: when rfc5626 is deployed, use reg-id and sip.instance
+ // in registration to avoid adding duplicate entries to server
+ mMyWakeLock.acquire(mSession);
+ mSession.unregister();
+ SAR_TAG = "SipAutoReg:" + mSession.getLocalProfile().getUriString();
+ if (SAR_DBG) log("start: group=" + group);
+ }
+ }
+
+ private void startKeepAliveProcess(int interval) {
+ if (SAR_DBG) log("startKeepAliveProcess: interval=" + interval);
+ if (mKeepAliveSession == null) {
+ mKeepAliveSession = mSession.duplicate();
+ } else {
+ mKeepAliveSession.stopKeepAliveProcess();
+ }
+ try {
+ mKeepAliveSession.startKeepAliveProcess(interval, this);
+ } catch (SipException e) {
+ loge("startKeepAliveProcess: interval=" + interval, e);
+ }
+ }
+
+ private void stopKeepAliveProcess() {
+ if (mKeepAliveSession != null) {
+ mKeepAliveSession.stopKeepAliveProcess();
+ mKeepAliveSession = null;
+ }
+ mKeepAliveSuccessCount = 0;
+ }
+
+ // SipSessionGroup.KeepAliveProcessCallback
+ @Override
+ public void onResponse(boolean portChanged) {
+ synchronized (SipService.this) {
+ if (portChanged) {
+ int interval = getKeepAliveInterval();
+ if (mKeepAliveSuccessCount < MIN_KEEPALIVE_SUCCESS_COUNT) {
+ if (SAR_DBG) {
+ log("onResponse: keepalive doesn't work with interval "
+ + interval + ", past success count="
+ + mKeepAliveSuccessCount);
+ }
+ if (interval > DEFAULT_KEEPALIVE_INTERVAL) {
+ restartPortMappingLifetimeMeasurement(
+ mSession.getLocalProfile(), interval);
+ mKeepAliveSuccessCount = 0;
+ }
+ } else {
+ if (SAR_DBG) {
+ log("keep keepalive going with interval "
+ + interval + ", past success count="
+ + mKeepAliveSuccessCount);
+ }
+ mKeepAliveSuccessCount /= 2;
+ }
+ } else {
+ // Start keep-alive interval measurement on the first
+ // successfully kept-alive SipSessionGroup
+ startPortMappingLifetimeMeasurement(
+ mSession.getLocalProfile());
+ mKeepAliveSuccessCount++;
+ }
+
+ if (!mRunning || !portChanged) return;
+
+ // The keep alive process is stopped when port is changed;
+ // Nullify the session so that the process can be restarted
+ // again when the re-registration is done
+ mKeepAliveSession = null;
+
+ // Acquire wake lock for the registration process. The
+ // lock will be released when registration is complete.
+ mMyWakeLock.acquire(mSession);
+ mSession.register(EXPIRY_TIME);
+ }
+ }
+
+ // SipSessionGroup.KeepAliveProcessCallback
+ @Override
+ public void onError(int errorCode, String description) {
+ if (SAR_DBG) {
+ loge("onError: errorCode=" + errorCode + " desc=" + description);
+ }
+ onResponse(true); // re-register immediately
+ }
+
+ public void stop() {
+ if (!mRunning) return;
+ mRunning = false;
+ mMyWakeLock.release(mSession);
+ if (mSession != null) {
+ mSession.setListener(null);
+ if (mNetworkType != -1 && mRegistered) mSession.unregister();
+ }
+
+ mTimer.cancel(this);
+ stopKeepAliveProcess();
+
+ mRegistered = false;
+ setListener(mProxy.getListener());
+ }
+
+ public void onKeepAliveIntervalChanged() {
+ if (mKeepAliveSession != null) {
+ int newInterval = getKeepAliveInterval();
+ if (SAR_DBG) {
+ log("onKeepAliveIntervalChanged: interval=" + newInterval);
+ }
+ mKeepAliveSuccessCount = 0;
+ startKeepAliveProcess(newInterval);
+ }
+ }
+
+ public void setListener(ISipSessionListener listener) {
+ synchronized (SipService.this) {
+ mProxy.setListener(listener);
+
+ try {
+ int state = (mSession == null)
+ ? SipSession.State.READY_TO_CALL
+ : mSession.getState();
+ if ((state == SipSession.State.REGISTERING)
+ || (state == SipSession.State.DEREGISTERING)) {
+ mProxy.onRegistering(mSession);
+ } else if (mRegistered) {
+ int duration = (int)
+ (mExpiryTime - SystemClock.elapsedRealtime());
+ mProxy.onRegistrationDone(mSession, duration);
+ } else if (mErrorCode != SipErrorCode.NO_ERROR) {
+ if (mErrorCode == SipErrorCode.TIME_OUT) {
+ mProxy.onRegistrationTimeout(mSession);
+ } else {
+ mProxy.onRegistrationFailed(mSession, mErrorCode,
+ mErrorMessage);
+ }
+ } else if (mNetworkType == -1) {
+ mProxy.onRegistrationFailed(mSession,
+ SipErrorCode.DATA_CONNECTION_LOST,
+ "no data connection");
+ } else if (!mRunning) {
+ mProxy.onRegistrationFailed(mSession,
+ SipErrorCode.CLIENT_ERROR,
+ "registration not running");
+ } else {
+ mProxy.onRegistrationFailed(mSession,
+ SipErrorCode.IN_PROGRESS,
+ String.valueOf(state));
+ }
+ } catch (Throwable t) {
+ loge("setListener: ", t);
+ }
+ }
+ }
+
+ public boolean isRegistered() {
+ return mRegistered;
+ }
+
+ // timeout handler: re-register
+ @Override
+ public void run() {
+ synchronized (SipService.this) {
+ if (!mRunning) return;
+
+ mErrorCode = SipErrorCode.NO_ERROR;
+ mErrorMessage = null;
+ if (SAR_DBG) log("run: registering");
+ if (mNetworkType != -1) {
+ mMyWakeLock.acquire(mSession);
+ mSession.register(EXPIRY_TIME);
+ }
+ }
+ }
+
+ private void restart(int duration) {
+ if (SAR_DBG) log("restart: duration=" + duration + "s later.");
+ mTimer.cancel(this);
+ mTimer.set(duration * 1000, this);
+ }
+
+ private int backoffDuration() {
+ int duration = SHORT_EXPIRY_TIME * mBackoff;
+ if (duration > 3600) {
+ duration = 3600;
+ } else {
+ mBackoff *= 2;
+ }
+ return duration;
+ }
+
+ @Override
+ public void onRegistering(ISipSession session) {
+ if (SAR_DBG) log("onRegistering: " + session);
+ synchronized (SipService.this) {
+ if (notCurrentSession(session)) return;
+
+ mRegistered = false;
+ mProxy.onRegistering(session);
+ }
+ }
+
+ private boolean notCurrentSession(ISipSession session) {
+ if (session != mSession) {
+ ((SipSessionGroup.SipSessionImpl) session).setListener(null);
+ mMyWakeLock.release(session);
+ return true;
+ }
+ return !mRunning;
+ }
+
+ @Override
+ public void onRegistrationDone(ISipSession session, int duration) {
+ if (SAR_DBG) log("onRegistrationDone: " + session);
+ synchronized (SipService.this) {
+ if (notCurrentSession(session)) return;
+
+ mProxy.onRegistrationDone(session, duration);
+
+ if (duration > 0) {
+ mExpiryTime = SystemClock.elapsedRealtime()
+ + (duration * 1000);
+
+ if (!mRegistered) {
+ mRegistered = true;
+ // allow some overlap to avoid call drop during renew
+ duration -= MIN_EXPIRY_TIME;
+ if (duration < MIN_EXPIRY_TIME) {
+ duration = MIN_EXPIRY_TIME;
+ }
+ restart(duration);
+
+ SipProfile localProfile = mSession.getLocalProfile();
+ if ((mKeepAliveSession == null) && (isBehindNAT(mLocalIp)
+ || localProfile.getSendKeepAlive())) {
+ startKeepAliveProcess(getKeepAliveInterval());
+ }
+ }
+ mMyWakeLock.release(session);
+ } else {
+ mRegistered = false;
+ mExpiryTime = -1L;
+ if (SAR_DBG) log("Refresh registration immediately");
+ run();
+ }
+ }
+ }
+
+ @Override
+ public void onRegistrationFailed(ISipSession session, int errorCode,
+ String message) {
+ if (SAR_DBG) log("onRegistrationFailed: " + session + ": "
+ + SipErrorCode.toString(errorCode) + ": " + message);
+ synchronized (SipService.this) {
+ if (notCurrentSession(session)) return;
+
+ switch (errorCode) {
+ case SipErrorCode.INVALID_CREDENTIALS:
+ case SipErrorCode.SERVER_UNREACHABLE:
+ if (SAR_DBG) log(" pause auto-registration");
+ stop();
+ break;
+ default:
+ restartLater();
+ }
+
+ mErrorCode = errorCode;
+ mErrorMessage = message;
+ mProxy.onRegistrationFailed(session, errorCode, message);
+ mMyWakeLock.release(session);
+ }
+ }
+
+ @Override
+ public void onRegistrationTimeout(ISipSession session) {
+ if (SAR_DBG) log("onRegistrationTimeout: " + session);
+ synchronized (SipService.this) {
+ if (notCurrentSession(session)) return;
+
+ mErrorCode = SipErrorCode.TIME_OUT;
+ mProxy.onRegistrationTimeout(session);
+ restartLater();
+ mMyWakeLock.release(session);
+ }
+ }
+
+ private void restartLater() {
+ if (SAR_DBG) loge("restartLater");
+ mRegistered = false;
+ restart(backoffDuration());
+ }
+
+ private void log(String s) {
+ Rlog.d(SAR_TAG, s);
+ }
+
+ private void loge(String s) {
+ Rlog.e(SAR_TAG, s);
+ }
+
+ private void loge(String s, Throwable e) {
+ Rlog.e(SAR_TAG, s, e);
+ }
+ }
+
+ private class ConnectivityReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ final NetworkInfo info = (NetworkInfo)
+ bundle.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+
+ // Run the handler in MyExecutor to be protected by wake lock
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onConnectivityChanged(info);
+ }
+ });
+ }
+ }
+ }
+
+ private void registerReceivers() {
+ mContext.registerReceiver(mConnectivityReceiver,
+ new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ if (DBG) log("registerReceivers:");
+ }
+
+ private void unregisterReceivers() {
+ mContext.unregisterReceiver(mConnectivityReceiver);
+ if (DBG) log("unregisterReceivers:");
+
+ // Reset variables maintained by ConnectivityReceiver.
+ mWifiLock.release();
+ mNetworkType = -1;
+ }
+
+ private void updateWakeLocks() {
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ if (group.isOpenedToReceiveCalls()) {
+ // Also grab the WifiLock when we are disconnected, so the
+ // system will keep trying to reconnect. It will be released
+ // when the system eventually connects to something else.
+ if (mNetworkType == ConnectivityManager.TYPE_WIFI || mNetworkType == -1) {
+ mWifiLock.acquire();
+ } else {
+ mWifiLock.release();
+ }
+ return;
+ }
+ }
+ mWifiLock.release();
+ mMyWakeLock.reset(); // in case there's a leak
+ }
+
+ private synchronized void onConnectivityChanged(NetworkInfo info) {
+ // We only care about the default network, and getActiveNetworkInfo()
+ // is the only way to distinguish them. However, as broadcasts are
+ // delivered asynchronously, we might miss DISCONNECTED events from
+ // getActiveNetworkInfo(), which is critical to our SIP stack. To
+ // solve this, if it is a DISCONNECTED event to our current network,
+ // respect it. Otherwise get a new one from getActiveNetworkInfo().
+ if (info == null || info.isConnected() || info.getType() != mNetworkType) {
+ ConnectivityManager cm = (ConnectivityManager)
+ mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ info = cm.getActiveNetworkInfo();
+ }
+
+ // Some devices limit SIP on Wi-Fi. In this case, if we are not on
+ // Wi-Fi, treat it as a DISCONNECTED event.
+ int networkType = (info != null && info.isConnected()) ? info.getType() : -1;
+ if (mSipOnWifiOnly && networkType != ConnectivityManager.TYPE_WIFI) {
+ networkType = -1;
+ }
+
+ // Ignore the event if the current active network is not changed.
+ if (mNetworkType == networkType) {
+ // TODO: Maybe we need to send seq/generation number
+ return;
+ }
+ if (DBG) {
+ log("onConnectivityChanged: " + mNetworkType +
+ " -> " + networkType);
+ }
+
+ try {
+ if (mNetworkType != -1) {
+ mLocalIp = null;
+ stopPortMappingMeasurement();
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ group.onConnectivityChanged(false);
+ }
+ }
+ mNetworkType = networkType;
+
+ if (mNetworkType != -1) {
+ mLocalIp = determineLocalIp();
+ mKeepAliveInterval = -1;
+ mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
+ for (SipSessionGroupExt group : mSipGroups.values()) {
+ group.onConnectivityChanged(true);
+ }
+ }
+ updateWakeLocks();
+ } catch (SipException e) {
+ loge("onConnectivityChanged()", e);
+ }
+ }
+
+ private static Looper createLooper() {
+ HandlerThread thread = new HandlerThread("SipService.Executor");
+ thread.start();
+ return thread.getLooper();
+ }
+
+ // Executes immediate tasks in a single thread.
+ // Hold/release wake lock for running tasks
+ private class MyExecutor extends Handler implements Executor {
+ MyExecutor() {
+ super(createLooper());
+ }
+
+ @Override
+ public void execute(Runnable task) {
+ mMyWakeLock.acquire(task);
+ Message.obtain(this, 0/* don't care */, task).sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.obj instanceof Runnable) {
+ executeInternal((Runnable) msg.obj);
+ } else {
+ if (DBG) log("handleMessage: not Runnable ignore msg=" + msg);
+ }
+ }
+
+ private void executeInternal(Runnable task) {
+ try {
+ task.run();
+ } catch (Throwable t) {
+ loge("run task: " + task, t);
+ } finally {
+ mMyWakeLock.release(task);
+ }
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private static void slog(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s, Throwable e) {
+ Rlog.e(TAG, s, e);
+ }
+}
diff --git a/java/com/android/server/sip/SipSessionGroup.java b/java/com/android/server/sip/SipSessionGroup.java
new file mode 100644
index 0000000..e820f35
--- /dev/null
+++ b/java/com/android/server/sip/SipSessionGroup.java
@@ -0,0 +1,1863 @@
+/*
+ * 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 com.android.server.sip;
+
+import gov.nist.javax.sip.clientauthutils.AccountManager;
+import gov.nist.javax.sip.clientauthutils.UserCredentials;
+import gov.nist.javax.sip.header.ProxyAuthenticate;
+import gov.nist.javax.sip.header.ReferTo;
+import gov.nist.javax.sip.header.SIPHeaderNames;
+import gov.nist.javax.sip.header.StatusLine;
+import gov.nist.javax.sip.header.WWWAuthenticate;
+import gov.nist.javax.sip.header.extensions.ReferredByHeader;
+import gov.nist.javax.sip.header.extensions.ReplacesHeader;
+import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.message.SIPResponse;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipErrorCode;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSession;
+import android.net.sip.SipSessionAdapter;
+import android.text.TextUtils;
+import android.telephony.Rlog;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.sip.ClientTransaction;
+import javax.sip.Dialog;
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.IOExceptionEvent;
+import javax.sip.ObjectInUseException;
+import javax.sip.RequestEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.SipFactory;
+import javax.sip.SipListener;
+import javax.sip.SipProvider;
+import javax.sip.SipStack;
+import javax.sip.TimeoutEvent;
+import javax.sip.Transaction;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.address.Address;
+import javax.sip.address.SipURI;
+import javax.sip.header.CSeqHeader;
+import javax.sip.header.ContactHeader;
+import javax.sip.header.ExpiresHeader;
+import javax.sip.header.FromHeader;
+import javax.sip.header.HeaderAddress;
+import javax.sip.header.MinExpiresHeader;
+import javax.sip.header.ReferToHeader;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Message;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+
+/**
+ * Manages {@link ISipSession}'s for a SIP account.
+ */
+class SipSessionGroup implements SipListener {
+ private static final String TAG = "SipSession";
+ private static final boolean DBG = false;
+ private static final boolean DBG_PING = false;
+ private static final String ANONYMOUS = "anonymous";
+ // Limit the size of thread pool to 1 for the order issue when the phone is
+ // waken up from sleep and there are many packets to be processed in the SIP
+ // stack. Note: The default thread pool size in NIST SIP stack is -1 which is
+ // unlimited.
+ private static final String THREAD_POOL_SIZE = "1";
+ private static final int EXPIRY_TIME = 3600; // in seconds
+ private static final int CANCEL_CALL_TIMER = 3; // in seconds
+ private static final int END_CALL_TIMER = 3; // in seconds
+ private static final int KEEPALIVE_TIMEOUT = 5; // in seconds
+ private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds
+ private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
+
+ private static final EventObject DEREGISTER = new EventObject("Deregister");
+ private static final EventObject END_CALL = new EventObject("End call");
+
+ private final SipProfile mLocalProfile;
+ private final String mPassword;
+
+ private SipStack mSipStack;
+ private SipHelper mSipHelper;
+
+ // session that processes INVITE requests
+ private SipSessionImpl mCallReceiverSession;
+ private String mLocalIp;
+
+ private SipWakeupTimer mWakeupTimer;
+ private SipWakeLock mWakeLock;
+
+ // call-id-to-SipSession map
+ private Map<String, SipSessionImpl> mSessionMap =
+ new HashMap<String, SipSessionImpl>();
+
+ // external address observed from any response
+ private String mExternalIp;
+ private int mExternalPort;
+
+ /**
+ * @param profile the local profile with password crossed out
+ * @param password the password of the profile
+ * @throws SipException if cannot assign requested address
+ */
+ public SipSessionGroup(SipProfile profile, String password,
+ SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException {
+ mLocalProfile = profile;
+ mPassword = password;
+ mWakeupTimer = timer;
+ mWakeLock = wakeLock;
+ reset();
+ }
+
+ // TODO: remove this method once SipWakeupTimer can better handle variety
+ // of timeout values
+ void setWakeupTimer(SipWakeupTimer timer) {
+ mWakeupTimer = timer;
+ }
+
+ synchronized void reset() throws SipException {
+ Properties properties = new Properties();
+
+ String protocol = mLocalProfile.getProtocol();
+ int port = mLocalProfile.getPort();
+ String server = mLocalProfile.getProxyAddress();
+
+ if (!TextUtils.isEmpty(server)) {
+ properties.setProperty("javax.sip.OUTBOUND_PROXY",
+ server + ':' + port + '/' + protocol);
+ } else {
+ server = mLocalProfile.getSipDomain();
+ }
+ if (server.startsWith("[") && server.endsWith("]")) {
+ server = server.substring(1, server.length() - 1);
+ }
+
+ String local = null;
+ try {
+ for (InetAddress remote : InetAddress.getAllByName(server)) {
+ DatagramSocket socket = new DatagramSocket();
+ socket.connect(remote, port);
+ if (socket.isConnected()) {
+ local = socket.getLocalAddress().getHostAddress();
+ port = socket.getLocalPort();
+ socket.close();
+ break;
+ }
+ socket.close();
+ }
+ } catch (Exception e) {
+ // ignore.
+ }
+ if (local == null) {
+ // We are unable to reach the server. Just bail out.
+ return;
+ }
+
+ close();
+ mLocalIp = local;
+
+ properties.setProperty("javax.sip.STACK_NAME", getStackName());
+ properties.setProperty(
+ "gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE);
+ mSipStack = SipFactory.getInstance().createSipStack(properties);
+ try {
+ SipProvider provider = mSipStack.createSipProvider(
+ mSipStack.createListeningPoint(local, port, protocol));
+ provider.addSipListener(this);
+ mSipHelper = new SipHelper(mSipStack, provider);
+ } catch (SipException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SipException("failed to initialize SIP stack", e);
+ }
+
+ if (DBG) log("reset: start stack for " + mLocalProfile.getUriString());
+ mSipStack.start();
+ }
+
+ synchronized void onConnectivityChanged() {
+ SipSessionImpl[] ss = mSessionMap.values().toArray(
+ new SipSessionImpl[mSessionMap.size()]);
+ // Iterate on the copied array instead of directly on mSessionMap to
+ // avoid ConcurrentModificationException being thrown when
+ // SipSessionImpl removes itself from mSessionMap in onError() in the
+ // following loop.
+ for (SipSessionImpl s : ss) {
+ s.onError(SipErrorCode.DATA_CONNECTION_LOST,
+ "data connection lost");
+ }
+ }
+
+ synchronized void resetExternalAddress() {
+ if (DBG) {
+ log("resetExternalAddress: " + mSipStack);
+ }
+ mExternalIp = null;
+ mExternalPort = 0;
+ }
+
+ public SipProfile getLocalProfile() {
+ return mLocalProfile;
+ }
+
+ public String getLocalProfileUri() {
+ return mLocalProfile.getUriString();
+ }
+
+ private String getStackName() {
+ return "stack" + System.currentTimeMillis();
+ }
+
+ public synchronized void close() {
+ if (DBG) log("close: " + mLocalProfile.getUriString());
+ onConnectivityChanged();
+ mSessionMap.clear();
+ closeToNotReceiveCalls();
+ if (mSipStack != null) {
+ mSipStack.stop();
+ mSipStack = null;
+ mSipHelper = null;
+ }
+ resetExternalAddress();
+ }
+
+ public synchronized boolean isClosed() {
+ return (mSipStack == null);
+ }
+
+ // For internal use, require listener not to block in callbacks.
+ public synchronized void openToReceiveCalls(ISipSessionListener listener) {
+ if (mCallReceiverSession == null) {
+ mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
+ } else {
+ mCallReceiverSession.setListener(listener);
+ }
+ }
+
+ public synchronized void closeToNotReceiveCalls() {
+ mCallReceiverSession = null;
+ }
+
+ public ISipSession createSession(ISipSessionListener listener) {
+ return (isClosed() ? null : new SipSessionImpl(listener));
+ }
+
+ synchronized boolean containsSession(String callId) {
+ return mSessionMap.containsKey(callId);
+ }
+
+ private synchronized SipSessionImpl getSipSession(EventObject event) {
+ String key = SipHelper.getCallId(event);
+ SipSessionImpl session = mSessionMap.get(key);
+ if ((session != null) && isLoggable(session)) {
+ if (DBG) log("getSipSession: event=" + key);
+ if (DBG) log("getSipSession: active sessions:");
+ for (String k : mSessionMap.keySet()) {
+ if (DBG) log("getSipSession: ..." + k + ": " + mSessionMap.get(k));
+ }
+ }
+ return ((session != null) ? session : mCallReceiverSession);
+ }
+
+ private synchronized void addSipSession(SipSessionImpl newSession) {
+ removeSipSession(newSession);
+ String key = newSession.getCallId();
+ mSessionMap.put(key, newSession);
+ if (isLoggable(newSession)) {
+ if (DBG) log("addSipSession: key='" + key + "'");
+ for (String k : mSessionMap.keySet()) {
+ if (DBG) log("addSipSession: " + k + ": " + mSessionMap.get(k));
+ }
+ }
+ }
+
+ private synchronized void removeSipSession(SipSessionImpl session) {
+ if (session == mCallReceiverSession) return;
+ String key = session.getCallId();
+ SipSessionImpl s = mSessionMap.remove(key);
+ // sanity check
+ if ((s != null) && (s != session)) {
+ if (DBG) log("removeSession: " + session + " is not associated with key '"
+ + key + "'");
+ mSessionMap.put(key, s);
+ for (Map.Entry<String, SipSessionImpl> entry
+ : mSessionMap.entrySet()) {
+ if (entry.getValue() == s) {
+ key = entry.getKey();
+ mSessionMap.remove(key);
+ }
+ }
+ }
+
+ if ((s != null) && isLoggable(s)) {
+ if (DBG) log("removeSession: " + session + " @key '" + key + "'");
+ for (String k : mSessionMap.keySet()) {
+ if (DBG) log("removeSession: " + k + ": " + mSessionMap.get(k));
+ }
+ }
+ }
+
+ @Override
+ public void processRequest(final RequestEvent event) {
+ if (isRequestEvent(Request.INVITE, event)) {
+ if (DBG) log("processRequest: mWakeLock.acquire got INVITE, thread:"
+ + Thread.currentThread());
+ // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME;
+ // should be large enough to bring up the app.
+ mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME);
+ }
+ process(event);
+ }
+
+ @Override
+ public void processResponse(ResponseEvent event) {
+ process(event);
+ }
+
+ @Override
+ public void processIOException(IOExceptionEvent event) {
+ process(event);
+ }
+
+ @Override
+ public void processTimeout(TimeoutEvent event) {
+ process(event);
+ }
+
+ @Override
+ public void processTransactionTerminated(TransactionTerminatedEvent event) {
+ process(event);
+ }
+
+ @Override
+ public void processDialogTerminated(DialogTerminatedEvent event) {
+ process(event);
+ }
+
+ private synchronized void process(EventObject event) {
+ SipSessionImpl session = getSipSession(event);
+ try {
+ boolean isLoggable = isLoggable(session, event);
+ boolean processed = (session != null) && session.process(event);
+ if (isLoggable && processed) {
+ log("process: event new state after: "
+ + SipSession.State.toString(session.mState));
+ }
+ } catch (Throwable e) {
+ loge("process: error event=" + event, getRootCause(e));
+ session.onError(e);
+ }
+ }
+
+ private String extractContent(Message message) {
+ // Currently we do not support secure MIME bodies.
+ byte[] bytes = message.getRawContent();
+ if (bytes != null) {
+ try {
+ if (message instanceof SIPMessage) {
+ return ((SIPMessage) message).getMessageContent();
+ } else {
+ return new String(bytes, "UTF-8");
+ }
+ } catch (UnsupportedEncodingException e) {
+ }
+ }
+ return null;
+ }
+
+ private void extractExternalAddress(ResponseEvent evt) {
+ Response response = evt.getResponse();
+ ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+ SIPHeaderNames.VIA));
+ if (viaHeader == null) return;
+ int rport = viaHeader.getRPort();
+ String externalIp = viaHeader.getReceived();
+ if ((rport > 0) && (externalIp != null)) {
+ mExternalIp = externalIp;
+ mExternalPort = rport;
+ if (DBG) {
+ log("extractExternalAddress: external addr " + externalIp + ":" + rport
+ + " on " + mSipStack);
+ }
+ }
+ }
+
+ private Throwable getRootCause(Throwable exception) {
+ Throwable cause = exception.getCause();
+ while (cause != null) {
+ exception = cause;
+ cause = exception.getCause();
+ }
+ return exception;
+ }
+
+ private SipSessionImpl createNewSession(RequestEvent event,
+ ISipSessionListener listener, ServerTransaction transaction,
+ int newState) throws SipException {
+ SipSessionImpl newSession = new SipSessionImpl(listener);
+ newSession.mServerTransaction = transaction;
+ newSession.mState = newState;
+ newSession.mDialog = newSession.mServerTransaction.getDialog();
+ newSession.mInviteReceived = event;
+ newSession.mPeerProfile = createPeerProfile((HeaderAddress)
+ event.getRequest().getHeader(FromHeader.NAME));
+ newSession.mPeerSessionDescription =
+ extractContent(event.getRequest());
+ return newSession;
+ }
+
+ private class SipSessionCallReceiverImpl extends SipSessionImpl {
+ private static final String SSCRI_TAG = "SipSessionCallReceiverImpl";
+ private static final boolean SSCRI_DBG = true;
+
+ public SipSessionCallReceiverImpl(ISipSessionListener listener) {
+ super(listener);
+ }
+
+ private int processInviteWithReplaces(RequestEvent event,
+ ReplacesHeader replaces) {
+ String callId = replaces.getCallId();
+ SipSessionImpl session = mSessionMap.get(callId);
+ if (session == null) {
+ return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+ }
+
+ Dialog dialog = session.mDialog;
+ if (dialog == null) return Response.DECLINE;
+
+ if (!dialog.getLocalTag().equals(replaces.getToTag()) ||
+ !dialog.getRemoteTag().equals(replaces.getFromTag())) {
+ // No match is found, returns 481.
+ return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+ }
+
+ ReferredByHeader referredBy = (ReferredByHeader) event.getRequest()
+ .getHeader(ReferredByHeader.NAME);
+ if ((referredBy == null) ||
+ !dialog.getRemoteParty().equals(referredBy.getAddress())) {
+ return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+ }
+ return Response.OK;
+ }
+
+ private void processNewInviteRequest(RequestEvent event)
+ throws SipException {
+ ReplacesHeader replaces = (ReplacesHeader) event.getRequest()
+ .getHeader(ReplacesHeader.NAME);
+ SipSessionImpl newSession = null;
+ if (replaces != null) {
+ int response = processInviteWithReplaces(event, replaces);
+ if (SSCRI_DBG) {
+ log("processNewInviteRequest: " + replaces
+ + " response=" + response);
+ }
+ if (response == Response.OK) {
+ SipSessionImpl replacedSession =
+ mSessionMap.get(replaces.getCallId());
+ // got INVITE w/ replaces request.
+ newSession = createNewSession(event,
+ replacedSession.mProxy.getListener(),
+ mSipHelper.getServerTransaction(event),
+ SipSession.State.INCOMING_CALL);
+ newSession.mProxy.onCallTransferring(newSession,
+ newSession.mPeerSessionDescription);
+ } else {
+ mSipHelper.sendResponse(event, response);
+ }
+ } else {
+ // New Incoming call.
+ newSession = createNewSession(event, mProxy,
+ mSipHelper.sendRinging(event, generateTag()),
+ SipSession.State.INCOMING_CALL);
+ mProxy.onRinging(newSession, newSession.mPeerProfile,
+ newSession.mPeerSessionDescription);
+ }
+ if (newSession != null) addSipSession(newSession);
+ }
+
+ @Override
+ public boolean process(EventObject evt) throws SipException {
+ if (isLoggable(this, evt)) log("process: " + this + ": "
+ + SipSession.State.toString(mState) + ": processing "
+ + logEvt(evt));
+ if (isRequestEvent(Request.INVITE, evt)) {
+ processNewInviteRequest((RequestEvent) evt);
+ return true;
+ } else if (isRequestEvent(Request.OPTIONS, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(SSCRI_TAG, s);
+ }
+ }
+
+ static interface KeepAliveProcessCallback {
+ /** Invoked when the response of keeping alive comes back. */
+ void onResponse(boolean portChanged);
+ void onError(int errorCode, String description);
+ }
+
+ class SipSessionImpl extends ISipSession.Stub {
+ private static final String SSI_TAG = "SipSessionImpl";
+ private static final boolean SSI_DBG = true;
+
+ SipProfile mPeerProfile;
+ SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
+ int mState = SipSession.State.READY_TO_CALL;
+ RequestEvent mInviteReceived;
+ Dialog mDialog;
+ ServerTransaction mServerTransaction;
+ ClientTransaction mClientTransaction;
+ String mPeerSessionDescription;
+ boolean mInCall;
+ SessionTimer mSessionTimer;
+ int mAuthenticationRetryCount;
+
+ private SipKeepAlive mSipKeepAlive;
+
+ private SipSessionImpl mSipSessionImpl;
+
+ // the following three members are used for handling refer request.
+ SipSessionImpl mReferSession;
+ ReferredByHeader mReferredBy;
+ String mReplaces;
+
+ // lightweight timer
+ class SessionTimer {
+ private boolean mRunning = true;
+
+ void start(final int timeout) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ sleep(timeout);
+ if (mRunning) timeout();
+ }
+ }, "SipSessionTimerThread").start();
+ }
+
+ synchronized void cancel() {
+ mRunning = false;
+ this.notify();
+ }
+
+ private void timeout() {
+ synchronized (SipSessionGroup.this) {
+ onError(SipErrorCode.TIME_OUT, "Session timed out!");
+ }
+ }
+
+ private synchronized void sleep(int timeout) {
+ try {
+ this.wait(timeout * 1000);
+ } catch (InterruptedException e) {
+ loge("session timer interrupted!", e);
+ }
+ }
+ }
+
+ public SipSessionImpl(ISipSessionListener listener) {
+ setListener(listener);
+ }
+
+ SipSessionImpl duplicate() {
+ return new SipSessionImpl(mProxy.getListener());
+ }
+
+ private void reset() {
+ mInCall = false;
+ removeSipSession(this);
+ mPeerProfile = null;
+ mState = SipSession.State.READY_TO_CALL;
+ mInviteReceived = null;
+ mPeerSessionDescription = null;
+ mAuthenticationRetryCount = 0;
+ mReferSession = null;
+ mReferredBy = null;
+ mReplaces = null;
+
+ if (mDialog != null) mDialog.delete();
+ mDialog = null;
+
+ try {
+ if (mServerTransaction != null) mServerTransaction.terminate();
+ } catch (ObjectInUseException e) {
+ // ignored
+ }
+ mServerTransaction = null;
+
+ try {
+ if (mClientTransaction != null) mClientTransaction.terminate();
+ } catch (ObjectInUseException e) {
+ // ignored
+ }
+ mClientTransaction = null;
+
+ cancelSessionTimer();
+
+ if (mSipSessionImpl != null) {
+ mSipSessionImpl.stopKeepAliveProcess();
+ mSipSessionImpl = null;
+ }
+ }
+
+ @Override
+ public boolean isInCall() {
+ return mInCall;
+ }
+
+ @Override
+ public String getLocalIp() {
+ return mLocalIp;
+ }
+
+ @Override
+ public SipProfile getLocalProfile() {
+ return mLocalProfile;
+ }
+
+ @Override
+ public SipProfile getPeerProfile() {
+ return mPeerProfile;
+ }
+
+ @Override
+ public String getCallId() {
+ return SipHelper.getCallId(getTransaction());
+ }
+
+ private Transaction getTransaction() {
+ if (mClientTransaction != null) return mClientTransaction;
+ if (mServerTransaction != null) return mServerTransaction;
+ return null;
+ }
+
+ @Override
+ public int getState() {
+ return mState;
+ }
+
+ @Override
+ public void setListener(ISipSessionListener listener) {
+ mProxy.setListener((listener instanceof SipSessionListenerProxy)
+ ? ((SipSessionListenerProxy) listener).getListener()
+ : listener);
+ }
+
+ // process the command in a new thread
+ private void doCommandAsync(final EventObject command) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ processCommand(command);
+ } catch (Throwable e) {
+ loge("command error: " + command + ": "
+ + mLocalProfile.getUriString(),
+ getRootCause(e));
+ onError(e);
+ }
+ }
+ }, "SipSessionAsyncCmdThread").start();
+ }
+
+ @Override
+ public void makeCall(SipProfile peerProfile, String sessionDescription,
+ int timeout) {
+ doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
+ timeout));
+ }
+
+ @Override
+ public void answerCall(String sessionDescription, int timeout) {
+ synchronized (SipSessionGroup.this) {
+ if (mPeerProfile == null) return;
+ doCommandAsync(new MakeCallCommand(mPeerProfile,
+ sessionDescription, timeout));
+ }
+ }
+
+ @Override
+ public void endCall() {
+ doCommandAsync(END_CALL);
+ }
+
+ @Override
+ public void changeCall(String sessionDescription, int timeout) {
+ synchronized (SipSessionGroup.this) {
+ if (mPeerProfile == null) return;
+ doCommandAsync(new MakeCallCommand(mPeerProfile,
+ sessionDescription, timeout));
+ }
+ }
+
+ @Override
+ public void register(int duration) {
+ doCommandAsync(new RegisterCommand(duration));
+ }
+
+ @Override
+ public void unregister() {
+ doCommandAsync(DEREGISTER);
+ }
+
+ private void processCommand(EventObject command) throws SipException {
+ if (isLoggable(command)) log("process cmd: " + command);
+ if (!process(command)) {
+ onError(SipErrorCode.IN_PROGRESS,
+ "cannot initiate a new transaction to execute: "
+ + command);
+ }
+ }
+
+ protected String generateTag() {
+ // 32-bit randomness
+ return String.valueOf((long) (Math.random() * 0x100000000L));
+ }
+
+ @Override
+ public String toString() {
+ try {
+ String s = super.toString();
+ return s.substring(s.indexOf("@")) + ":"
+ + SipSession.State.toString(mState);
+ } catch (Throwable e) {
+ return super.toString();
+ }
+ }
+
+ public boolean process(EventObject evt) throws SipException {
+ if (isLoggable(this, evt)) log(" ~~~~~ " + this + ": "
+ + SipSession.State.toString(mState) + ": processing "
+ + logEvt(evt));
+ synchronized (SipSessionGroup.this) {
+ if (isClosed()) return false;
+
+ if (mSipKeepAlive != null) {
+ // event consumed by keepalive process
+ if (mSipKeepAlive.process(evt)) return true;
+ }
+
+ Dialog dialog = null;
+ if (evt instanceof RequestEvent) {
+ dialog = ((RequestEvent) evt).getDialog();
+ } else if (evt instanceof ResponseEvent) {
+ dialog = ((ResponseEvent) evt).getDialog();
+ extractExternalAddress((ResponseEvent) evt);
+ }
+ if (dialog != null) mDialog = dialog;
+
+ boolean processed;
+
+ switch (mState) {
+ case SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ processed = registeringToReady(evt);
+ break;
+ case SipSession.State.READY_TO_CALL:
+ processed = readyForCall(evt);
+ break;
+ case SipSession.State.INCOMING_CALL:
+ processed = incomingCall(evt);
+ break;
+ case SipSession.State.INCOMING_CALL_ANSWERING:
+ processed = incomingCallToInCall(evt);
+ break;
+ case SipSession.State.OUTGOING_CALL:
+ case SipSession.State.OUTGOING_CALL_RING_BACK:
+ processed = outgoingCall(evt);
+ break;
+ case SipSession.State.OUTGOING_CALL_CANCELING:
+ processed = outgoingCallToReady(evt);
+ break;
+ case SipSession.State.IN_CALL:
+ processed = inCall(evt);
+ break;
+ case SipSession.State.ENDING_CALL:
+ processed = endingCall(evt);
+ break;
+ default:
+ processed = false;
+ }
+ return (processed || processExceptions(evt));
+ }
+ }
+
+ private boolean processExceptions(EventObject evt) throws SipException {
+ if (isRequestEvent(Request.BYE, evt)) {
+ // terminate the call whenever a BYE is received
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ endCallNormally();
+ return true;
+ } else if (isRequestEvent(Request.CANCEL, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt,
+ Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
+ return true;
+ } else if (evt instanceof TransactionTerminatedEvent) {
+ if (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
+ if (evt instanceof TimeoutEvent) {
+ processTimeout((TimeoutEvent) evt);
+ } else {
+ processTransactionTerminated(
+ (TransactionTerminatedEvent) evt);
+ }
+ return true;
+ }
+ } else if (isRequestEvent(Request.OPTIONS, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ return true;
+ } else if (evt instanceof DialogTerminatedEvent) {
+ processDialogTerminated((DialogTerminatedEvent) evt);
+ return true;
+ }
+ return false;
+ }
+
+ private void processDialogTerminated(DialogTerminatedEvent event) {
+ if (mDialog == event.getDialog()) {
+ onError(new SipException("dialog terminated"));
+ } else {
+ if (SSI_DBG) log("not the current dialog; current=" + mDialog
+ + ", terminated=" + event.getDialog());
+ }
+ }
+
+ private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
+ Transaction current = event.isServerTransaction()
+ ? mServerTransaction
+ : mClientTransaction;
+ Transaction target = event.isServerTransaction()
+ ? event.getServerTransaction()
+ : event.getClientTransaction();
+
+ if ((current != target) && (mState != SipSession.State.PINGING)) {
+ if (SSI_DBG) log("not the current transaction; current="
+ + toString(current) + ", target=" + toString(target));
+ return false;
+ } else if (current != null) {
+ if (SSI_DBG) log("transaction terminated: " + toString(current));
+ return true;
+ } else {
+ // no transaction; shouldn't be here; ignored
+ return true;
+ }
+ }
+
+ private String toString(Transaction transaction) {
+ if (transaction == null) return "null";
+ Request request = transaction.getRequest();
+ Dialog dialog = transaction.getDialog();
+ CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
+ return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
+ cseq.getSeqNumber(), transaction.getState(),
+ ((dialog == null) ? "-" : dialog.getState()));
+ }
+
+ private void processTransactionTerminated(
+ TransactionTerminatedEvent event) {
+ switch (mState) {
+ case SipSession.State.IN_CALL:
+ case SipSession.State.READY_TO_CALL:
+ if (SSI_DBG) log("Transaction terminated; do nothing");
+ break;
+ default:
+ if (SSI_DBG) log("Transaction terminated early: " + this);
+ onError(SipErrorCode.TRANSACTION_TERMINTED,
+ "transaction terminated");
+ }
+ }
+
+ private void processTimeout(TimeoutEvent event) {
+ if (SSI_DBG) log("processing Timeout...");
+ switch (mState) {
+ case SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ reset();
+ mProxy.onRegistrationTimeout(this);
+ break;
+ case SipSession.State.INCOMING_CALL:
+ case SipSession.State.INCOMING_CALL_ANSWERING:
+ case SipSession.State.OUTGOING_CALL:
+ case SipSession.State.OUTGOING_CALL_CANCELING:
+ onError(SipErrorCode.TIME_OUT, event.toString());
+ break;
+
+ default:
+ if (SSI_DBG) log(" do nothing");
+ break;
+ }
+ }
+
+ private int getExpiryTime(Response response) {
+ int time = -1;
+ ContactHeader contact = (ContactHeader) response.getHeader(ContactHeader.NAME);
+ if (contact != null) {
+ time = contact.getExpires();
+ }
+ ExpiresHeader expires = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME);
+ if (expires != null && (time < 0 || time > expires.getExpires())) {
+ time = expires.getExpires();
+ }
+ if (time <= 0) {
+ time = EXPIRY_TIME;
+ }
+ expires = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME);
+ if (expires != null && time < expires.getExpires()) {
+ time = expires.getExpires();
+ }
+ if (SSI_DBG) {
+ log("Expiry time = " + time);
+ }
+ return time;
+ }
+
+ private boolean registeringToReady(EventObject evt)
+ throws SipException {
+ if (expectResponse(Request.REGISTER, evt)) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+
+ int statusCode = response.getStatusCode();
+ switch (statusCode) {
+ case Response.OK:
+ int state = mState;
+ onRegistrationDone((state == SipSession.State.REGISTERING)
+ ? getExpiryTime(((ResponseEvent) evt).getResponse())
+ : -1);
+ return true;
+ case Response.UNAUTHORIZED:
+ case Response.PROXY_AUTHENTICATION_REQUIRED:
+ handleAuthentication(event);
+ return true;
+ default:
+ if (statusCode >= 500) {
+ onRegistrationFailed(response);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean handleAuthentication(ResponseEvent event)
+ throws SipException {
+ Response response = event.getResponse();
+ String nonce = getNonceFromResponse(response);
+ if (nonce == null) {
+ onError(SipErrorCode.SERVER_ERROR,
+ "server does not provide challenge");
+ return false;
+ } else if (mAuthenticationRetryCount < 2) {
+ mClientTransaction = mSipHelper.handleChallenge(
+ event, getAccountManager());
+ mDialog = mClientTransaction.getDialog();
+ mAuthenticationRetryCount++;
+ if (isLoggable(this, event)) {
+ if (SSI_DBG) log(" authentication retry count="
+ + mAuthenticationRetryCount);
+ }
+ return true;
+ } else {
+ if (crossDomainAuthenticationRequired(response)) {
+ onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
+ getRealmFromResponse(response));
+ } else {
+ onError(SipErrorCode.INVALID_CREDENTIALS,
+ "incorrect username or password");
+ }
+ return false;
+ }
+ }
+
+ private boolean crossDomainAuthenticationRequired(Response response) {
+ String realm = getRealmFromResponse(response);
+ if (realm == null) realm = "";
+ return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
+ }
+
+ private AccountManager getAccountManager() {
+ return new AccountManager() {
+ @Override
+ public UserCredentials getCredentials(ClientTransaction
+ challengedTransaction, String realm) {
+ return new UserCredentials() {
+ @Override
+ public String getUserName() {
+ String username = mLocalProfile.getAuthUserName();
+ return (!TextUtils.isEmpty(username) ? username :
+ mLocalProfile.getUserName());
+ }
+
+ @Override
+ public String getPassword() {
+ return mPassword;
+ }
+
+ @Override
+ public String getSipDomain() {
+ return mLocalProfile.getSipDomain();
+ }
+ };
+ }
+ };
+ }
+
+ private String getRealmFromResponse(Response response) {
+ WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
+ SIPHeaderNames.WWW_AUTHENTICATE);
+ if (wwwAuth != null) return wwwAuth.getRealm();
+ ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
+ SIPHeaderNames.PROXY_AUTHENTICATE);
+ return (proxyAuth == null) ? null : proxyAuth.getRealm();
+ }
+
+ private String getNonceFromResponse(Response response) {
+ WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
+ SIPHeaderNames.WWW_AUTHENTICATE);
+ if (wwwAuth != null) return wwwAuth.getNonce();
+ ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
+ SIPHeaderNames.PROXY_AUTHENTICATE);
+ return (proxyAuth == null) ? null : proxyAuth.getNonce();
+ }
+
+ private String getResponseString(int statusCode) {
+ StatusLine statusLine = new StatusLine();
+ statusLine.setStatusCode(statusCode);
+ statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode));
+ return statusLine.encode();
+ }
+
+ private boolean readyForCall(EventObject evt) throws SipException {
+ // expect MakeCallCommand, RegisterCommand, DEREGISTER
+ if (evt instanceof MakeCallCommand) {
+ mState = SipSession.State.OUTGOING_CALL;
+ MakeCallCommand cmd = (MakeCallCommand) evt;
+ mPeerProfile = cmd.getPeerProfile();
+ if (mReferSession != null) {
+ mSipHelper.sendReferNotify(mReferSession.mDialog,
+ getResponseString(Response.TRYING));
+ }
+ mClientTransaction = mSipHelper.sendInvite(
+ mLocalProfile, mPeerProfile, cmd.getSessionDescription(),
+ generateTag(), mReferredBy, mReplaces);
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ startSessionTimer(cmd.getTimeout());
+ mProxy.onCalling(this);
+ return true;
+ } else if (evt instanceof RegisterCommand) {
+ mState = SipSession.State.REGISTERING;
+ int duration = ((RegisterCommand) evt).getDuration();
+ mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+ generateTag(), duration);
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ mProxy.onRegistering(this);
+ return true;
+ } else if (DEREGISTER == evt) {
+ mState = SipSession.State.DEREGISTERING;
+ mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+ generateTag(), 0);
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ mProxy.onRegistering(this);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean incomingCall(EventObject evt) throws SipException {
+ // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
+ if (evt instanceof MakeCallCommand) {
+ // answer call
+ mState = SipSession.State.INCOMING_CALL_ANSWERING;
+ mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
+ mLocalProfile,
+ ((MakeCallCommand) evt).getSessionDescription(),
+ mServerTransaction,
+ mExternalIp, mExternalPort);
+ startSessionTimer(((MakeCallCommand) evt).getTimeout());
+ return true;
+ } else if (END_CALL == evt) {
+ mSipHelper.sendInviteBusyHere(mInviteReceived,
+ mServerTransaction);
+ endCallNormally();
+ return true;
+ } else if (isRequestEvent(Request.CANCEL, evt)) {
+ RequestEvent event = (RequestEvent) evt;
+ mSipHelper.sendResponse(event, Response.OK);
+ mSipHelper.sendInviteRequestTerminated(
+ mInviteReceived.getRequest(), mServerTransaction);
+ endCallNormally();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean incomingCallToInCall(EventObject evt) {
+ // expect ACK, CANCEL request
+ if (isRequestEvent(Request.ACK, evt)) {
+ String sdp = extractContent(((RequestEvent) evt).getRequest());
+ if (sdp != null) mPeerSessionDescription = sdp;
+ if (mPeerSessionDescription == null) {
+ onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty");
+ } else {
+ establishCall(false);
+ }
+ return true;
+ } else if (isRequestEvent(Request.CANCEL, evt)) {
+ // http://tools.ietf.org/html/rfc3261#section-9.2
+ // Final response has been sent; do nothing here.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean outgoingCall(EventObject evt) throws SipException {
+ if (expectResponse(Request.INVITE, evt)) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+
+ int statusCode = response.getStatusCode();
+ switch (statusCode) {
+ case Response.RINGING:
+ case Response.CALL_IS_BEING_FORWARDED:
+ case Response.QUEUED:
+ case Response.SESSION_PROGRESS:
+ // feedback any provisional responses (except TRYING) as
+ // ring back for better UX
+ if (mState == SipSession.State.OUTGOING_CALL) {
+ mState = SipSession.State.OUTGOING_CALL_RING_BACK;
+ cancelSessionTimer();
+ mProxy.onRingingBack(this);
+ }
+ return true;
+ case Response.OK:
+ if (mReferSession != null) {
+ mSipHelper.sendReferNotify(mReferSession.mDialog,
+ getResponseString(Response.OK));
+ // since we don't need to remember the session anymore.
+ mReferSession = null;
+ }
+ mSipHelper.sendInviteAck(event, mDialog);
+ mPeerSessionDescription = extractContent(response);
+ establishCall(true);
+ return true;
+ case Response.UNAUTHORIZED:
+ case Response.PROXY_AUTHENTICATION_REQUIRED:
+ if (handleAuthentication(event)) {
+ addSipSession(this);
+ }
+ return true;
+ case Response.REQUEST_PENDING:
+ // TODO: rfc3261#section-14.1; re-schedule invite
+ return true;
+ default:
+ if (mReferSession != null) {
+ mSipHelper.sendReferNotify(mReferSession.mDialog,
+ getResponseString(Response.SERVICE_UNAVAILABLE));
+ }
+ if (statusCode >= 400) {
+ // error: an ack is sent automatically by the stack
+ onError(response);
+ return true;
+ } else if (statusCode >= 300) {
+ // TODO: handle 3xx (redirect)
+ } else {
+ return true;
+ }
+ }
+ return false;
+ } else if (END_CALL == evt) {
+ // RFC says that UA should not send out cancel when no
+ // response comes back yet. We are cheating for not checking
+ // response.
+ mState = SipSession.State.OUTGOING_CALL_CANCELING;
+ mSipHelper.sendCancel(mClientTransaction);
+ startSessionTimer(CANCEL_CALL_TIMER);
+ return true;
+ } else if (isRequestEvent(Request.INVITE, evt)) {
+ // Call self? Send BUSY HERE so server may redirect the call to
+ // voice mailbox.
+ RequestEvent event = (RequestEvent) evt;
+ mSipHelper.sendInviteBusyHere(event,
+ event.getServerTransaction());
+ return true;
+ }
+ return false;
+ }
+
+ private boolean outgoingCallToReady(EventObject evt)
+ throws SipException {
+ if (evt instanceof ResponseEvent) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+ int statusCode = response.getStatusCode();
+ if (expectResponse(Request.CANCEL, evt)) {
+ if (statusCode == Response.OK) {
+ // do nothing; wait for REQUEST_TERMINATED
+ return true;
+ }
+ } else if (expectResponse(Request.INVITE, evt)) {
+ switch (statusCode) {
+ case Response.OK:
+ outgoingCall(evt); // abort Cancel
+ return true;
+ case Response.REQUEST_TERMINATED:
+ endCallNormally();
+ return true;
+ }
+ } else {
+ return false;
+ }
+
+ if (statusCode >= 400) {
+ onError(response);
+ return true;
+ }
+ } else if (evt instanceof TransactionTerminatedEvent) {
+ // rfc3261#section-14.1:
+ // if re-invite gets timed out, terminate the dialog; but
+ // re-invite is not reliable, just let it go and pretend
+ // nothing happened.
+ onError(new SipException("timed out"));
+ }
+ return false;
+ }
+
+ private boolean processReferRequest(RequestEvent event)
+ throws SipException {
+ try {
+ ReferToHeader referto = (ReferToHeader) event.getRequest()
+ .getHeader(ReferTo.NAME);
+ Address address = referto.getAddress();
+ SipURI uri = (SipURI) address.getURI();
+ String replacesHeader = uri.getHeader(ReplacesHeader.NAME);
+ String username = uri.getUser();
+ if (username == null) {
+ mSipHelper.sendResponse(event, Response.BAD_REQUEST);
+ return false;
+ }
+ // send notify accepted
+ mSipHelper.sendResponse(event, Response.ACCEPTED);
+ SipSessionImpl newSession = createNewSession(event,
+ this.mProxy.getListener(),
+ mSipHelper.getServerTransaction(event),
+ SipSession.State.READY_TO_CALL);
+ newSession.mReferSession = this;
+ newSession.mReferredBy = (ReferredByHeader) event.getRequest()
+ .getHeader(ReferredByHeader.NAME);
+ newSession.mReplaces = replacesHeader;
+ newSession.mPeerProfile = createPeerProfile(referto);
+ newSession.mProxy.onCallTransferring(newSession,
+ null);
+ return true;
+ } catch (IllegalArgumentException e) {
+ throw new SipException("createPeerProfile()", e);
+ }
+ }
+
+ private boolean inCall(EventObject evt) throws SipException {
+ // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
+ // OK retransmission is handled in SipStack
+ if (END_CALL == evt) {
+ // rfc3261#section-15.1.1
+ mState = SipSession.State.ENDING_CALL;
+ mSipHelper.sendBye(mDialog);
+ mProxy.onCallEnded(this);
+ startSessionTimer(END_CALL_TIMER);
+ return true;
+ } else if (isRequestEvent(Request.INVITE, evt)) {
+ // got Re-INVITE
+ mState = SipSession.State.INCOMING_CALL;
+ RequestEvent event = mInviteReceived = (RequestEvent) evt;
+ mPeerSessionDescription = extractContent(event.getRequest());
+ mServerTransaction = null;
+ mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
+ return true;
+ } else if (isRequestEvent(Request.BYE, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ endCallNormally();
+ return true;
+ } else if (isRequestEvent(Request.REFER, evt)) {
+ return processReferRequest((RequestEvent) evt);
+ } else if (evt instanceof MakeCallCommand) {
+ // to change call
+ mState = SipSession.State.OUTGOING_CALL;
+ mClientTransaction = mSipHelper.sendReinvite(mDialog,
+ ((MakeCallCommand) evt).getSessionDescription());
+ startSessionTimer(((MakeCallCommand) evt).getTimeout());
+ return true;
+ } else if (evt instanceof ResponseEvent) {
+ if (expectResponse(Request.NOTIFY, evt)) return true;
+ }
+ return false;
+ }
+
+ private boolean endingCall(EventObject evt) throws SipException {
+ if (expectResponse(Request.BYE, evt)) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+
+ int statusCode = response.getStatusCode();
+ switch (statusCode) {
+ case Response.UNAUTHORIZED:
+ case Response.PROXY_AUTHENTICATION_REQUIRED:
+ if (handleAuthentication(event)) {
+ return true;
+ } else {
+ // can't authenticate; pass through to end session
+ }
+ }
+ cancelSessionTimer();
+ reset();
+ return true;
+ }
+ return false;
+ }
+
+ // timeout in seconds
+ private void startSessionTimer(int timeout) {
+ if (timeout > 0) {
+ mSessionTimer = new SessionTimer();
+ mSessionTimer.start(timeout);
+ }
+ }
+
+ private void cancelSessionTimer() {
+ if (mSessionTimer != null) {
+ mSessionTimer.cancel();
+ mSessionTimer = null;
+ }
+ }
+
+ private String createErrorMessage(Response response) {
+ return String.format("%s (%d)", response.getReasonPhrase(),
+ response.getStatusCode());
+ }
+
+ private void enableKeepAlive() {
+ if (mSipSessionImpl != null) {
+ mSipSessionImpl.stopKeepAliveProcess();
+ } else {
+ mSipSessionImpl = duplicate();
+ }
+ try {
+ mSipSessionImpl.startKeepAliveProcess(
+ INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null);
+ } catch (SipException e) {
+ loge("keepalive cannot be enabled; ignored", e);
+ mSipSessionImpl.stopKeepAliveProcess();
+ }
+ }
+
+ private void establishCall(boolean enableKeepAlive) {
+ mState = SipSession.State.IN_CALL;
+ cancelSessionTimer();
+ if (!mInCall && enableKeepAlive) enableKeepAlive();
+ mInCall = true;
+ mProxy.onCallEstablished(this, mPeerSessionDescription);
+ }
+
+ private void endCallNormally() {
+ reset();
+ mProxy.onCallEnded(this);
+ }
+
+ private void endCallOnError(int errorCode, String message) {
+ reset();
+ mProxy.onError(this, errorCode, message);
+ }
+
+ private void endCallOnBusy() {
+ reset();
+ mProxy.onCallBusy(this);
+ }
+
+ private void onError(int errorCode, String message) {
+ cancelSessionTimer();
+ switch (mState) {
+ case SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ onRegistrationFailed(errorCode, message);
+ break;
+ default:
+ endCallOnError(errorCode, message);
+ }
+ }
+
+
+ private void onError(Throwable exception) {
+ exception = getRootCause(exception);
+ onError(getErrorCode(exception), exception.toString());
+ }
+
+ private void onError(Response response) {
+ int statusCode = response.getStatusCode();
+ if (!mInCall && (statusCode == Response.BUSY_HERE)) {
+ endCallOnBusy();
+ } else {
+ onError(getErrorCode(statusCode), createErrorMessage(response));
+ }
+ }
+
+ private int getErrorCode(int responseStatusCode) {
+ switch (responseStatusCode) {
+ case Response.TEMPORARILY_UNAVAILABLE:
+ case Response.FORBIDDEN:
+ case Response.GONE:
+ case Response.NOT_FOUND:
+ case Response.NOT_ACCEPTABLE:
+ case Response.NOT_ACCEPTABLE_HERE:
+ return SipErrorCode.PEER_NOT_REACHABLE;
+
+ case Response.REQUEST_URI_TOO_LONG:
+ case Response.ADDRESS_INCOMPLETE:
+ case Response.AMBIGUOUS:
+ return SipErrorCode.INVALID_REMOTE_URI;
+
+ case Response.REQUEST_TIMEOUT:
+ return SipErrorCode.TIME_OUT;
+
+ default:
+ if (responseStatusCode < 500) {
+ return SipErrorCode.CLIENT_ERROR;
+ } else {
+ return SipErrorCode.SERVER_ERROR;
+ }
+ }
+ }
+
+ private int getErrorCode(Throwable exception) {
+ String message = exception.getMessage();
+ if (exception instanceof UnknownHostException) {
+ return SipErrorCode.SERVER_UNREACHABLE;
+ } else if (exception instanceof IOException) {
+ return SipErrorCode.SOCKET_ERROR;
+ } else {
+ return SipErrorCode.CLIENT_ERROR;
+ }
+ }
+
+ private void onRegistrationDone(int duration) {
+ reset();
+ mProxy.onRegistrationDone(this, duration);
+ }
+
+ private void onRegistrationFailed(int errorCode, String message) {
+ reset();
+ mProxy.onRegistrationFailed(this, errorCode, message);
+ }
+
+ private void onRegistrationFailed(Response response) {
+ int statusCode = response.getStatusCode();
+ onRegistrationFailed(getErrorCode(statusCode),
+ createErrorMessage(response));
+ }
+
+ // Notes: SipSessionListener will be replaced by the keepalive process
+ // @param interval in seconds
+ public void startKeepAliveProcess(int interval,
+ KeepAliveProcessCallback callback) throws SipException {
+ synchronized (SipSessionGroup.this) {
+ startKeepAliveProcess(interval, mLocalProfile, callback);
+ }
+ }
+
+ // Notes: SipSessionListener will be replaced by the keepalive process
+ // @param interval in seconds
+ public void startKeepAliveProcess(int interval, SipProfile peerProfile,
+ KeepAliveProcessCallback callback) throws SipException {
+ synchronized (SipSessionGroup.this) {
+ if (mSipKeepAlive != null) {
+ throw new SipException("Cannot create more than one "
+ + "keepalive process in a SipSession");
+ }
+ mPeerProfile = peerProfile;
+ mSipKeepAlive = new SipKeepAlive();
+ mProxy.setListener(mSipKeepAlive);
+ mSipKeepAlive.start(interval, callback);
+ }
+ }
+
+ public void stopKeepAliveProcess() {
+ synchronized (SipSessionGroup.this) {
+ if (mSipKeepAlive != null) {
+ mSipKeepAlive.stop();
+ mSipKeepAlive = null;
+ }
+ }
+ }
+
+ class SipKeepAlive extends SipSessionAdapter implements Runnable {
+ private static final String SKA_TAG = "SipKeepAlive";
+ private static final boolean SKA_DBG = true;
+
+ private boolean mRunning = false;
+ private KeepAliveProcessCallback mCallback;
+
+ private boolean mPortChanged = false;
+ private int mRPort = 0;
+ private int mInterval; // just for debugging
+
+ // @param interval in seconds
+ void start(int interval, KeepAliveProcessCallback callback) {
+ if (mRunning) return;
+ mRunning = true;
+ mInterval = interval;
+ mCallback = new KeepAliveProcessCallbackProxy(callback);
+ mWakeupTimer.set(interval * 1000, this);
+ if (SKA_DBG) {
+ log("start keepalive:"
+ + mLocalProfile.getUriString());
+ }
+
+ // No need to run the first time in a separate thread for now
+ run();
+ }
+
+ // return true if the event is consumed
+ boolean process(EventObject evt) {
+ if (mRunning && (mState == SipSession.State.PINGING)) {
+ if (evt instanceof ResponseEvent) {
+ if (parseOptionsResult(evt)) {
+ if (mPortChanged) {
+ resetExternalAddress();
+ stop();
+ } else {
+ cancelSessionTimer();
+ removeSipSession(SipSessionImpl.this);
+ }
+ mCallback.onResponse(mPortChanged);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // SipSessionAdapter
+ // To react to the session timeout event and network error.
+ @Override
+ public void onError(ISipSession session, int errorCode, String message) {
+ stop();
+ mCallback.onError(errorCode, message);
+ }
+
+ // SipWakeupTimer timeout handler
+ // To send out keepalive message.
+ @Override
+ public void run() {
+ synchronized (SipSessionGroup.this) {
+ if (!mRunning) return;
+
+ if (DBG_PING) {
+ String peerUri = (mPeerProfile == null)
+ ? "null"
+ : mPeerProfile.getUriString();
+ log("keepalive: " + mLocalProfile.getUriString()
+ + " --> " + peerUri + ", interval=" + mInterval);
+ }
+ try {
+ sendKeepAlive();
+ } catch (Throwable t) {
+ if (SKA_DBG) {
+ loge("keepalive error: "
+ + mLocalProfile.getUriString(), getRootCause(t));
+ }
+ // It's possible that the keepalive process is being stopped
+ // during session.sendKeepAlive() so need to check mRunning
+ // again here.
+ if (mRunning) SipSessionImpl.this.onError(t);
+ }
+ }
+ }
+
+ void stop() {
+ synchronized (SipSessionGroup.this) {
+ if (SKA_DBG) {
+ log("stop keepalive:" + mLocalProfile.getUriString()
+ + ",RPort=" + mRPort);
+ }
+ mRunning = false;
+ mWakeupTimer.cancel(this);
+ reset();
+ }
+ }
+
+ private void sendKeepAlive() throws SipException {
+ synchronized (SipSessionGroup.this) {
+ mState = SipSession.State.PINGING;
+ mClientTransaction = mSipHelper.sendOptions(
+ mLocalProfile, mPeerProfile, generateTag());
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(SipSessionImpl.this);
+
+ startSessionTimer(KEEPALIVE_TIMEOUT);
+ // when timed out, onError() will be called with SipErrorCode.TIME_OUT
+ }
+ }
+
+ private boolean parseOptionsResult(EventObject evt) {
+ if (expectResponse(Request.OPTIONS, evt)) {
+ ResponseEvent event = (ResponseEvent) evt;
+ int rPort = getRPortFromResponse(event.getResponse());
+ if (rPort != -1) {
+ if (mRPort == 0) mRPort = rPort;
+ if (mRPort != rPort) {
+ mPortChanged = true;
+ if (SKA_DBG) log(String.format(
+ "rport is changed: %d <> %d", mRPort, rPort));
+ mRPort = rPort;
+ } else {
+ if (SKA_DBG) log("rport is the same: " + rPort);
+ }
+ } else {
+ if (SKA_DBG) log("peer did not respond rport");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private int getRPortFromResponse(Response response) {
+ ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+ SIPHeaderNames.VIA));
+ return (viaHeader == null) ? -1 : viaHeader.getRPort();
+ }
+
+ private void log(String s) {
+ Rlog.d(SKA_TAG, s);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(SSI_TAG, s);
+ }
+ }
+
+ /**
+ * @return true if the event is a request event matching the specified
+ * method; false otherwise
+ */
+ private static boolean isRequestEvent(String method, EventObject event) {
+ try {
+ if (event instanceof RequestEvent) {
+ RequestEvent requestEvent = (RequestEvent) event;
+ return method.equals(requestEvent.getRequest().getMethod());
+ }
+ } catch (Throwable e) {
+ }
+ return false;
+ }
+
+ private static String getCseqMethod(Message message) {
+ return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
+ }
+
+ /**
+ * @return true if the event is a response event and the CSeqHeader method
+ * match the given arguments; false otherwise
+ */
+ private static boolean expectResponse(
+ String expectedMethod, EventObject evt) {
+ if (evt instanceof ResponseEvent) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+ return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
+ }
+ return false;
+ }
+
+ private static SipProfile createPeerProfile(HeaderAddress header)
+ throws SipException {
+ try {
+ Address address = header.getAddress();
+ SipURI uri = (SipURI) address.getURI();
+ String username = uri.getUser();
+ if (username == null) username = ANONYMOUS;
+ int port = uri.getPort();
+ SipProfile.Builder builder =
+ new SipProfile.Builder(username, uri.getHost())
+ .setDisplayName(address.getDisplayName());
+ if (port > 0) builder.setPort(port);
+ return builder.build();
+ } catch (IllegalArgumentException e) {
+ throw new SipException("createPeerProfile()", e);
+ } catch (ParseException e) {
+ throw new SipException("createPeerProfile()", e);
+ }
+ }
+
+ private static boolean isLoggable(SipSessionImpl s) {
+ if (s != null) {
+ switch (s.mState) {
+ case SipSession.State.PINGING:
+ return DBG_PING;
+ }
+ }
+ return DBG;
+ }
+
+ private static boolean isLoggable(EventObject evt) {
+ return isLoggable(null, evt);
+ }
+
+ private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
+ if (!isLoggable(s)) return false;
+ if (evt == null) return false;
+
+ if (evt instanceof ResponseEvent) {
+ Response response = ((ResponseEvent) evt).getResponse();
+ if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
+ return DBG_PING;
+ }
+ return DBG;
+ } else if (evt instanceof RequestEvent) {
+ if (isRequestEvent(Request.OPTIONS, evt)) {
+ return DBG_PING;
+ }
+ return DBG;
+ }
+ return false;
+ }
+
+ private static String logEvt(EventObject evt) {
+ if (evt instanceof RequestEvent) {
+ return ((RequestEvent) evt).getRequest().toString();
+ } else if (evt instanceof ResponseEvent) {
+ return ((ResponseEvent) evt).getResponse().toString();
+ } else {
+ return evt.toString();
+ }
+ }
+
+ private class RegisterCommand extends EventObject {
+ private int mDuration;
+
+ public RegisterCommand(int duration) {
+ super(SipSessionGroup.this);
+ mDuration = duration;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+ }
+
+ private class MakeCallCommand extends EventObject {
+ private String mSessionDescription;
+ private int mTimeout; // in seconds
+
+ public MakeCallCommand(SipProfile peerProfile,
+ String sessionDescription, int timeout) {
+ super(peerProfile);
+ mSessionDescription = sessionDescription;
+ mTimeout = timeout;
+ }
+
+ public SipProfile getPeerProfile() {
+ return (SipProfile) getSource();
+ }
+
+ public String getSessionDescription() {
+ return mSessionDescription;
+ }
+
+ public int getTimeout() {
+ return mTimeout;
+ }
+ }
+
+ /** Class to help safely run KeepAliveProcessCallback in a different thread. */
+ static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback {
+ private static final String KAPCP_TAG = "KeepAliveProcessCallbackProxy";
+ private KeepAliveProcessCallback mCallback;
+
+ KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) {
+ mCallback = callback;
+ }
+
+ private void proxy(Runnable runnable) {
+ // One thread for each calling back.
+ // Note: Guarantee ordering if the issue becomes important. Currently,
+ // the chance of handling two callback events at a time is none.
+ new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start();
+ }
+
+ @Override
+ public void onResponse(final boolean portChanged) {
+ if (mCallback == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mCallback.onResponse(portChanged);
+ } catch (Throwable t) {
+ loge("onResponse", t);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(final int errorCode, final String description) {
+ if (mCallback == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mCallback.onError(errorCode, description);
+ } catch (Throwable t) {
+ loge("onError", t);
+ }
+ }
+ });
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(KAPCP_TAG, s, t);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(TAG, s, t);
+ }
+}
diff --git a/java/com/android/server/sip/SipSessionListenerProxy.java b/java/com/android/server/sip/SipSessionListenerProxy.java
new file mode 100644
index 0000000..7a4ae8d
--- /dev/null
+++ b/java/com/android/server/sip/SipSessionListenerProxy.java
@@ -0,0 +1,265 @@
+/*
+ * 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 com.android.server.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipProfile;
+import android.os.DeadObjectException;
+import android.telephony.Rlog;
+
+/** Class to help safely run a callback in a different thread. */
+class SipSessionListenerProxy extends ISipSessionListener.Stub {
+ private static final String TAG = "SipSessionListnerProxy";
+
+ private ISipSessionListener mListener;
+
+ public void setListener(ISipSessionListener listener) {
+ mListener = listener;
+ }
+
+ public ISipSessionListener getListener() {
+ return mListener;
+ }
+
+ private void proxy(Runnable runnable) {
+ // One thread for each calling back.
+ // Note: Guarantee ordering if the issue becomes important. Currently,
+ // the chance of handling two callback events at a time is none.
+ new Thread(runnable, "SipSessionCallbackThread").start();
+ }
+
+ @Override
+ public void onCalling(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCalling(session);
+ } catch (Throwable t) {
+ handle(t, "onCalling()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRinging(final ISipSession session, final SipProfile caller,
+ final String sessionDescription) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRinging(session, caller, sessionDescription);
+ } catch (Throwable t) {
+ handle(t, "onRinging()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRingingBack(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRingingBack(session);
+ } catch (Throwable t) {
+ handle(t, "onRingingBack()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onCallEstablished(final ISipSession session,
+ final String sessionDescription) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCallEstablished(session, sessionDescription);
+ } catch (Throwable t) {
+ handle(t, "onCallEstablished()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onCallEnded(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCallEnded(session);
+ } catch (Throwable t) {
+ handle(t, "onCallEnded()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onCallTransferring(final ISipSession newSession,
+ final String sessionDescription) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCallTransferring(newSession, sessionDescription);
+ } catch (Throwable t) {
+ handle(t, "onCallTransferring()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onCallBusy(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCallBusy(session);
+ } catch (Throwable t) {
+ handle(t, "onCallBusy()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onCallChangeFailed(final ISipSession session,
+ final int errorCode, final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onCallChangeFailed(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onCallChangeFailed()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onError(final ISipSession session, final int errorCode,
+ final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onError(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onError()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRegistering(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRegistering(session);
+ } catch (Throwable t) {
+ handle(t, "onRegistering()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRegistrationDone(final ISipSession session,
+ final int duration) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRegistrationDone(session, duration);
+ } catch (Throwable t) {
+ handle(t, "onRegistrationDone()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRegistrationFailed(final ISipSession session,
+ final int errorCode, final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRegistrationFailed(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onRegistrationFailed()");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRegistrationTimeout(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mListener.onRegistrationTimeout(session);
+ } catch (Throwable t) {
+ handle(t, "onRegistrationTimeout()");
+ }
+ }
+ });
+ }
+
+ private void handle(Throwable t, String message) {
+ if (t instanceof DeadObjectException) {
+ mListener = null;
+ // This creates race but it's harmless. Just don't log the error
+ // when it happens.
+ } else if (mListener != null) {
+ loge(message, t);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(TAG, s, t);
+ }
+}
diff --git a/java/com/android/server/sip/SipWakeLock.java b/java/com/android/server/sip/SipWakeLock.java
new file mode 100644
index 0000000..b3fbb56
--- /dev/null
+++ b/java/com/android/server/sip/SipWakeLock.java
@@ -0,0 +1,73 @@
+/*
+ * 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 com.android.server.sip;
+
+import android.os.PowerManager;
+import android.telephony.Rlog;
+
+import java.util.HashSet;
+
+class SipWakeLock {
+ private static final String TAG = "SipWakeLock";
+ private static final boolean DBG = false;
+ private PowerManager mPowerManager;
+ private PowerManager.WakeLock mWakeLock;
+ private PowerManager.WakeLock mTimerWakeLock;
+ private HashSet<Object> mHolders = new HashSet<Object>();
+
+ SipWakeLock(PowerManager powerManager) {
+ mPowerManager = powerManager;
+ }
+
+ synchronized void reset() {
+ if (DBG) log("reset count=" + mHolders.size());
+ mHolders.clear();
+ release(null);
+ }
+
+ synchronized void acquire(long timeout) {
+ if (mTimerWakeLock == null) {
+ mTimerWakeLock = mPowerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, "SipWakeLock.timer");
+ mTimerWakeLock.setReferenceCounted(true);
+ }
+ mTimerWakeLock.acquire(timeout);
+ }
+
+ synchronized void acquire(Object holder) {
+ mHolders.add(holder);
+ if (mWakeLock == null) {
+ mWakeLock = mPowerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, "SipWakeLock");
+ }
+ if (!mWakeLock.isHeld()) mWakeLock.acquire();
+ if (DBG) log("acquire count=" + mHolders.size());
+ }
+
+ synchronized void release(Object holder) {
+ mHolders.remove(holder);
+ if ((mWakeLock != null) && mHolders.isEmpty()
+ && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ if (DBG) log("release count=" + mHolders.size());
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+}
diff --git a/java/com/android/server/sip/SipWakeupTimer.java b/java/com/android/server/sip/SipWakeupTimer.java
new file mode 100644
index 0000000..3ba4331
--- /dev/null
+++ b/java/com/android/server/sip/SipWakeupTimer.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2011, 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 com.android.server.sip;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+import android.telephony.Rlog;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.TreeSet;
+import java.util.concurrent.Executor;
+
+/**
+ * Timer that can schedule events to occur even when the device is in sleep.
+ */
+class SipWakeupTimer extends BroadcastReceiver {
+ private static final String TAG = "SipWakeupTimer";
+ private static final boolean DBG = SipService.DBG && true; // STOPSHIP if true
+ private static final String TRIGGER_TIME = "TriggerTime";
+
+ private Context mContext;
+ private AlarmManager mAlarmManager;
+
+ // runnable --> time to execute in SystemClock
+ private TreeSet<MyEvent> mEventQueue =
+ new TreeSet<MyEvent>(new MyEventComparator());
+
+ private PendingIntent mPendingIntent;
+
+ private Executor mExecutor;
+
+ public SipWakeupTimer(Context context, Executor executor) {
+ mContext = context;
+ mAlarmManager = (AlarmManager)
+ context.getSystemService(Context.ALARM_SERVICE);
+
+ IntentFilter filter = new IntentFilter(getAction());
+ context.registerReceiver(this, filter);
+ mExecutor = executor;
+ }
+
+ /**
+ * Stops the timer. No event can be scheduled after this method is called.
+ */
+ public synchronized void stop() {
+ mContext.unregisterReceiver(this);
+ if (mPendingIntent != null) {
+ mAlarmManager.cancel(mPendingIntent);
+ mPendingIntent = null;
+ }
+ mEventQueue.clear();
+ mEventQueue = null;
+ }
+
+ private boolean stopped() {
+ if (mEventQueue == null) {
+ if (DBG) log("Timer stopped");
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void cancelAlarm() {
+ mAlarmManager.cancel(mPendingIntent);
+ mPendingIntent = null;
+ }
+
+ private void recalculatePeriods() {
+ if (mEventQueue.isEmpty()) return;
+
+ MyEvent firstEvent = mEventQueue.first();
+ int minPeriod = firstEvent.mMaxPeriod;
+ long minTriggerTime = firstEvent.mTriggerTime;
+ for (MyEvent e : mEventQueue) {
+ e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
+ int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
+ - minTriggerTime);
+ interval = interval / minPeriod * minPeriod;
+ e.mTriggerTime = minTriggerTime + interval;
+ }
+ TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
+ mEventQueue.comparator());
+ newQueue.addAll(mEventQueue);
+ mEventQueue.clear();
+ mEventQueue = newQueue;
+ if (DBG) {
+ log("queue re-calculated");
+ printQueue();
+ }
+ }
+
+ // Determines the period and the trigger time of the new event and insert it
+ // to the queue.
+ private void insertEvent(MyEvent event) {
+ long now = SystemClock.elapsedRealtime();
+ if (mEventQueue.isEmpty()) {
+ event.mTriggerTime = now + event.mPeriod;
+ mEventQueue.add(event);
+ return;
+ }
+ MyEvent firstEvent = mEventQueue.first();
+ int minPeriod = firstEvent.mPeriod;
+ if (minPeriod <= event.mMaxPeriod) {
+ event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
+ int interval = event.mMaxPeriod;
+ interval -= (int) (firstEvent.mTriggerTime - now);
+ interval = interval / minPeriod * minPeriod;
+ event.mTriggerTime = firstEvent.mTriggerTime + interval;
+ mEventQueue.add(event);
+ } else {
+ long triggerTime = now + event.mPeriod;
+ if (firstEvent.mTriggerTime < triggerTime) {
+ event.mTriggerTime = firstEvent.mTriggerTime;
+ event.mLastTriggerTime -= event.mPeriod;
+ } else {
+ event.mTriggerTime = triggerTime;
+ }
+ mEventQueue.add(event);
+ recalculatePeriods();
+ }
+ }
+
+ /**
+ * Sets a periodic timer.
+ *
+ * @param period the timer period; in milli-second
+ * @param callback is called back when the timer goes off; the same callback
+ * can be specified in multiple timer events
+ */
+ public synchronized void set(int period, Runnable callback) {
+ if (stopped()) return;
+
+ long now = SystemClock.elapsedRealtime();
+ MyEvent event = new MyEvent(period, callback, now);
+ insertEvent(event);
+
+ if (mEventQueue.first() == event) {
+ if (mEventQueue.size() > 1) cancelAlarm();
+ scheduleNext();
+ }
+
+ long triggerTime = event.mTriggerTime;
+ if (DBG) {
+ log("set: add event " + event + " scheduled on "
+ + showTime(triggerTime) + " at " + showTime(now)
+ + ", #events=" + mEventQueue.size());
+ printQueue();
+ }
+ }
+
+ /**
+ * Cancels all the timer events with the specified callback.
+ *
+ * @param callback the callback
+ */
+ public synchronized void cancel(Runnable callback) {
+ if (stopped() || mEventQueue.isEmpty()) return;
+ if (DBG) log("cancel:" + callback);
+
+ MyEvent firstEvent = mEventQueue.first();
+ for (Iterator<MyEvent> iter = mEventQueue.iterator();
+ iter.hasNext();) {
+ MyEvent event = iter.next();
+ if (event.mCallback == callback) {
+ iter.remove();
+ if (DBG) log(" cancel found:" + event);
+ }
+ }
+ if (mEventQueue.isEmpty()) {
+ cancelAlarm();
+ } else if (mEventQueue.first() != firstEvent) {
+ cancelAlarm();
+ firstEvent = mEventQueue.first();
+ firstEvent.mPeriod = firstEvent.mMaxPeriod;
+ firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
+ + firstEvent.mPeriod;
+ recalculatePeriods();
+ scheduleNext();
+ }
+ if (DBG) {
+ log("cancel: X");
+ printQueue();
+ }
+ }
+
+ private void scheduleNext() {
+ if (stopped() || mEventQueue.isEmpty()) return;
+
+ if (mPendingIntent != null) {
+ throw new RuntimeException("pendingIntent is not null!");
+ }
+
+ MyEvent event = mEventQueue.first();
+ Intent intent = new Intent(getAction());
+ intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
+ PendingIntent pendingIntent = mPendingIntent =
+ PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ event.mTriggerTime, pendingIntent);
+ }
+
+ @Override
+ public synchronized void onReceive(Context context, Intent intent) {
+ // This callback is already protected by AlarmManager's wake lock.
+ String action = intent.getAction();
+ if (getAction().equals(action)
+ && intent.getExtras().containsKey(TRIGGER_TIME)) {
+ mPendingIntent = null;
+ long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
+ execute(triggerTime);
+ } else {
+ log("onReceive: unrecognized intent: " + intent);
+ }
+ }
+
+ private void printQueue() {
+ int count = 0;
+ for (MyEvent event : mEventQueue) {
+ log(" " + event + ": scheduled at "
+ + showTime(event.mTriggerTime) + ": last at "
+ + showTime(event.mLastTriggerTime));
+ if (++count >= 5) break;
+ }
+ if (mEventQueue.size() > count) {
+ log(" .....");
+ } else if (count == 0) {
+ log(" <empty>");
+ }
+ }
+
+ private void execute(long triggerTime) {
+ if (DBG) log("time's up, triggerTime = "
+ + showTime(triggerTime) + ": " + mEventQueue.size());
+ if (stopped() || mEventQueue.isEmpty()) return;
+
+ for (MyEvent event : mEventQueue) {
+ if (event.mTriggerTime != triggerTime) continue;
+ if (DBG) log("execute " + event);
+
+ event.mLastTriggerTime = triggerTime;
+ event.mTriggerTime += event.mPeriod;
+
+ // run the callback in the handler thread to prevent deadlock
+ mExecutor.execute(event.mCallback);
+ }
+ if (DBG) {
+ log("after timeout execution");
+ printQueue();
+ }
+ scheduleNext();
+ }
+
+ private String getAction() {
+ return toString();
+ }
+
+ private String showTime(long time) {
+ int ms = (int) (time % 1000);
+ int s = (int) (time / 1000);
+ int m = s / 60;
+ s %= 60;
+ return String.format("%d.%d.%d", m, s, ms);
+ }
+
+ private static class MyEvent {
+ int mPeriod;
+ int mMaxPeriod;
+ long mTriggerTime;
+ long mLastTriggerTime;
+ Runnable mCallback;
+
+ MyEvent(int period, Runnable callback, long now) {
+ mPeriod = mMaxPeriod = period;
+ mCallback = callback;
+ mLastTriggerTime = now;
+ }
+
+ @Override
+ public String toString() {
+ String s = super.toString();
+ s = s.substring(s.indexOf("@"));
+ return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
+ + toString(mCallback);
+ }
+
+ private String toString(Object o) {
+ String s = o.toString();
+ int index = s.indexOf("$");
+ if (index > 0) s = s.substring(index + 1);
+ return s;
+ }
+ }
+
+ // Sort the events by mMaxPeriod so that the first event can be used to
+ // align events with larger periods
+ private static class MyEventComparator implements Comparator<MyEvent> {
+ @Override
+ public int compare(MyEvent e1, MyEvent e2) {
+ if (e1 == e2) return 0;
+ int diff = e1.mMaxPeriod - e2.mMaxPeriod;
+ if (diff == 0) diff = -1;
+ return diff;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ return (this == that);
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+}
diff --git a/jni/rtp/AmrCodec.cpp b/jni/rtp/AmrCodec.cpp
new file mode 100644
index 0000000..e2d820e
--- /dev/null
+++ b/jni/rtp/AmrCodec.cpp
@@ -0,0 +1,272 @@
+/*
+ * 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"
+
+#include "gsmamr_dec.h"
+#include "gsmamr_enc.h"
+
+namespace {
+
+const int gFrameBits[8] = {95, 103, 118, 134, 148, 159, 204, 244};
+
+//------------------------------------------------------------------------------
+
+// See RFC 4867 for the encoding details.
+
+class AmrCodec : public AudioCodec
+{
+public:
+ AmrCodec() {
+ if (AMREncodeInit(&mEncoder, &mSidSync, false)) {
+ mEncoder = NULL;
+ }
+ if (GSMInitDecode(&mDecoder, (Word8 *)"RTP")) {
+ mDecoder = NULL;
+ }
+ }
+
+ ~AmrCodec() {
+ if (mEncoder) {
+ AMREncodeExit(&mEncoder, &mSidSync);
+ }
+ if (mDecoder) {
+ GSMDecodeFrameExit(&mDecoder);
+ }
+ }
+
+ int set(int sampleRate, const char *fmtp);
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, int count, void *payload, int length);
+
+private:
+ void *mEncoder;
+ void *mSidSync;
+ void *mDecoder;
+
+ int mMode;
+ int mModeSet;
+ bool mOctetAligned;
+};
+
+int AmrCodec::set(int sampleRate, const char *fmtp)
+{
+ // These parameters are not supported.
+ if (strcasestr(fmtp, "crc=1") || strcasestr(fmtp, "robust-sorting=1") ||
+ strcasestr(fmtp, "interleaving=")) {
+ return -1;
+ }
+
+ // Handle mode-set and octet-align.
+ const char *modes = strcasestr(fmtp, "mode-set=");
+ if (modes) {
+ mMode = 0;
+ mModeSet = 0;
+ for (char c = *modes; c && c != ' '; c = *++modes) {
+ if (c >= '0' && c <= '7') {
+ int mode = c - '0';
+ if (mode > mMode) {
+ mMode = mode;
+ }
+ mModeSet |= 1 << mode;
+ }
+ }
+ } else {
+ mMode = 7;
+ mModeSet = 0xFF;
+ }
+ mOctetAligned = (strcasestr(fmtp, "octet-align=1") != NULL);
+
+ // TODO: handle mode-change-*.
+
+ return (sampleRate == 8000 && mEncoder && mDecoder) ? 160 : -1;
+}
+
+int AmrCodec::encode(void *payload, int16_t *samples)
+{
+ unsigned char *bytes = (unsigned char *)payload;
+ Frame_Type_3GPP type;
+
+ int length = AMREncode(mEncoder, mSidSync, (Mode)mMode,
+ samples, bytes + 1, &type, AMR_TX_WMF);
+
+ if (type != mMode || length != (8 + gFrameBits[mMode] + 7) >> 3) {
+ return -1;
+ }
+
+ if (mOctetAligned) {
+ bytes[0] = 0xF0;
+ bytes[1] = (mMode << 3) | 0x04;
+ ++length;
+ } else {
+ // CMR = 15 (4-bit), F = 0 (1-bit), FT = mMode (4-bit), Q = 1 (1-bit).
+ bytes[0] = 0xFF;
+ bytes[1] = 0xC0 | (mMode << 1) | 1;
+
+ // Shift left 6 bits and update the length.
+ bytes[length + 1] = 0;
+ for (int i = 0; i <= length; ++i) {
+ bytes[i] = (bytes[i] << 6) | (bytes[i + 1] >> 2);
+ }
+ length = (10 + gFrameBits[mMode] + 7) >> 3;
+ }
+ return length;
+}
+
+int AmrCodec::decode(int16_t *samples, int count, void *payload, int length)
+{
+ unsigned char *bytes = (unsigned char *)payload;
+ Frame_Type_3GPP type;
+ if (length < 2) {
+ return -1;
+ }
+ int request = bytes[0] >> 4;
+
+ if (mOctetAligned) {
+ if ((bytes[1] & 0xC4) != 0x04) {
+ return -1;
+ }
+ type = (Frame_Type_3GPP)(bytes[1] >> 3);
+ if (length != (16 + gFrameBits[type] + 7) >> 3) {
+ return -1;
+ }
+ length -= 2;
+ bytes += 2;
+ } else {
+ if ((bytes[0] & 0x0C) || !(bytes[1] & 0x40)) {
+ return -1;
+ }
+ type = (Frame_Type_3GPP)((bytes[0] << 1 | bytes[1] >> 7) & 0x07);
+ if (length != (10 + gFrameBits[type] + 7) >> 3) {
+ return -1;
+ }
+
+ // Shift left 2 bits and update the length.
+ --length;
+ for (int i = 1; i < length; ++i) {
+ bytes[i] = (bytes[i] << 2) | (bytes[i + 1] >> 6);
+ }
+ bytes[length] <<= 2;
+ length = (gFrameBits[type] + 7) >> 3;
+ ++bytes;
+ }
+
+ if (AMRDecode(mDecoder, type, bytes, samples, MIME_IETF) != length) {
+ return -1;
+ }
+
+ // Handle CMR
+ if (request < 8 && request != mMode) {
+ for (int i = request; i >= 0; --i) {
+ if (mModeSet & (1 << i)) {
+ mMode = request;
+ break;
+ }
+ }
+ }
+
+ return 160;
+}
+
+//------------------------------------------------------------------------------
+
+// See RFC 3551 for the encoding details.
+
+class GsmEfrCodec : public AudioCodec
+{
+public:
+ GsmEfrCodec() {
+ if (AMREncodeInit(&mEncoder, &mSidSync, false)) {
+ mEncoder = NULL;
+ }
+ if (GSMInitDecode(&mDecoder, (Word8 *)"RTP")) {
+ mDecoder = NULL;
+ }
+ }
+
+ ~GsmEfrCodec() {
+ if (mEncoder) {
+ AMREncodeExit(&mEncoder, &mSidSync);
+ }
+ if (mDecoder) {
+ GSMDecodeFrameExit(&mDecoder);
+ }
+ }
+
+ int set(int sampleRate, const char *fmtp) {
+ return (sampleRate == 8000 && mEncoder && mDecoder) ? 160 : -1;
+ }
+
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, int count, void *payload, int length);
+
+private:
+ void *mEncoder;
+ void *mSidSync;
+ void *mDecoder;
+};
+
+int GsmEfrCodec::encode(void *payload, int16_t *samples)
+{
+ unsigned char *bytes = (unsigned char *)payload;
+ Frame_Type_3GPP type;
+
+ int length = AMREncode(mEncoder, mSidSync, MR122,
+ samples, bytes, &type, AMR_TX_WMF);
+
+ if (type == AMR_122 && length == 32) {
+ bytes[0] = 0xC0 | (bytes[1] >> 4);
+ for (int i = 1; i < 31; ++i) {
+ bytes[i] = (bytes[i] << 4) | (bytes[i + 1] >> 4);
+ }
+ return 31;
+ }
+ return -1;
+}
+
+int GsmEfrCodec::decode(int16_t *samples, int count, void *payload, int length)
+{
+ unsigned char *bytes = (unsigned char *)payload;
+ int n = 0;
+ while (n + 160 <= count && length >= 31 && (bytes[0] >> 4) == 0x0C) {
+ for (int i = 0; i < 30; ++i) {
+ bytes[i] = (bytes[i] << 4) | (bytes[i + 1] >> 4);
+ }
+ bytes[30] <<= 4;
+
+ if (AMRDecode(mDecoder, AMR_122, bytes, &samples[n], MIME_IETF) != 31) {
+ break;
+ }
+ n += 160;
+ length -= 31;
+ bytes += 31;
+ }
+ return n;
+}
+
+} // namespace
+
+AudioCodec *newAmrCodec()
+{
+ return new AmrCodec;
+}
+
+AudioCodec *newGsmEfrCodec()
+{
+ return new GsmEfrCodec;
+}
diff --git a/jni/rtp/Android.mk b/jni/rtp/Android.mk
new file mode 100644
index 0000000..b265cdd
--- /dev/null
+++ b/jni/rtp/Android.mk
@@ -0,0 +1,59 @@
+#
+# 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 \
+ EchoSuppressor.cpp \
+ RtpStream.cpp \
+ util.cpp \
+ rtp_jni.cpp
+
+LOCAL_SRC_FILES += \
+ AmrCodec.cpp \
+ G711Codec.cpp \
+ GsmCodec.cpp
+
+LOCAL_SHARED_LIBRARIES := \
+ libnativehelper \
+ libcutils \
+ libutils \
+ libmedia \
+ libstagefright_amrnb_common
+
+LOCAL_STATIC_LIBRARIES := libgsm libstagefright_amrnbdec libstagefright_amrnbenc
+
+LOCAL_C_INCLUDES += \
+ $(JNI_H_INCLUDE) \
+ external/libgsm/inc \
+ frameworks/av/media/libstagefright/codecs/amrnb/common/include \
+ frameworks/av/media/libstagefright/codecs/amrnb/common/ \
+ frameworks/av/media/libstagefright/codecs/amrnb/enc/include \
+ frameworks/av/media/libstagefright/codecs/amrnb/enc/src \
+ frameworks/av/media/libstagefright/codecs/amrnb/dec/include \
+ frameworks/av/media/libstagefright/codecs/amrnb/dec/src \
+ $(call include-path-for, audio-effects)
+
+LOCAL_CFLAGS += -fvisibility=hidden
+
+
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/jni/rtp/AudioCodec.cpp b/jni/rtp/AudioCodec.cpp
new file mode 100644
index 0000000..c75fbc9
--- /dev/null
+++ b/jni/rtp/AudioCodec.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 <strings.h>
+
+#include "AudioCodec.h"
+
+extern AudioCodec *newAlawCodec();
+extern AudioCodec *newUlawCodec();
+extern AudioCodec *newGsmCodec();
+extern AudioCodec *newAmrCodec();
+extern AudioCodec *newGsmEfrCodec();
+
+struct AudioCodecType {
+ const char *name;
+ AudioCodec *(*create)();
+} gAudioCodecTypes[] = {
+ {"PCMA", newAlawCodec},
+ {"PCMU", newUlawCodec},
+ {"GSM", newGsmCodec},
+ {"AMR", newAmrCodec},
+ {"GSM-EFR", newGsmEfrCodec},
+ {NULL, NULL},
+};
+
+AudioCodec *newAudioCodec(const char *codecName)
+{
+ AudioCodecType *type = gAudioCodecTypes;
+ while (type->name != NULL) {
+ if (strcasecmp(codecName, type->name) == 0) {
+ AudioCodec *codec = type->create();
+ codec->name = type->name;
+ return codec;
+ }
+ ++type;
+ }
+ return NULL;
+}
diff --git a/jni/rtp/AudioCodec.h b/jni/rtp/AudioCodec.h
new file mode 100644
index 0000000..741730b
--- /dev/null
+++ b/jni/rtp/AudioCodec.h
@@ -0,0 +1,38 @@
+/*
+ * 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:
+ const char *name;
+ // Needed by destruction through base class pointers.
+ virtual ~AudioCodec() {}
+ // Returns sampleCount or non-positive value if unsupported.
+ virtual int set(int sampleRate, const char *fmtp) = 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, int count, void *payload, int length) = 0;
+};
+
+AudioCodec *newAudioCodec(const char *codecName);
+
+#endif
diff --git a/jni/rtp/AudioGroup.cpp b/jni/rtp/AudioGroup.cpp
new file mode 100644
index 0000000..2f0829e
--- /dev/null
+++ b/jni/rtp/AudioGroup.cpp
@@ -0,0 +1,1073 @@
+/*
+ * 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_NDEBUG 0
+#define LOG_TAG "AudioGroup"
+#include <cutils/atomic.h>
+#include <cutils/properties.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 <media/AudioEffect.h>
+#include <audio_effects/effect_aec.h>
+#include <system/audio.h>
+
+#include "jni.h"
+#include "JNIHelp.h"
+
+#include "AudioCodec.h"
+#include "EchoSuppressor.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 2048ms long and assume packet interval is 50ms or less.
+// The first 100ms is the place where samples get mixed. The rest is the real
+// jitter buffer. For a stream at 8000Hz it takes 32 kilobytes. These numbers
+// are chosen by experiments and each of them can be adjusted as needed.
+
+// Originally a stream does not send packets when it is receive-only or there is
+// nothing to mix. However, this causes some problems with certain firewalls and
+// proxies. A firewall might remove a port mapping when there is no outgoing
+// packet for a preiod of time, and a proxy might wait for incoming packets from
+// both sides before start forwarding. To solve these problems, we send out a
+// silence packet on the stream for every second. It should be good enough to
+// keep the stream alive with relatively low resources.
+
+// 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 only 8000Hz is supported.
+
+#define BUFFER_SIZE 2048
+#define HISTORY_SIZE 100
+#define MEASURE_BASE 100
+#define MEASURE_PERIOD 5000
+#define DTMF_PERIOD 200
+
+class AudioStream
+{
+public:
+ AudioStream();
+ ~AudioStream();
+ bool set(int mode, int socket, sockaddr_storage *remote,
+ AudioCodec *codec, 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;
+ bool mFixRemote;
+
+ int mTick;
+ int mSampleRate;
+ int mSampleCount;
+ int mInterval;
+ int mKeepAlive;
+
+ int16_t *mBuffer;
+ int mBufferMask;
+ int mBufferHead;
+ int mBufferTail;
+ int mLatencyTimer;
+ 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;
+ ALOGD("stream[%d] is dead", mSocket);
+}
+
+bool AudioStream::set(int mode, int socket, sockaddr_storage *remote,
+ AudioCodec *codec, int sampleRate, int sampleCount,
+ int codecType, int dtmfType)
+{
+ if (mode < 0 || mode > LAST_MODE) {
+ return false;
+ }
+ mMode = mode;
+
+ 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 = 8; mBufferMask < mSampleRate; mBufferMask <<= 1);
+ mBufferMask *= BUFFER_SIZE;
+ mBuffer = new int16_t[mBufferMask];
+ --mBufferMask;
+ mBufferHead = 0;
+ mBufferTail = 0;
+ mLatencyTimer = 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 these things when succeeded.
+ mSocket = socket;
+ if (codec) {
+ mRemote = *remote;
+ mCodec = codec;
+
+ // Here we should never get an private address, but some buggy proxy
+ // servers do give us one. To solve this, we replace the address when
+ // the first time we successfully decode an incoming packet.
+ mFixRemote = false;
+ if (remote->ss_family == AF_INET) {
+ unsigned char *address =
+ (unsigned char *)&((sockaddr_in *)remote)->sin_addr;
+ if (address[0] == 10 ||
+ (address[0] == 172 && (address[1] >> 4) == 1) ||
+ (address[0] == 192 && address[1] == 168)) {
+ mFixRemote = true;
+ }
+ }
+ }
+
+ ALOGD("stream[%d] is configured as %s %dkHz %dms mode %d", mSocket,
+ (codec ? codec->name : "RAW"), mSampleRate, mInterval, mMode);
+ 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;
+ ALOGV("stream[%d] skips %d packets", mSocket, skipped);
+ }
+
+ tick = mTick;
+ mTick += mInterval;
+ ++mSequence;
+ mTimestamp += mSampleCount;
+
+ // If there is an ongoing DTMF event, send it now.
+ if (mMode != RECEIVE_ONLY && mDtmfEvent != -1) {
+ int duration = mTimestamp - mDtmfStart;
+ // Make sure duration is reasonable.
+ if (duration >= 0 && duration < mSampleRate * DTMF_PERIOD) {
+ duration += mSampleCount;
+ int32_t buffer[4] = {
+ htonl(mDtmfMagic | mSequence),
+ htonl(mDtmfStart),
+ mSsrc,
+ htonl(mDtmfEvent | duration),
+ };
+ if (duration >= mSampleRate * DTMF_PERIOD) {
+ buffer[3] |= htonl(1 << 23);
+ mDtmfEvent = -1;
+ }
+ sendto(mSocket, buffer, sizeof(buffer), MSG_DONTWAIT,
+ (sockaddr *)&mRemote, sizeof(mRemote));
+ return;
+ }
+ mDtmfEvent = -1;
+ }
+
+ int32_t buffer[mSampleCount + 3];
+ bool data = false;
+ if (mMode != RECEIVE_ONLY) {
+ // Mix all other streams.
+ memset(buffer, 0, sizeof(buffer));
+ while (chain) {
+ if (chain != this) {
+ data |= chain->mix(buffer, tick - mInterval, tick, mSampleRate);
+ }
+ chain = chain->mNext;
+ }
+ }
+
+ int16_t samples[mSampleCount];
+ if (data) {
+ // Saturate into 16 bits.
+ 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;
+ }
+ } else {
+ if ((mTick ^ mKeepAlive) >> 10 == 0) {
+ return;
+ }
+ mKeepAlive = mTick;
+ memset(samples, 0, sizeof(samples));
+
+ if (mMode != RECEIVE_ONLY) {
+ ALOGV("stream[%d] no data", mSocket);
+ }
+ }
+
+ if (!mCodec) {
+ // Special case for device stream.
+ send(mSocket, samples, sizeof(samples), MSG_DONTWAIT);
+ return;
+ }
+
+ // Cook the packet and send it out.
+ buffer[0] = htonl(mCodecMagic | mSequence);
+ buffer[1] = htonl(mTimestamp);
+ buffer[2] = mSsrc;
+ int length = mCodec->encode(&buffer[3], samples);
+ if (length <= 0) {
+ ALOGV("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 + BUFFER_SIZE - mBufferHead) > BUFFER_SIZE * 2) {
+ mBufferHead = tick - HISTORY_SIZE;
+ mBufferTail = mBufferHead;
+ }
+
+ if (tick - mBufferHead > HISTORY_SIZE) {
+ // Throw away outdated samples.
+ mBufferHead = tick - HISTORY_SIZE;
+ if (mBufferTail - mBufferHead < 0) {
+ mBufferTail = mBufferHead;
+ }
+ }
+
+ // Adjust the jitter buffer if the latency keeps larger than the threshold
+ // in the measurement period.
+ int score = mBufferTail - tick - MEASURE_BASE;
+ if (mLatencyScore > score || mLatencyScore <= 0) {
+ mLatencyScore = score;
+ mLatencyTimer = tick;
+ } else if (tick - mLatencyTimer >= MEASURE_PERIOD) {
+ ALOGV("stream[%d] reduces latency of %dms", mSocket, mLatencyScore);
+ mBufferTail -= mLatencyScore;
+ mLatencyScore = -1;
+ }
+
+ int count = (BUFFER_SIZE - (mBufferTail - mBufferHead)) * mSampleRate;
+ if (count < mSampleCount) {
+ // Buffer overflow. Drop the packet.
+ ALOGV("stream[%d] buffer overflow", mSocket);
+ recv(mSocket, &c, 1, MSG_DONTWAIT);
+ return;
+ }
+
+ // Receive the packet and decode it.
+ int16_t samples[count];
+ if (!mCodec) {
+ // Special case for device stream.
+ count = recv(mSocket, samples, sizeof(samples),
+ MSG_TRUNC | MSG_DONTWAIT) >> 1;
+ } else {
+ __attribute__((aligned(4))) uint8_t buffer[2048];
+ sockaddr_storage remote;
+ socklen_t addrlen = sizeof(remote);
+
+ int length = recvfrom(mSocket, buffer, sizeof(buffer),
+ MSG_TRUNC | MSG_DONTWAIT, (sockaddr *)&remote, &addrlen);
+
+ // 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) {
+ ALOGV("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, count, &buffer[offset], length);
+ }
+ if (length > 0 && mFixRemote) {
+ mRemote = remote;
+ mFixRemote = false;
+ }
+ count = length;
+ }
+ if (count <= 0) {
+ ALOGV("stream[%d] decoder error", mSocket);
+ return;
+ }
+
+ if (tick - mBufferTail > 0) {
+ // Buffer underrun. Reset the jitter buffer.
+ ALOGV("stream[%d] buffer underrun", mSocket);
+ if (mBufferTail - mBufferHead <= 0) {
+ mBufferHead = tick + mInterval;
+ mBufferTail = mBufferHead;
+ } else {
+ int tail = (tick + mInterval) * mSampleRate;
+ for (int i = mBufferTail * mSampleRate; i - tail < 0; ++i) {
+ mBuffer[i & mBufferMask] = 0;
+ }
+ mBufferTail = tick + mInterval;
+ }
+ }
+
+ // Append to the jitter buffer.
+ int tail = mBufferTail * mSampleRate;
+ for (int i = 0; i < count; ++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(AudioStream *stream);
+ bool platformHasAec() { return mPlatformHasAec; }
+
+private:
+ enum {
+ ON_HOLD = 0,
+ MUTED = 1,
+ NORMAL = 2,
+ ECHO_SUPPRESSION = 3,
+ LAST_MODE = 3,
+ };
+
+ bool checkPlatformAec();
+
+ AudioStream *mChain;
+ int mEventQueue;
+ volatile int mDtmfEvent;
+
+ int mMode;
+ int mSampleRate;
+ int mSampleCount;
+ int mDeviceSocket;
+ bool mPlatformHasAec;
+
+ class NetworkThread : public Thread
+ {
+ public:
+ NetworkThread(AudioGroup *group) : Thread(false), mGroup(group) {}
+
+ bool start()
+ {
+ if (run("Network", ANDROID_PRIORITY_AUDIO) != NO_ERROR) {
+ ALOGE("cannot start network thread");
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ AudioGroup *mGroup;
+ bool threadLoop();
+ };
+ sp<NetworkThread> mNetworkThread;
+
+ class DeviceThread : public Thread
+ {
+ public:
+ DeviceThread(AudioGroup *group) : Thread(false), mGroup(group) {}
+
+ bool start()
+ {
+ if (run("Device", ANDROID_PRIORITY_AUDIO) != NO_ERROR) {
+ ALOGE("cannot start device thread");
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ AudioGroup *mGroup;
+ bool threadLoop();
+ };
+ sp<DeviceThread> mDeviceThread;
+};
+
+AudioGroup::AudioGroup()
+{
+ mMode = ON_HOLD;
+ mChain = NULL;
+ mEventQueue = -1;
+ mDtmfEvent = -1;
+ mDeviceSocket = -1;
+ mNetworkThread = new NetworkThread(this);
+ mDeviceThread = new DeviceThread(this);
+ mPlatformHasAec = checkPlatformAec();
+}
+
+AudioGroup::~AudioGroup()
+{
+ mNetworkThread->requestExitAndWait();
+ mDeviceThread->requestExitAndWait();
+ close(mEventQueue);
+ close(mDeviceSocket);
+ while (mChain) {
+ AudioStream *next = mChain->mNext;
+ delete mChain;
+ mChain = next;
+ }
+ ALOGD("group[%d] is dead", mDeviceSocket);
+}
+
+bool AudioGroup::set(int sampleRate, int sampleCount)
+{
+ mEventQueue = epoll_create(2);
+ if (mEventQueue == -1) {
+ ALOGE("epoll_create: %s", strerror(errno));
+ return false;
+ }
+
+ mSampleRate = sampleRate;
+ mSampleCount = sampleCount;
+
+ // Create device socket.
+ int pair[2];
+ if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)) {
+ ALOGE("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]);
+ ALOGE("cannot initialize device stream");
+ return false;
+ }
+
+ // Give device socket a reasonable timeout.
+ timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 1000 * sampleCount / sampleRate * 500;
+ if (setsockopt(pair[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) {
+ ALOGE("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)) {
+ ALOGE("epoll_ctl: %s", strerror(errno));
+ return false;
+ }
+
+ // Anything else?
+ ALOGD("stream[%d] joins group[%d]", pair[1], pair[0]);
+ return true;
+}
+
+bool AudioGroup::setMode(int mode)
+{
+ if (mode < 0 || mode > LAST_MODE) {
+ return false;
+ }
+ // FIXME: temporary code to overcome echo and mic gain issues on herring and tuna boards.
+ // Must be modified/removed when the root cause of the issue is fixed in the hardware or
+ // driver
+ char value[PROPERTY_VALUE_MAX];
+ property_get("ro.product.board", value, "");
+ if (mode == NORMAL &&
+ (!strcmp(value, "herring") || !strcmp(value, "tuna"))) {
+ mode = ECHO_SUPPRESSION;
+ }
+ if (mMode == mode) {
+ return true;
+ }
+
+ mDeviceThread->requestExitAndWait();
+ ALOGD("group[%d] switches from mode %d to %d", mDeviceSocket, mMode, mode);
+ mMode = mode;
+ return (mode == ON_HOLD) || mDeviceThread->start();
+}
+
+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)) {
+ ALOGE("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;
+ }
+
+ ALOGD("stream[%d] joins group[%d]", stream->mSocket, mDeviceSocket);
+ return true;
+}
+
+bool AudioGroup::remove(AudioStream *stream)
+{
+ mNetworkThread->requestExitAndWait();
+
+ for (AudioStream *chain = mChain; chain->mNext; chain = chain->mNext) {
+ if (chain->mNext == stream) {
+ if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, stream->mSocket, NULL)) {
+ ALOGE("epoll_ctl: %s", strerror(errno));
+ return false;
+ }
+ chain->mNext = stream->mNext;
+ ALOGD("stream[%d] leaves group[%d]", stream->mSocket, mDeviceSocket);
+ delete stream;
+ break;
+ }
+ }
+
+ // Do not start network thread if there is only one stream.
+ if (!mChain->mNext || !mNetworkThread->start()) {
+ return false;
+ }
+ return true;
+}
+
+bool AudioGroup::NetworkThread::threadLoop()
+{
+ AudioStream *chain = mGroup->mChain;
+ int tick = elapsedRealtime();
+ int deadline = tick + 10;
+ int count = 0;
+
+ for (AudioStream *stream = chain; stream; stream = stream->mNext) {
+ if (tick - stream->mTick >= 0) {
+ stream->encode(tick, chain);
+ }
+ if (deadline - stream->mTick > 0) {
+ deadline = stream->mTick;
+ }
+ ++count;
+ }
+
+ int event = mGroup->mDtmfEvent;
+ if (event != -1) {
+ for (AudioStream *stream = chain; stream; stream = stream->mNext) {
+ stream->sendDtmf(event);
+ }
+ mGroup->mDtmfEvent = -1;
+ }
+
+ deadline -= tick;
+ if (deadline < 1) {
+ deadline = 1;
+ }
+
+ epoll_event events[count];
+ count = epoll_wait(mGroup->mEventQueue, events, count, deadline);
+ if (count == -1) {
+ ALOGE("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::checkPlatformAec()
+{
+ effect_descriptor_t fxDesc;
+ uint32_t numFx;
+
+ if (AudioEffect::queryNumberEffects(&numFx) != NO_ERROR) {
+ return false;
+ }
+ for (uint32_t i = 0; i < numFx; i++) {
+ if (AudioEffect::queryEffect(i, &fxDesc) != NO_ERROR) {
+ continue;
+ }
+ if (memcmp(&fxDesc.type, FX_IID_AEC, sizeof(effect_uuid_t)) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool AudioGroup::DeviceThread::threadLoop()
+{
+ int mode = mGroup->mMode;
+ int sampleRate = mGroup->mSampleRate;
+ int sampleCount = mGroup->mSampleCount;
+ int deviceSocket = mGroup->mDeviceSocket;
+
+ // Find out the frame count for AudioTrack and AudioRecord.
+ size_t output = 0;
+ size_t input = 0;
+ if (AudioTrack::getMinFrameCount(&output, AUDIO_STREAM_VOICE_CALL,
+ sampleRate) != NO_ERROR || output <= 0 ||
+ AudioRecord::getMinFrameCount(&input, sampleRate,
+ AUDIO_FORMAT_PCM_16_BIT, AUDIO_CHANNEL_IN_MONO) != NO_ERROR || input <= 0) {
+ ALOGE("cannot compute frame count");
+ return false;
+ }
+ ALOGD("reported frame count: output %d, input %d", output, input);
+
+ if (output < sampleCount * 2) {
+ output = sampleCount * 2;
+ }
+ if (input < sampleCount * 2) {
+ input = sampleCount * 2;
+ }
+ ALOGD("adjusted frame count: output %d, input %d", output, input);
+
+ // Initialize AudioTrack and AudioRecord.
+ AudioTrack track;
+ AudioRecord record;
+ if (track.set(AUDIO_STREAM_VOICE_CALL, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
+ AUDIO_CHANNEL_OUT_MONO, output) != NO_ERROR ||
+ record.set(AUDIO_SOURCE_VOICE_COMMUNICATION, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
+ AUDIO_CHANNEL_IN_MONO, input) != NO_ERROR) {
+ ALOGE("cannot initialize audio device");
+ return false;
+ }
+ ALOGD("latency: output %d, input %d", track.latency(), record.latency());
+
+ // Give device socket a reasonable buffer size.
+ setsockopt(deviceSocket, SOL_SOCKET, SO_RCVBUF, &output, sizeof(output));
+ setsockopt(deviceSocket, SOL_SOCKET, SO_SNDBUF, &output, sizeof(output));
+
+ // Drain device socket.
+ char c;
+ while (recv(deviceSocket, &c, 1, MSG_DONTWAIT) == 1);
+
+ // check if platform supports echo cancellation and do not active local echo suppression in
+ // this case
+ EchoSuppressor *echo = NULL;
+ AudioEffect *aec = NULL;
+ if (mode == ECHO_SUPPRESSION) {
+ if (mGroup->platformHasAec()) {
+ aec = new AudioEffect(FX_IID_AEC,
+ NULL,
+ 0,
+ 0,
+ 0,
+ record.getSessionId(),
+ record.getInput());
+ status_t status = aec->initCheck();
+ if (status == NO_ERROR || status == ALREADY_EXISTS) {
+ aec->setEnabled(true);
+ } else {
+ delete aec;
+ aec = NULL;
+ }
+ }
+ // Create local echo suppressor if platform AEC cannot be used.
+ if (aec == NULL) {
+ echo = new EchoSuppressor(sampleCount,
+ (track.latency() + record.latency()) * sampleRate / 1000);
+ }
+ }
+ // Start AudioRecord before AudioTrack. This prevents AudioTrack from being
+ // disabled due to buffer underrun while waiting for AudioRecord.
+ if (mode != MUTED) {
+ record.start();
+ int16_t one;
+ record.read(&one, sizeof(one));
+ }
+ track.start();
+
+ while (!exitPending()) {
+ int16_t output[sampleCount];
+ if (recv(deviceSocket, output, sizeof(output), 0) <= 0) {
+ memset(output, 0, sizeof(output));
+ }
+
+ int16_t input[sampleCount];
+ int toWrite = sampleCount;
+ int toRead = (mode == MUTED) ? 0 : sampleCount;
+ int chances = 100;
+
+ while (--chances > 0 && (toWrite > 0 || toRead > 0)) {
+ if (toWrite > 0) {
+ AudioTrack::Buffer buffer;
+ buffer.frameCount = toWrite;
+
+ status_t status = track.obtainBuffer(&buffer, 1);
+ if (status == NO_ERROR) {
+ int offset = sampleCount - toWrite;
+ memcpy(buffer.i8, &output[offset], buffer.size);
+ toWrite -= buffer.frameCount;
+ track.releaseBuffer(&buffer);
+ } else if (status != TIMED_OUT && status != WOULD_BLOCK) {
+ ALOGE("cannot write to AudioTrack");
+ goto exit;
+ }
+ }
+
+ if (toRead > 0) {
+ AudioRecord::Buffer buffer;
+ buffer.frameCount = toRead;
+
+ status_t status = record.obtainBuffer(&buffer, 1);
+ if (status == NO_ERROR) {
+ int offset = sampleCount - toRead;
+ memcpy(&input[offset], buffer.i8, buffer.size);
+ toRead -= buffer.frameCount;
+ record.releaseBuffer(&buffer);
+ } else if (status != TIMED_OUT && status != WOULD_BLOCK) {
+ ALOGE("cannot read from AudioRecord");
+ goto exit;
+ }
+ }
+ }
+
+ if (chances <= 0) {
+ ALOGW("device loop timeout");
+ while (recv(deviceSocket, &c, 1, MSG_DONTWAIT) == 1);
+ }
+
+ if (mode != MUTED) {
+ if (echo != NULL) {
+ ALOGV("echo->run()");
+ echo->run(output, input);
+ }
+ send(deviceSocket, input, sizeof(input), MSG_DONTWAIT);
+ }
+ }
+
+exit:
+ delete echo;
+ delete aec;
+ return true;
+}
+
+//------------------------------------------------------------------------------
+
+static jfieldID gNative;
+static jfieldID gMode;
+
+int add(JNIEnv *env, jobject thiz, jint mode,
+ jint socket, jstring jRemoteAddress, jint remotePort,
+ jstring jCodecSpec, jint dtmfType)
+{
+ AudioCodec *codec = NULL;
+ AudioStream *stream = NULL;
+ AudioGroup *group = NULL;
+
+ // Sanity check.
+ sockaddr_storage remote;
+ if (parse(env, jRemoteAddress, remotePort, &remote) < 0) {
+ // Exception already thrown.
+ return 0;
+ }
+ if (!jCodecSpec) {
+ jniThrowNullPointerException(env, "codecSpec");
+ return 0;
+ }
+ const char *codecSpec = env->GetStringUTFChars(jCodecSpec, NULL);
+ if (!codecSpec) {
+ // Exception already thrown.
+ return 0;
+ }
+ socket = dup(socket);
+ if (socket == -1) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot get stream socket");
+ return 0;
+ }
+
+ // Create audio codec.
+ int codecType = -1;
+ char codecName[16];
+ int sampleRate = -1;
+ sscanf(codecSpec, "%d %15[^/]%*c%d", &codecType, codecName, &sampleRate);
+ codec = newAudioCodec(codecName);
+ int sampleCount = (codec ? codec->set(sampleRate, codecSpec) : -1);
+ env->ReleaseStringUTFChars(jCodecSpec, codecSpec);
+ if (sampleCount <= 0) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot initialize audio codec");
+ goto error;
+ }
+
+ // Create audio stream.
+ stream = new AudioStream;
+ if (!stream->set(mode, socket, &remote, codec, sampleRate, sampleCount,
+ codecType, dtmfType)) {
+ jniThrowException(env, "java/lang/IllegalStateException",
+ "cannot initialize audio stream");
+ goto error;
+ }
+ socket = -1;
+ codec = NULL;
+
+ // 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 (int)stream;
+
+error:
+ delete group;
+ delete stream;
+ delete codec;
+ close(socket);
+ env->SetIntField(thiz, gNative, 0);
+ return 0;
+}
+
+void remove(JNIEnv *env, jobject thiz, jint stream)
+{
+ AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
+ if (group) {
+ if (!stream || !group->remove((AudioStream *)stream)) {
+ delete group;
+ env->SetIntField(thiz, gNative, 0);
+ }
+ }
+}
+
+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);
+ }
+}
+
+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[] = {
+ {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)I", (void *)add},
+ {"nativeRemove", "(I)V", (void *)remove},
+ {"nativeSetMode", "(I)V", (void *)setMode},
+ {"nativeSendDtmf", "(I)V", (void *)sendDtmf},
+};
+
+} // namespace
+
+int registerAudioGroup(JNIEnv *env)
+{
+ gRandom = open("/dev/urandom", O_RDONLY);
+ if (gRandom == -1) {
+ ALOGE("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) {
+ ALOGE("JNI registration failed");
+ return -1;
+ }
+ return 0;
+}
diff --git a/jni/rtp/EchoSuppressor.cpp b/jni/rtp/EchoSuppressor.cpp
new file mode 100644
index 0000000..e223136
--- /dev/null
+++ b/jni/rtp/EchoSuppressor.cpp
@@ -0,0 +1,196 @@
+/*
+ * 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 <stdio.h>
+#include <string.h>
+#include <stdint.h>
+#include <string.h>
+#include <math.h>
+
+#define LOG_TAG "Echo"
+#include <utils/Log.h>
+
+#include "EchoSuppressor.h"
+
+// It is very difficult to do echo cancellation at this level due to the lack of
+// the timing information of the samples being played and recorded. Therefore,
+// for the first release only echo suppression is implemented.
+
+// The algorithm is derived from the "previous works" summarized in
+// A new class of doubletalk detectors based on cross-correlation,
+// J Benesty, DR Morgan, JH Cho, IEEE Trans. on Speech and Audio Processing.
+// The method proposed in that paper is not used because of its high complexity.
+
+// It is well known that cross-correlation can be computed using convolution,
+// but unfortunately not every mobile processor has a (fast enough) FPU. Thus
+// we use integer arithmetic as much as possible and do lots of bookkeeping.
+// Again, parameters and thresholds are chosen by experiments.
+
+EchoSuppressor::EchoSuppressor(int sampleCount, int tailLength)
+{
+ tailLength += sampleCount * 4;
+
+ int shift = 0;
+ while ((sampleCount >> shift) > 1 && (tailLength >> shift) > 256) {
+ ++shift;
+ }
+
+ mShift = shift + 4;
+ mScale = 1 << shift;
+ mSampleCount = sampleCount;
+ mWindowSize = sampleCount >> shift;
+ mTailLength = tailLength >> shift;
+ mRecordLength = tailLength * 2 / sampleCount;
+ mRecordOffset = 0;
+
+ mXs = new uint16_t[mTailLength + mWindowSize];
+ memset(mXs, 0, sizeof(*mXs) * (mTailLength + mWindowSize));
+ mXSums = new uint32_t[mTailLength];
+ memset(mXSums, 0, sizeof(*mXSums) * mTailLength);
+ mX2Sums = new uint32_t[mTailLength];
+ memset(mX2Sums, 0, sizeof(*mX2Sums) * mTailLength);
+ mXRecords = new uint16_t[mRecordLength * mWindowSize];
+ memset(mXRecords, 0, sizeof(*mXRecords) * mRecordLength * mWindowSize);
+
+ mYSum = 0;
+ mY2Sum = 0;
+ mYRecords = new uint32_t[mRecordLength];
+ memset(mYRecords, 0, sizeof(*mYRecords) * mRecordLength);
+ mY2Records = new uint32_t[mRecordLength];
+ memset(mY2Records, 0, sizeof(*mY2Records) * mRecordLength);
+
+ mXYSums = new uint32_t[mTailLength];
+ memset(mXYSums, 0, sizeof(*mXYSums) * mTailLength);
+ mXYRecords = new uint32_t[mRecordLength * mTailLength];
+ memset(mXYRecords, 0, sizeof(*mXYRecords) * mRecordLength * mTailLength);
+
+ mLastX = 0;
+ mLastY = 0;
+ mWeight = 1.0f / (mRecordLength * mWindowSize);
+}
+
+EchoSuppressor::~EchoSuppressor()
+{
+ delete [] mXs;
+ delete [] mXSums;
+ delete [] mX2Sums;
+ delete [] mXRecords;
+ delete [] mYRecords;
+ delete [] mY2Records;
+ delete [] mXYSums;
+ delete [] mXYRecords;
+}
+
+void EchoSuppressor::run(int16_t *playbacked, int16_t *recorded)
+{
+ // Update Xs.
+ for (int i = mTailLength - 1; i >= 0; --i) {
+ mXs[i + mWindowSize] = mXs[i];
+ }
+ for (int i = mWindowSize - 1, j = 0; i >= 0; --i, j += mScale) {
+ uint32_t sum = 0;
+ for (int k = 0; k < mScale; ++k) {
+ int32_t x = playbacked[j + k] << 15;
+ mLastX += x;
+ sum += ((mLastX >= 0) ? mLastX : -mLastX) >> 15;
+ mLastX -= (mLastX >> 10) + x;
+ }
+ mXs[i] = sum >> mShift;
+ }
+
+ // Update XSums, X2Sums, and XRecords.
+ for (int i = mTailLength - mWindowSize - 1; i >= 0; --i) {
+ mXSums[i + mWindowSize] = mXSums[i];
+ mX2Sums[i + mWindowSize] = mX2Sums[i];
+ }
+ uint16_t *xRecords = &mXRecords[mRecordOffset * mWindowSize];
+ for (int i = mWindowSize - 1; i >= 0; --i) {
+ uint16_t x = mXs[i];
+ mXSums[i] = mXSums[i + 1] + x - xRecords[i];
+ mX2Sums[i] = mX2Sums[i + 1] + x * x - xRecords[i] * xRecords[i];
+ xRecords[i] = x;
+ }
+
+ // Compute Ys.
+ uint16_t ys[mWindowSize];
+ for (int i = mWindowSize - 1, j = 0; i >= 0; --i, j += mScale) {
+ uint32_t sum = 0;
+ for (int k = 0; k < mScale; ++k) {
+ int32_t y = recorded[j + k] << 15;
+ mLastY += y;
+ sum += ((mLastY >= 0) ? mLastY : -mLastY) >> 15;
+ mLastY -= (mLastY >> 10) + y;
+ }
+ ys[i] = sum >> mShift;
+ }
+
+ // Update YSum, Y2Sum, YRecords, and Y2Records.
+ uint32_t ySum = 0;
+ uint32_t y2Sum = 0;
+ for (int i = mWindowSize - 1; i >= 0; --i) {
+ ySum += ys[i];
+ y2Sum += ys[i] * ys[i];
+ }
+ mYSum += ySum - mYRecords[mRecordOffset];
+ mY2Sum += y2Sum - mY2Records[mRecordOffset];
+ mYRecords[mRecordOffset] = ySum;
+ mY2Records[mRecordOffset] = y2Sum;
+
+ // Update XYSums and XYRecords.
+ uint32_t *xyRecords = &mXYRecords[mRecordOffset * mTailLength];
+ for (int i = mTailLength - 1; i >= 0; --i) {
+ uint32_t xySum = 0;
+ for (int j = mWindowSize - 1; j >= 0; --j) {
+ xySum += mXs[i + j] * ys[j];
+ }
+ mXYSums[i] += xySum - xyRecords[i];
+ xyRecords[i] = xySum;
+ }
+
+ // Compute correlations.
+ int latency = 0;
+ float corr2 = 0.0f;
+ float varX = 0.0f;
+ float varY = mY2Sum - mWeight * mYSum * mYSum;
+ for (int i = mTailLength - 1; i >= 0; --i) {
+ float cov = mXYSums[i] - mWeight * mXSums[i] * mYSum;
+ if (cov > 0.0f) {
+ float varXi = mX2Sums[i] - mWeight * mXSums[i] * mXSums[i];
+ float corr2i = cov * cov / (varXi * varY + 1);
+ if (corr2i > corr2) {
+ varX = varXi;
+ corr2 = corr2i;
+ latency = i;
+ }
+ }
+ }
+ //ALOGI("corr^2 %.5f, var %8.0f %8.0f, latency %d", corr2, varX, varY,
+ // latency * mScale);
+
+ // Do echo suppression.
+ if (corr2 > 0.1f && varX > 10000.0f) {
+ int factor = (corr2 > 1.0f) ? 0 : (1.0f - sqrtf(corr2)) * 4096;
+ for (int i = 0; i < mSampleCount; ++i) {
+ recorded[i] = recorded[i] * factor >> 16;
+ }
+ }
+
+ // Increase RecordOffset.
+ ++mRecordOffset;
+ if (mRecordOffset == mRecordLength) {
+ mRecordOffset = 0;
+ }
+}
diff --git a/jni/rtp/EchoSuppressor.h b/jni/rtp/EchoSuppressor.h
new file mode 100644
index 0000000..2f3b593
--- /dev/null
+++ b/jni/rtp/EchoSuppressor.h
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+#ifndef __ECHO_SUPPRESSOR_H__
+#define __ECHO_SUPPRESSOR_H__
+
+#include <stdint.h>
+
+class EchoSuppressor
+{
+public:
+ // The sampleCount must be power of 2.
+ EchoSuppressor(int sampleCount, int tailLength);
+ ~EchoSuppressor();
+ void run(int16_t *playbacked, int16_t *recorded);
+
+private:
+ int mShift;
+ int mScale;
+ int mSampleCount;
+ int mWindowSize;
+ int mTailLength;
+ int mRecordLength;
+ int mRecordOffset;
+
+ uint16_t *mXs;
+ uint32_t *mXSums;
+ uint32_t *mX2Sums;
+ uint16_t *mXRecords;
+
+ uint32_t mYSum;
+ uint32_t mY2Sum;
+ uint32_t *mYRecords;
+ uint32_t *mY2Records;
+
+ uint32_t *mXYSums;
+ uint32_t *mXYRecords;
+
+ int32_t mLastX;
+ int32_t mLastY;
+
+ float mWeight;
+};
+
+#endif
diff --git a/jni/rtp/G711Codec.cpp b/jni/rtp/G711Codec.cpp
new file mode 100644
index 0000000..ef54863
--- /dev/null
+++ b/jni/rtp/G711Codec.cpp
@@ -0,0 +1,144 @@
+/*
+ * 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 "AudioCodec.h"
+
+namespace {
+
+const 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:
+ int set(int sampleRate, const char *fmtp) {
+ mSampleCount = sampleRate / 50;
+ return mSampleCount;
+ }
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, int count, 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, int count, void *payload, int length)
+{
+ int8_t *ulaws = (int8_t *)payload;
+ if (length > count) {
+ length = count;
+ }
+ 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;
+}
+
+//------------------------------------------------------------------------------
+
+class AlawCodec : public AudioCodec
+{
+public:
+ int set(int sampleRate, const char *fmtp) {
+ mSampleCount = sampleRate / 50;
+ return mSampleCount;
+ }
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, int count, 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, int count, void *payload, int length)
+{
+ int8_t *alaws = (int8_t *)payload;
+ if (length > count) {
+ length = count;
+ }
+ 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;
+}
+
+} // namespace
+
+AudioCodec *newUlawCodec()
+{
+ return new UlawCodec;
+}
+
+AudioCodec *newAlawCodec()
+{
+ return new AlawCodec;
+}
diff --git a/jni/rtp/GsmCodec.cpp b/jni/rtp/GsmCodec.cpp
new file mode 100644
index 0000000..61dfdc9
--- /dev/null
+++ b/jni/rtp/GsmCodec.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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 "AudioCodec.h"
+
+extern "C" {
+#include "gsm.h"
+}
+
+namespace {
+
+class GsmCodec : public AudioCodec
+{
+public:
+ GsmCodec() {
+ mEncode = gsm_create();
+ mDecode = gsm_create();
+ }
+
+ ~GsmCodec() {
+ if (mEncode) {
+ gsm_destroy(mEncode);
+ }
+ if (mDecode) {
+ gsm_destroy(mDecode);
+ }
+ }
+
+ int set(int sampleRate, const char *fmtp) {
+ return (sampleRate == 8000 && mEncode && mDecode) ? 160 : -1;
+ }
+
+ int encode(void *payload, int16_t *samples);
+ int decode(int16_t *samples, int count, void *payload, int length);
+
+private:
+ gsm mEncode;
+ gsm mDecode;
+};
+
+int GsmCodec::encode(void *payload, int16_t *samples)
+{
+ gsm_encode(mEncode, samples, (unsigned char *)payload);
+ return 33;
+}
+
+int GsmCodec::decode(int16_t *samples, int count, void *payload, int length)
+{
+ unsigned char *bytes = (unsigned char *)payload;
+ int n = 0;
+ while (n + 160 <= count && length >= 33 &&
+ gsm_decode(mDecode, bytes, &samples[n]) == 0) {
+ n += 160;
+ length -= 33;
+ bytes += 33;
+ }
+ return n;
+}
+
+} // namespace
+
+AudioCodec *newGsmCodec()
+{
+ return new GsmCodec;
+}
diff --git a/jni/rtp/RtpStream.cpp b/jni/rtp/RtpStream.cpp
new file mode 100644
index 0000000..bfe8e24
--- /dev/null
+++ b/jni/rtp/RtpStream.cpp
@@ -0,0 +1,113 @@
+/*
+ * 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 gSocket;
+
+jint create(JNIEnv *env, jobject thiz, jstring jAddress)
+{
+ env->SetIntField(thiz, gSocket, -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, gSocket, 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, gSocket, socket);
+ return port;
+ }
+ }
+ }
+
+ jniThrowException(env, "java/net/SocketException", strerror(errno));
+ ::close(socket);
+ return -1;
+}
+
+void close(JNIEnv *env, jobject thiz)
+{
+ int socket = env->GetIntField(thiz, gSocket);
+ ::close(socket);
+ env->SetIntField(thiz, gSocket, -1);
+}
+
+JNINativeMethod gMethods[] = {
+ {"create", "(Ljava/lang/String;)I", (void *)create},
+ {"close", "()V", (void *)close},
+};
+
+} // namespace
+
+int registerRtpStream(JNIEnv *env)
+{
+ jclass clazz;
+ if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL ||
+ (gSocket = env->GetFieldID(clazz, "mSocket", "I")) == NULL ||
+ env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
+ ALOGE("JNI registration failed");
+ return -1;
+ }
+ return 0;
+}
diff --git a/jni/rtp/rtp_jni.cpp b/jni/rtp/rtp_jni.cpp
new file mode 100644
index 0000000..9f4bff9
--- /dev/null
+++ b/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/jni/rtp/util.cpp b/jni/rtp/util.cpp
new file mode 100644
index 0000000..1d702fc
--- /dev/null
+++ b/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;
+}