From 363c2ab82cca4f095e9e0c8465e28f6d27a24bf8 Mon Sep 17 00:00:00 2001 From: Chung-yih Wang Date: Thu, 5 Aug 2010 10:21:20 +0800 Subject: Move the sip related codes to framework. Change-Id: Ib81dadc39b73325c8438f078c7251857a83834fe --- voip/java/android/net/rtp/AudioCodec.java | 56 ++ voip/java/android/net/rtp/AudioGroup.java | 91 +++ voip/java/android/net/rtp/AudioStream.java | 136 ++++ voip/java/android/net/rtp/RtpStream.java | 173 +++++ voip/java/android/net/sip/BinderHelper.java | 79 +++ voip/java/android/net/sip/ISipService.aidl | 42 ++ voip/java/android/net/sip/ISipSession.aidl | 147 +++++ voip/java/android/net/sip/ISipSessionListener.aidl | 126 ++++ .../android/net/sip/SdpSessionDescription.java | 428 +++++++++++++ voip/java/android/net/sip/SessionDescription.aidl | 19 + voip/java/android/net/sip/SessionDescription.java | 83 +++ voip/java/android/net/sip/SipAudioCall.java | 306 +++++++++ voip/java/android/net/sip/SipAudioCallImpl.java | 701 +++++++++++++++++++++ voip/java/android/net/sip/SipManager.java | 499 +++++++++++++++ voip/java/android/net/sip/SipProfile.aidl | 19 + voip/java/android/net/sip/SipProfile.java | 401 ++++++++++++ .../android/net/sip/SipRegistrationListener.java | 48 ++ voip/java/android/net/sip/SipSessionAdapter.java | 67 ++ voip/java/android/net/sip/SipSessionState.java | 66 ++ 19 files changed, 3487 insertions(+) create mode 100644 voip/java/android/net/rtp/AudioCodec.java create mode 100644 voip/java/android/net/rtp/AudioGroup.java create mode 100644 voip/java/android/net/rtp/AudioStream.java create mode 100644 voip/java/android/net/rtp/RtpStream.java create mode 100644 voip/java/android/net/sip/BinderHelper.java create mode 100644 voip/java/android/net/sip/ISipService.aidl create mode 100644 voip/java/android/net/sip/ISipSession.aidl create mode 100644 voip/java/android/net/sip/ISipSessionListener.aidl create mode 100644 voip/java/android/net/sip/SdpSessionDescription.java create mode 100644 voip/java/android/net/sip/SessionDescription.aidl create mode 100644 voip/java/android/net/sip/SessionDescription.java create mode 100644 voip/java/android/net/sip/SipAudioCall.java create mode 100644 voip/java/android/net/sip/SipAudioCallImpl.java create mode 100644 voip/java/android/net/sip/SipManager.java create mode 100644 voip/java/android/net/sip/SipProfile.aidl create mode 100644 voip/java/android/net/sip/SipProfile.java create mode 100644 voip/java/android/net/sip/SipRegistrationListener.java create mode 100644 voip/java/android/net/sip/SipSessionAdapter.java create mode 100644 voip/java/android/net/sip/SipSessionState.java (limited to 'voip') diff --git a/voip/java/android/net/rtp/AudioCodec.java b/voip/java/android/net/rtp/AudioCodec.java new file mode 100644 index 0000000..89e6aa9 --- /dev/null +++ b/voip/java/android/net/rtp/AudioCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +/** @hide */ +public class AudioCodec { + public static final AudioCodec ULAW = new AudioCodec("PCMU", 8000, 160, 0); + public static final AudioCodec ALAW = new AudioCodec("PCMA", 8000, 160, 8); + + /** + * Returns system supported codecs. + */ + public static AudioCodec[] getSystemSupportedCodecs() { + return new AudioCodec[] {AudioCodec.ULAW, AudioCodec.ALAW}; + } + + /** + * Returns the codec instance if it is supported by the system. + * + * @param name name of the codec + * @return the matched codec or null if the codec name is not supported by + * the system + */ + public static AudioCodec getSystemSupportedCodec(String name) { + for (AudioCodec codec : getSystemSupportedCodecs()) { + if (codec.name.equals(name)) return codec; + } + return null; + } + + public final String name; + public final int sampleRate; + public final int sampleCount; + public final int defaultType; + + private AudioCodec(String name, int sampleRate, int sampleCount, int defaultType) { + this.name = name; + this.sampleRate = sampleRate; + this.sampleCount = sampleCount; + this.defaultType = defaultType; + } +} diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java new file mode 100644 index 0000000..dc86082 --- /dev/null +++ b/voip/java/android/net/rtp/AudioGroup.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.util.HashMap; +import java.util.Map; + +/** + */ +/** @hide */ +public class AudioGroup { + public static final int MODE_ON_HOLD = 0; + public static final int MODE_MUTED = 1; + public static final int MODE_NORMAL = 2; + public static final int MODE_EC_ENABLED = 3; + + private final Map mStreams; + private int mMode = MODE_ON_HOLD; + + private int mNative; + static { + System.loadLibrary("rtp_jni"); + } + + public AudioGroup() { + mStreams = new HashMap(); + } + + public int getMode() { + return mMode; + } + + public synchronized native void setMode(int mode); + + synchronized void add(AudioStream stream, AudioCodec codec, int codecType, int dtmfType) { + if (!mStreams.containsKey(stream)) { + try { + int id = add(stream.getMode(), stream.dup(), + stream.getRemoteAddress().getHostAddress(), stream.getRemotePort(), + codec.name, codec.sampleRate, codec.sampleCount, codecType, dtmfType); + mStreams.put(stream, id); + } catch (NullPointerException e) { + throw new IllegalStateException(e); + } + } + } + + private native int add(int mode, int socket, String remoteAddress, int remotePort, + String codecName, int sampleRate, int sampleCount, int codecType, int dtmfType); + + synchronized void remove(AudioStream stream) { + Integer id = mStreams.remove(stream); + if (id != null) { + remove(id); + } + } + + private native void remove(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 native synchronized void sendDtmf(int event); + + public synchronized void reset() { + remove(-1); + } + + @Override + protected void finalize() throws Throwable { + reset(); + super.finalize(); + } +} diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java new file mode 100644 index 0000000..a955fd2 --- /dev/null +++ b/voip/java/android/net/rtp/AudioStream.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.net.InetAddress; +import java.net.SocketException; + +/** + * AudioStream represents a RTP stream carrying audio payloads. + */ +/** @hide */ +public class AudioStream extends RtpStream { + private AudioCodec mCodec; + private int mCodecType = -1; + private int mDtmfType = -1; + private AudioGroup mGroup; + + /** + * Creates an AudioStream on the given local address. Note that the local + * port is assigned automatically to conform with RFC 3550. + * + * @param address The network address of the local host to bind to. + * @throws SocketException if the address cannot be bound or a problem + * occurs during binding. + */ + public AudioStream(InetAddress address) throws SocketException { + super(address); + } + + /** + * Returns {@code true} if the stream already joined an {@link AudioGroup}. + */ + @Override + public final boolean isBusy() { + return mGroup != null; + } + + /** + * Returns the joined {@link AudioGroup}. + */ + public AudioGroup getAudioGroup() { + return mGroup; + } + + /** + * Joins an {@link AudioGroup}. Each stream can join only one group at a + * time. The group can be changed by passing a different one or removed + * by calling this method with {@code null}. + * + * @param group The AudioGroup to join or {@code null} to leave. + * @throws IllegalStateException if the stream is not properly configured. + * @see AudioGroup + */ + public void join(AudioGroup group) { + if (mGroup == group) { + return; + } + if (mGroup != null) { + mGroup.remove(this); + mGroup = null; + } + if (group != null) { + group.add(this, mCodec, mCodecType, mDtmfType); + mGroup = group; + } + } + + /** + * Sets the {@link AudioCodec} and its RTP payload type. According to RFC + * 3551, the type must be in the range of 0 and 127, where 96 and above are + * dynamic types. For codecs with static mappings (non-negative + * {@link AudioCodec#defaultType}), assigning a different non-dynamic type + * is disallowed. + * + * @param codec The AudioCodec to be used. + * @param type The RTP payload type. + * @throws IllegalArgumentException if the type is invalid or used by DTMF. + * @throws IllegalStateException if the stream is busy. + */ + public void setCodec(AudioCodec codec, int type) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (type < 0 || type > 127 || (type != codec.defaultType && type < 96)) { + throw new IllegalArgumentException("Invalid type"); + } + if (type == mDtmfType) { + throw new IllegalArgumentException("The type is used by DTMF"); + } + mCodec = codec; + mCodecType = type; + } + + /** + * Sets the RTP payload type for dual-tone multi-frequency (DTMF) digits. + * The primary usage is to send digits to the remote gateway to perform + * certain tasks, such as second-stage dialing. According to RFC 2833, the + * RTP payload type for DTMF is assigned dynamically, so it must be in the + * range of 96 and 127. One can use {@code -1} to disable DTMF and free up + * the previous assigned value. This method cannot be called when the stream + * already joined an {@link AudioGroup}. + * + * @param type The RTP payload type to be used or {@code -1} to disable it. + * @throws IllegalArgumentException if the type is invalid or used by codec. + * @throws IllegalStateException if the stream is busy. + * @see AudioGroup#sendDtmf(int) + */ + public void setDtmfType(int type) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (type != -1) { + if (type < 96 || type > 127) { + throw new IllegalArgumentException("Invalid type"); + } + if (type == mCodecType) { + throw new IllegalArgumentException("The type is used by codec"); + } + } + mDtmfType = type; + } +} diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java new file mode 100644 index 0000000..ef5ca17 --- /dev/null +++ b/voip/java/android/net/rtp/RtpStream.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.SocketException; + +/** + * RtpStream represents a base class of media streams running over + * Real-time Transport Protocol (RTP). + */ +/** @hide */ +public class RtpStream { + public static final int MODE_NORMAL = 0; + public static final int MODE_SEND_ONLY = 1; + public static final int MODE_RECEIVE_ONLY = 2; + + private final InetAddress mLocalAddress; + private final int mLocalPort; + + private InetAddress mRemoteAddress; + private int mRemotePort = -1; + private int mMode = MODE_NORMAL; + + private int mNative; + static { + System.loadLibrary("rtp_jni"); + } + + /** + * Creates a RtpStream on the given local address. Note that the local + * port is assigned automatically to conform with RFC 3550. + * + * @param address The network address of the local host to bind to. + * @throws SocketException if the address cannot be bound or a problem + * occurs during binding. + */ + RtpStream(InetAddress address) throws SocketException { + mLocalPort = create(address.getHostAddress()); + mLocalAddress = address; + } + + private native int create(String address) throws SocketException; + + /** + * Returns the network address of the local host. + */ + public InetAddress getLocalAddress() { + return mLocalAddress; + } + + /** + * Returns the network port of the local host. + */ + public int getLocalPort() { + return mLocalPort; + } + + /** + * Returns the network address of the remote host or {@code null} if the + * stream is not associated. + */ + public InetAddress getRemoteAddress() { + return mRemoteAddress; + } + + /** + * Returns the network port of the remote host or {@code -1} if the stream + * is not associated. + */ + public int getRemotePort() { + return mRemotePort; + } + + /** + * Returns {@code true} if the stream is busy. This method is intended to be + * overridden by subclasses. + */ + public boolean isBusy() { + return false; + } + + /** + * Returns the current mode. The initial mode is {@link #MODE_NORMAL}. + */ + public int getMode() { + return mMode; + } + + /** + * Changes the current mode. It must be one of {@link #MODE_NORMAL}, + * {@link #MODE_SEND_ONLY}, and {@link #MODE_RECEIVE_ONLY}. + * + * @param mode The mode to change to. + * @throws IllegalArgumentException if the mode is invalid. + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void setMode(int mode) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (mode != MODE_NORMAL && mode != MODE_SEND_ONLY && mode != MODE_RECEIVE_ONLY) { + throw new IllegalArgumentException("Invalid mode"); + } + mMode = mode; + } + + /** + * Associates with a remote host. + * + * @param address The network address of the remote host. + * @param port The network port of the remote host. + * @throws IllegalArgumentException if the address is not supported or the + * port is invalid. + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void associate(InetAddress address, int port) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (!(address instanceof Inet4Address && mLocalAddress instanceof Inet4Address) && + !(address instanceof Inet6Address && mLocalAddress instanceof Inet6Address)) { + throw new IllegalArgumentException("Unsupported address"); + } + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Invalid port"); + } + mRemoteAddress = address; + mRemotePort = port; + } + + synchronized native int dup(); + + /** + * Releases allocated resources. The stream becomes inoperable after calling + * this method. + * + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void release() { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + close(); + } + + private synchronized native void close(); + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } +} diff --git a/voip/java/android/net/sip/BinderHelper.java b/voip/java/android/net/sip/BinderHelper.java new file mode 100644 index 0000000..bd3da32 --- /dev/null +++ b/voip/java/android/net/sip/BinderHelper.java @@ -0,0 +1,79 @@ +/* + * 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.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.ConditionVariable; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Looper; +import android.util.Log; + +// TODO: throw away this class after moving SIP classes to framework +// This class helps to get IBinder instance of a service in a blocking call. +// The method cannot be called in app's main thread as the ServiceConnection +// callback will. +class BinderHelper { + private Context mContext; + private IBinder mBinder; + private Class mClass; + + BinderHelper(Context context, Class klass) { + mContext = context; + mClass = klass; + } + + void startService() { + mContext.startService(new Intent(mClass.getName())); + } + + void stopService() { + mContext.stopService(new Intent(mClass.getName())); + } + + IBinder getBinder() { + // cannot call this method in app's main thread + if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + throw new RuntimeException( + "This method cannot be called in app's main thread"); + } + + final ConditionVariable cv = new ConditionVariable(); + cv.close(); + ServiceConnection c = new ServiceConnection() { + public synchronized void onServiceConnected( + ComponentName className, IBinder binder) { + Log.v("BinderHelper", "service connected!"); + mBinder = binder; + cv.open(); + mContext.unbindService(this); + } + + public void onServiceDisconnected(ComponentName className) { + cv.open(); + mContext.unbindService(this); + } + }; + if (mContext.bindService(new Intent(mClass.getName()), c, 0)) { + cv.block(4500); + } + return mBinder; + } +} diff --git a/voip/java/android/net/sip/ISipService.aidl b/voip/java/android/net/sip/ISipService.aidl new file mode 100644 index 0000000..6c68213 --- /dev/null +++ b/voip/java/android/net/sip/ISipService.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSession; +import android.net.sip.ISipSessionListener; +import android.net.sip.SipProfile; + +/** + * {@hide} + */ +interface ISipService { + void open(in SipProfile localProfile); + void open3(in SipProfile localProfile, + String incomingCallBroadcastAction, + in ISipSessionListener listener); + void close(in String localProfileUri); + boolean isOpened(String localProfileUri); + boolean isRegistered(String localProfileUri); + void setRegistrationListener(String localProfileUri, + ISipSessionListener listener); + + ISipSession createSession(in SipProfile localProfile, + in ISipSessionListener listener); + ISipSession getPendingSession(String callId); + + SipProfile[] getListOfProfiles(); +} diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl new file mode 100644 index 0000000..fbcb056 --- /dev/null +++ b/voip/java/android/net/sip/ISipSession.aidl @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSessionListener; +import android.net.sip.SessionDescription; +import android.net.sip.SipProfile; + +/** + * A SIP session that is associated with a SIP dialog or a transaction + * (e.g., registration) not within a dialog. + * @hide + */ +interface ISipSession { + /** + * Gets the IP address of the local host on which this SIP session runs. + * + * @return the IP address of the local host + */ + String getLocalIp(); + + /** + * Gets the SIP profile that this session is associated with. + * + * @return the SIP profile that this session is associated with + */ + SipProfile getLocalProfile(); + + /** + * Gets the SIP profile that this session is connected to. Only available + * when the session is associated with a SIP dialog. + * + * @return the SIP profile that this session is connected to + */ + SipProfile getPeerProfile(); + + /** + * Gets the session state. The value returned must be one of the states in + * {@link SipSessionState}. One may convert it to {@link SipSessionState} by + * + * Enum.valueOf(SipSessionState.class, session.getState()); + * + * + * @return the session state + */ + String getState(); + + /** + * Checks if the session is in a call. + * + * @return true if the session is in a call + */ + boolean isInCall(); + + /** + * Gets the call ID of the session. + * + * @return the call ID + */ + String getCallId(); + + + /** + * Sets the listener to listen to the session events. A {@link ISipSession} + * can only hold one listener at a time. Subsequent calls to this method + * override the previous listener. + * + * @param listener to listen to the session events of this object + */ + void setListener(in ISipSessionListener listener); + + + /** + * Performs registration to the server specified by the associated local + * profile. The session listener is called back upon success or failure of + * registration. The method is only valid to call when the session state is + * in {@link SipSessionState#READY_TO_CALL}. + * + * @param duration duration in second before the registration expires + * @see ISipSessionListener + */ + void register(int duration); + + /** + * Performs unregistration to the server specified by the associated local + * profile. Unregistration is technically the same as registration with zero + * expiration duration. The session listener is called back upon success or + * failure of unregistration. The method is only valid to call when the + * session state is in {@link SipSessionState#READY_TO_CALL}. + * + * @see ISipSessionListener + */ + void unregister(); + + /** + * Initiates a call to the specified profile. The session listener is called + * back upon defined session events. The method is only valid to call when + * the session state is in {@link SipSessionState#READY_TO_CALL}. + * + * @param callee the SIP profile to make the call to + * @param sessionDescription the session description of this call + * @see ISipSessionListener + */ + void makeCall(in SipProfile callee, + in SessionDescription sessionDescription); + + /** + * Answers an incoming call with the specified session description. The + * method is only valid to call when the session state is in + * {@link SipSessionState#INCOMING_CALL}. + * + * @param sessionDescription the session description to answer this call + */ + void answerCall(in SessionDescription sessionDescription); + + /** + * Ends an established call, terminates an outgoing call or rejects an + * incoming call. The method is only valid to call when the session state is + * in {@link SipSessionState#IN_CALL}, + * {@link SipSessionState#INCOMING_CALL}, + * {@link SipSessionState#OUTGOING_CALL} or + * {@link SipSessionState#OUTGOING_CALL_RING_BACK}. + */ + void endCall(); + + /** + * Changes the session description during a call. The method is only valid + * to call when the session state is in {@link SipSessionState#IN_CALL}. + * + * @param sessionDescription the new session description + */ + void changeCall(in SessionDescription sessionDescription); +} diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl new file mode 100644 index 0000000..8570958 --- /dev/null +++ b/voip/java/android/net/sip/ISipSessionListener.aidl @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSession; +import android.net.sip.SipProfile; + +/** + * Listener class to listen to SIP session events. + * @hide + */ +interface ISipSessionListener { + /** + * Called when an INVITE request is sent to initiate a new call. + * + * @param session the session object that carries out the transaction + */ + void onCalling(in ISipSession session); + + /** + * Called when an INVITE request is received. + * + * @param session the session object that carries out the transaction + * @param caller the SIP profile of the caller + * @param sessionDescription the caller's session description + */ + void onRinging(in ISipSession session, in SipProfile caller, + in byte[] sessionDescription); + + /** + * Called when a RINGING response is received for the INVITE request sent + * + * @param session the session object that carries out the transaction + */ + void onRingingBack(in ISipSession session); + + /** + * Called when the session is established. + * + * @param session the session object that is associated with the dialog + * @param sessionDescription the peer's session description + */ + void onCallEstablished(in ISipSession session, + in byte[] sessionDescription); + + /** + * Called when the session is terminated. + * + * @param session the session object that is associated with the dialog + */ + void onCallEnded(in ISipSession session); + + /** + * Called when the peer is busy during session initialization. + * + * @param session the session object that carries out the transaction + */ + void onCallBusy(in ISipSession session); + + /** + * Called when an error occurs during session initialization and + * termination. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onError(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when an error occurs during session modification negotiation. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onCallChangeFailed(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when a registration request is sent. + * + * @param session the session object that carries out the transaction + */ + void onRegistering(in ISipSession session); + + /** + * Called when registration is successfully done. + * + * @param session the session object that carries out the transaction + * @param duration duration in second before the registration expires + */ + void onRegistrationDone(in ISipSession session, int duration); + + /** + * Called when the registration fails. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onRegistrationFailed(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when the registration gets timed out. + * + * @param session the session object that carries out the transaction + */ + void onRegistrationTimeout(in ISipSession session); +} diff --git a/voip/java/android/net/sip/SdpSessionDescription.java b/voip/java/android/net/sip/SdpSessionDescription.java new file mode 100644 index 0000000..0c29935 --- /dev/null +++ b/voip/java/android/net/sip/SdpSessionDescription.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import gov.nist.javax.sdp.SessionDescriptionImpl; +import gov.nist.javax.sdp.fields.AttributeField; +import gov.nist.javax.sdp.fields.ConnectionField; +import gov.nist.javax.sdp.fields.MediaField; +import gov.nist.javax.sdp.fields.OriginField; +import gov.nist.javax.sdp.fields.ProtoVersionField; +import gov.nist.javax.sdp.fields.SessionNameField; +import gov.nist.javax.sdp.fields.TimeField; +import gov.nist.javax.sdp.parser.SDPAnnounceParser; + +import android.util.Log; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Vector; +import javax.sdp.Connection; +import javax.sdp.MediaDescription; +import javax.sdp.SdpException; + +/** + * A session description that follows SDP (Session Description Protocol). + * Refer to RFC 4566. + * @hide + */ +public class SdpSessionDescription extends SessionDescription { + private static final String TAG = "SDP"; + private static final String AUDIO = "audio"; + private static final String RTPMAP = "rtpmap"; + private static final String PTIME = "ptime"; + private static final String SENDONLY = "sendonly"; + private static final String RECVONLY = "recvonly"; + private static final String INACTIVE = "inactive"; + + private SessionDescriptionImpl mSessionDescription; + + /** + * The audio codec information parsed from "rtpmap". + */ + public static class AudioCodec { + public final int payloadType; + public final String name; + public final int sampleRate; + public final int sampleCount; + + public AudioCodec(int payloadType, String name, int sampleRate, + int sampleCount) { + this.payloadType = payloadType; + this.name = name; + this.sampleRate = sampleRate; + this.sampleCount = sampleCount; + } + } + + /** + * The builder class used to create an {@link SdpSessionDescription} object. + */ + public static class Builder { + private SdpSessionDescription mSdp = new SdpSessionDescription(); + private SessionDescriptionImpl mSessionDescription; + + public Builder(String sessionName) throws SdpException { + mSessionDescription = new SessionDescriptionImpl(); + mSdp.mSessionDescription = mSessionDescription; + try { + ProtoVersionField proto = new ProtoVersionField(); + proto.setVersion(0); + mSessionDescription.addField(proto); + + TimeField time = new TimeField(); + time.setZero(); + mSessionDescription.addField(time); + + SessionNameField session = new SessionNameField(); + session.setValue(sessionName); + mSessionDescription.addField(session); + } catch (Exception e) { + throwSdpException(e); + } + } + + public Builder setConnectionInfo(String networkType, String addressType, + String addr) throws SdpException { + try { + ConnectionField connection = new ConnectionField(); + connection.setNetworkType(networkType); + connection.setAddressType(addressType); + connection.setAddress(addr); + mSessionDescription.addField(connection); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder setOrigin(SipProfile user, long sessionId, + long sessionVersion, String networkType, String addressType, + String address) throws SdpException { + try { + OriginField origin = new OriginField(); + origin.setUsername(user.getUserName()); + origin.setSessionId(sessionId); + origin.setSessionVersion(sessionVersion); + origin.setAddressType(addressType); + origin.setNetworkType(networkType); + origin.setAddress(address); + mSessionDescription.addField(origin); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addMedia(String media, int port, int numPorts, + String transport, Integer... types) throws SdpException { + MediaField field = new MediaField(); + Vector typeVector = new Vector(); + Collections.addAll(typeVector, types); + try { + field.setMediaType(media); + field.setMediaPort(port); + field.setPortCount(numPorts); + field.setProtocol(transport); + field.setMediaFormats(typeVector); + mSessionDescription.addField(field); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addMediaAttribute(String type, String name, String value) + throws SdpException { + try { + MediaDescription md = mSdp.getMediaDescription(type); + if (md == null) { + throw new SdpException("Should add media first!"); + } + AttributeField attribute = new AttributeField(); + attribute.setName(name); + attribute.setValueAllowNull(value); + mSessionDescription.addField(attribute); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addSessionAttribute(String name, String value) + throws SdpException { + try { + AttributeField attribute = new AttributeField(); + attribute.setName(name); + attribute.setValueAllowNull(value); + mSessionDescription.addField(attribute); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + private void throwSdpException(Exception e) throws SdpException { + if (e instanceof SdpException) { + throw (SdpException) e; + } else { + throw new SdpException(e.toString(), e); + } + } + + public SdpSessionDescription build() { + return mSdp; + } + } + + private SdpSessionDescription() { + } + + /** + * Constructor. + * + * @param sdpString an SDP session description to parse + */ + public SdpSessionDescription(String sdpString) throws SdpException { + try { + mSessionDescription = new SDPAnnounceParser(sdpString).parse(); + } catch (ParseException e) { + throw new SdpException(e.toString(), e); + } + verify(); + } + + /** + * Constructor. + * + * @param content a raw SDP session description to parse + */ + public SdpSessionDescription(byte[] content) throws SdpException { + this(new String(content)); + } + + private void verify() throws SdpException { + // make sure the syntax is correct over the fields we're interested in + Vector descriptions = (Vector) + mSessionDescription.getMediaDescriptions(false); + for (MediaDescription md : descriptions) { + md.getMedia().getMediaPort(); + Connection connection = md.getConnection(); + if (connection != null) connection.getAddress(); + md.getMedia().getFormats(); + } + Connection connection = mSessionDescription.getConnection(); + if (connection != null) connection.getAddress(); + } + + /** + * Gets the connection address of the media. + * + * @param type the media type; e.g., "AUDIO" + * @return the media connection address of the peer + */ + public String getPeerMediaAddress(String type) { + try { + MediaDescription md = getMediaDescription(type); + Connection connection = md.getConnection(); + if (connection == null) { + connection = mSessionDescription.getConnection(); + } + return ((connection == null) ? null : connection.getAddress()); + } catch (SdpException e) { + // should not occur + return null; + } + } + + /** + * Gets the connection port number of the media. + * + * @param type the media type; e.g., "AUDIO" + * @return the media connection port number of the peer + */ + public int getPeerMediaPort(String type) { + try { + MediaDescription md = getMediaDescription(type); + return md.getMedia().getMediaPort(); + } catch (SdpException e) { + // should not occur + return -1; + } + } + + private boolean containsAttribute(String type, String name) { + if (name == null) return false; + MediaDescription md = getMediaDescription(type); + Vector v = (Vector) + md.getAttributeFields(); + for (AttributeField field : v) { + if (name.equals(field.getAttribute().getName())) return true; + } + return false; + } + + /** + * Checks if the media is "sendonly". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is "sendonly" + */ + public boolean isSendOnly(String type) { + boolean answer = containsAttribute(type, SENDONLY); + Log.d(TAG, " sendonly? " + answer); + return answer; + } + + /** + * Checks if the media is "recvonly". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is "recvonly" + */ + public boolean isReceiveOnly(String type) { + boolean answer = containsAttribute(type, RECVONLY); + Log.d(TAG, " recvonly? " + answer); + return answer; + } + + /** + * Checks if the media is in sending; i.e., not "recvonly" and not + * "inactive". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is sending + */ + public boolean isSending(String type) { + boolean answer = !containsAttribute(type, RECVONLY) + && !containsAttribute(type, INACTIVE); + + Log.d(TAG, " sending? " + answer); + return answer; + } + + /** + * Checks if the media is in receiving; i.e., not "sendonly" and not + * "inactive". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is receiving + */ + public boolean isReceiving(String type) { + boolean answer = !containsAttribute(type, SENDONLY) + && !containsAttribute(type, INACTIVE); + Log.d(TAG, " receiving? " + answer); + return answer; + } + + private AudioCodec parseAudioCodec(String rtpmap, int ptime) { + String[] ss = rtpmap.split(" "); + int payloadType = Integer.parseInt(ss[0]); + + ss = ss[1].split("/"); + String name = ss[0]; + int sampleRate = Integer.parseInt(ss[1]); + int channelCount = 1; + if (ss.length > 2) channelCount = Integer.parseInt(ss[2]); + int sampleCount = sampleRate / (1000 / ptime) * channelCount; + return new AudioCodec(payloadType, name, sampleRate, sampleCount); + } + + /** + * Gets the list of audio codecs in this session description. + * + * @return the list of audio codecs in this session description + */ + public List getAudioCodecs() { + MediaDescription md = getMediaDescription(AUDIO); + if (md == null) return new ArrayList(); + + // FIXME: what happens if ptime is missing + int ptime = 20; + try { + String value = md.getAttribute(PTIME); + if (value != null) ptime = Integer.parseInt(value); + } catch (Throwable t) { + Log.w(TAG, "getCodecs(): ignored: " + t); + } + + List codecs = new ArrayList(); + Vector v = (Vector) + md.getAttributeFields(); + for (AttributeField field : v) { + try { + if (RTPMAP.equals(field.getName())) { + AudioCodec codec = parseAudioCodec(field.getValue(), ptime); + if (codec != null) codecs.add(codec); + } + } catch (Throwable t) { + Log.w(TAG, "getCodecs(): ignored: " + t); + } + } + return codecs; + } + + /** + * Gets the media description of the specified type. + * + * @param type the media type; e.g., "AUDIO" + * @return the media description of the specified type + */ + public MediaDescription getMediaDescription(String type) { + MediaDescription[] all = getMediaDescriptions(); + if ((all == null) || (all.length == 0)) return null; + for (MediaDescription md : all) { + String t = md.getMedia().getMedia(); + if (t.equalsIgnoreCase(type)) return md; + } + return null; + } + + /** + * Gets all the media descriptions in this session description. + * + * @return all the media descriptions in this session description + */ + public MediaDescription[] getMediaDescriptions() { + try { + Vector descriptions = (Vector) + mSessionDescription.getMediaDescriptions(false); + MediaDescription[] all = new MediaDescription[descriptions.size()]; + return descriptions.toArray(all); + } catch (SdpException e) { + Log.e(TAG, "getMediaDescriptions", e); + } + return null; + } + + @Override + public String getType() { + return "sdp"; + } + + @Override + public byte[] getContent() { + return mSessionDescription.toString().getBytes(); + } + + @Override + public String toString() { + return mSessionDescription.toString(); + } +} diff --git a/voip/java/android/net/sip/SessionDescription.aidl b/voip/java/android/net/sip/SessionDescription.aidl new file mode 100644 index 0000000..a120d16 --- /dev/null +++ b/voip/java/android/net/sip/SessionDescription.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +parcelable SessionDescription; diff --git a/voip/java/android/net/sip/SessionDescription.java b/voip/java/android/net/sip/SessionDescription.java new file mode 100644 index 0000000..d476f0b --- /dev/null +++ b/voip/java/android/net/sip/SessionDescription.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Abstract class of a session description. + * @hide + */ +public abstract class SessionDescription implements Parcelable { + /** @hide */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SessionDescription createFromParcel(Parcel in) { + return new SessionDescriptionImpl(in); + } + + public SessionDescription[] newArray(int size) { + return new SessionDescriptionImpl[size]; + } + }; + + /** + * Gets the type of the session description; e.g., "SDP". + * + * @return the session description type + */ + public abstract String getType(); + + /** + * Gets the raw content of the session description. + * + * @return the content of the session description + */ + public abstract byte[] getContent(); + + /** @hide */ + public void writeToParcel(Parcel out, int flags) { + out.writeString(getType()); + out.writeByteArray(getContent()); + } + + /** @hide */ + public int describeContents() { + return 0; + } + + private static class SessionDescriptionImpl extends SessionDescription { + private String mType; + private byte[] mContent; + + SessionDescriptionImpl(Parcel in) { + mType = in.readString(); + mContent = in.createByteArray(); + } + + @Override + public String getType() { + return mType; + } + + @Override + public byte[] getContent() { + return mContent; + } + } +} diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java new file mode 100644 index 0000000..abdc9d7 --- /dev/null +++ b/voip/java/android/net/sip/SipAudioCall.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.rtp.AudioGroup; +import android.net.rtp.AudioStream; +import android.os.Message; + +import javax.sip.SipException; + +/** + * Interface for making audio calls over SIP. + * @hide + */ +public interface SipAudioCall { + /** Listener class for all event callbacks. */ + public interface Listener { + /** + * Called when the call object is ready to make another call. + * + * @param call the call object that is ready to make another call + */ + void onReadyToCall(SipAudioCall call); + + /** + * Called when a request is sent out to initiate a new call. + * + * @param call the call object that carries out the audio call + */ + void onCalling(SipAudioCall call); + + /** + * Called when a new call comes in. + * + * @param call the call object that carries out the audio call + * @param caller the SIP profile of the caller + */ + void onRinging(SipAudioCall call, SipProfile caller); + + /** + * Called when a RINGING response is received for the INVITE request sent + * + * @param call the call object that carries out the audio call + */ + void onRingingBack(SipAudioCall call); + + /** + * Called when the session is established. + * + * @param call the call object that carries out the audio call + */ + void onCallEstablished(SipAudioCall call); + + /** + * Called when the session is terminated. + * + * @param call the call object that carries out the audio call + */ + void onCallEnded(SipAudioCall call); + + /** + * Called when the peer is busy during session initialization. + * + * @param call the call object that carries out the audio call + */ + void onCallBusy(SipAudioCall call); + + /** + * Called when the call is on hold. + * + * @param call the call object that carries out the audio call + */ + void onCallHeld(SipAudioCall call); + + /** + * Called when an error occurs. + * + * @param call the call object that carries out the audio call + * @param errorMessage error message + */ + void onError(SipAudioCall call, String errorMessage); + } + + /** + * The adapter class for {@link SipAudioCall#Listener}. The default + * implementation of all callback methods is no-op. + */ + public class Adapter implements Listener { + protected void onChanged(SipAudioCall call) { + } + public void onReadyToCall(SipAudioCall call) { + onChanged(call); + } + public void onCalling(SipAudioCall call) { + onChanged(call); + } + public void onRinging(SipAudioCall call, SipProfile caller) { + onChanged(call); + } + public void onRingingBack(SipAudioCall call) { + onChanged(call); + } + public void onCallEstablished(SipAudioCall call) { + onChanged(call); + } + public void onCallEnded(SipAudioCall call) { + onChanged(call); + } + public void onCallBusy(SipAudioCall call) { + onChanged(call); + } + public void onCallHeld(SipAudioCall call) { + onChanged(call); + } + public void onError(SipAudioCall call, String errorMessage) { + onChanged(call); + } + } + + /** + * Sets the listener to listen to the audio call events. The method calls + * {@link #setListener(Listener, false)}. + * + * @param listener to listen to the audio call events of this object + * @see #setListener(Listener, boolean) + */ + void setListener(Listener listener); + + /** + * Sets the listener to listen to the audio call events. A + * {@link SipAudioCall} can only hold one listener at a time. Subsequent + * calls to this method override the previous listener. + * + * @param listener to listen to the audio call events of this object + * @param callbackImmediately set to true if the caller wants to be called + * back immediately on the current state + */ + void setListener(Listener listener, boolean callbackImmediately); + + /** + * Closes this object. The object is not usable after being closed. + */ + void close(); + + /** + * Initiates an audio call to the specified profile. + * + * @param callee the SIP profile to make the call to + * @param sipManager the {@link SipManager} object to help make call with + */ + void makeCall(SipProfile callee, SipManager sipManager) throws SipException; + + /** + * Attaches an incoming call to this call object. + * + * @param session the session that receives the incoming call + * @param sdp the session description of the incoming call + */ + void attachCall(ISipSession session, SdpSessionDescription sdp) + throws SipException; + + /** Ends a call. */ + void endCall() throws SipException; + + /** + * Puts a call on hold. When succeeds, + * {@link #Listener#onCallHeld(SipAudioCall)} is called. + */ + void holdCall() throws SipException; + + /** Answers a call. */ + void answerCall() throws SipException; + + /** + * Continues a call that's on hold. When succeeds, + * {@link #Listener#onCallEstablished(SipAudioCall)} is called. + */ + void continueCall() throws SipException; + + /** Puts the device to in-call mode. */ + void setInCallMode(); + + /** Puts the device to speaker mode. */ + void setSpeakerMode(); + + /** Toggles mute. */ + void toggleMute(); + + /** + * Checks if the call is on hold. + * + * @return true if the call is on hold + */ + boolean isOnHold(); + + /** + * Checks if the call is muted. + * + * @return true if the call is muted + */ + boolean isMuted(); + + /** + * Sends a DTMF code. + * + * @param code the DTMF code to send + */ + void sendDtmf(int code); + + /** + * Sends a DTMF code. + * + * @param code the DTMF code to send + * @param result the result message to send when done + */ + void sendDtmf(int code, Message result); + + /** + * Gets the {@link AudioStream} object used in this call. The object + * represents the RTP stream that carries the audio data to and from the + * peer. The object may not be created before the call is established. And + * it is undefined after the call ends or the {@link #close} method is + * called. + * + * @return the {@link AudioStream} object or null if the RTP stream has not + * yet been set up + */ + AudioStream getAudioStream(); + + /** + * Gets the {@link AudioGroup} object which the {@link AudioStream} object + * joins. The group object may not exist before the call is established. + * Also, the {@code AudioStream} may change its group during a call (e.g., + * after the call is held/un-held). Finally, the {@code AudioGroup} object + * returned by this method is undefined after the call ends or the + * {@link #close} method is called. + * + * @return the {@link AudioGroup} object or null if the RTP stream has not + * yet been set up + * @see #getAudioStream + */ + AudioGroup getAudioGroup(); + + /** + * Checks if the call is established. + * + * @return true if the call is established + */ + boolean isInCall(); + + /** + * Gets the local SIP profile. + * + * @return the local SIP profile + */ + SipProfile getLocalProfile(); + + /** + * Gets the peer's SIP profile. + * + * @return the peer's SIP profile + */ + SipProfile getPeerProfile(); + + /** + * Gets the state of the {@link ISipSession} that carries this call. + * + * @return the session state + */ + SipSessionState getState(); + + /** + * Gets the {@link ISipSession} that carries this call. + * + * @return the session object that carries this call + */ + ISipSession getSipSession(); + + /** + * Enables/disables the ring-back tone. + * + * @param enabled true to enable; false to disable + */ + void setRingbackToneEnabled(boolean enabled); + + /** + * Enables/disables the ring tone. + * + * @param enabled true to enable; false to disable + */ + void setRingtoneEnabled(boolean enabled); +} diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java new file mode 100644 index 0000000..57e0bd2 --- /dev/null +++ b/voip/java/android/net/sip/SipAudioCallImpl.java @@ -0,0 +1,701 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import gov.nist.javax.sdp.fields.SDPKeywords; + +import android.content.Context; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.net.rtp.AudioCodec; +import android.net.rtp.AudioGroup; +import android.net.rtp.AudioStream; +import android.net.rtp.RtpStream; +import android.net.sip.ISipSession; +import android.net.sip.SdpSessionDescription; +import android.net.sip.SessionDescription; +import android.net.sip.SipAudioCall; +import android.net.sip.SipManager; +import android.net.sip.SipProfile; +import android.net.sip.SipSessionAdapter; +import android.net.sip.SipSessionState; +import android.os.Message; +import android.os.RemoteException; +import android.os.Vibrator; +import android.provider.Settings; +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.sdp.SdpException; +import javax.sip.SipException; + +/** + * Class that handles an audio call over SIP. + */ +/** @hide */ +public class SipAudioCallImpl extends SipSessionAdapter + implements SipAudioCall { + private static final String TAG = SipAudioCallImpl.class.getSimpleName(); + private static final boolean RELEASE_SOCKET = true; + private static final boolean DONT_RELEASE_SOCKET = false; + private static final String AUDIO = "audio"; + private static final int DTMF = 101; + + private Context mContext; + private SipProfile mLocalProfile; + private SipAudioCall.Listener mListener; + private ISipSession mSipSession; + private SdpSessionDescription mPeerSd; + + private AudioStream mRtpSession; + private SdpSessionDescription.AudioCodec mCodec; + private long mSessionId = -1L; // SDP session ID + private boolean mInCall = false; + private boolean mMuted = false; + private boolean mHold = false; + + private boolean mRingbackToneEnabled = true; + private boolean mRingtoneEnabled = true; + private Ringtone mRingtone; + private ToneGenerator mRingbackTone; + + private SipProfile mPendingCallRequest; + + public SipAudioCallImpl(Context context, SipProfile localProfile) { + mContext = context; + mLocalProfile = localProfile; + } + + public void setListener(SipAudioCall.Listener listener) { + setListener(listener, false); + } + + public void setListener(SipAudioCall.Listener listener, + boolean callbackImmediately) { + mListener = listener; + if ((listener == null) || !callbackImmediately) return; + try { + SipSessionState state = getState(); + switch (state) { + case READY_TO_CALL: + listener.onReadyToCall(this); + break; + case INCOMING_CALL: + listener.onRinging(this, getPeerProfile(mSipSession)); + startRinging(); + break; + case OUTGOING_CALL: + listener.onCalling(this); + break; + default: + listener.onError(this, "wrong state to attach call: " + state); + } + } catch (Throwable t) { + Log.e(TAG, "setListener()", t); + } + } + + public synchronized boolean isInCall() { + return mInCall; + } + + public synchronized boolean isOnHold() { + return mHold; + } + + public void close() { + close(true); + } + + private synchronized void close(boolean closeRtp) { + if (closeRtp) stopCall(RELEASE_SOCKET); + stopRingbackTone(); + stopRinging(); + mSipSession = null; + mInCall = false; + mHold = false; + mSessionId = -1L; + } + + public synchronized SipProfile getLocalProfile() { + return mLocalProfile; + } + + public synchronized SipProfile getPeerProfile() { + try { + return (mSipSession == null) ? null : mSipSession.getPeerProfile(); + } catch (RemoteException e) { + return null; + } + } + + public synchronized SipSessionState getState() { + if (mSipSession == null) return SipSessionState.READY_TO_CALL; + try { + return Enum.valueOf(SipSessionState.class, mSipSession.getState()); + } catch (RemoteException e) { + return SipSessionState.REMOTE_ERROR; + } + } + + + public synchronized ISipSession getSipSession() { + return mSipSession; + } + + @Override + public void onCalling(ISipSession session) { + Log.d(TAG, "calling... " + session); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCalling(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCalling()", t); + } + } + } + + @Override + public void onRingingBack(ISipSession session) { + Log.d(TAG, "sip call ringing back: " + session); + if (!mInCall) startRingbackTone(); + Listener listener = mListener; + if (listener != null) { + try { + listener.onRingingBack(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onRingingBack()", t); + } + } + } + + @Override + public synchronized void onRinging(ISipSession session, + SipProfile peerProfile, byte[] sessionDescription) { + try { + if ((mSipSession == null) || !mInCall + || !session.getCallId().equals(mSipSession.getCallId())) { + // should not happen + session.endCall(); + return; + } + + // session changing request + try { + mPeerSd = new SdpSessionDescription(sessionDescription); + answerCall(); + } catch (Throwable e) { + Log.e(TAG, "onRinging()", e); + session.endCall(); + } + } catch (RemoteException e) { + Log.e(TAG, "onRinging()", e); + } + } + + private synchronized void establishCall(byte[] sessionDescription) { + stopRingbackTone(); + stopRinging(); + try { + SdpSessionDescription sd = + new SdpSessionDescription(sessionDescription); + Log.d(TAG, "sip call established: " + sd); + startCall(sd); + mInCall = true; + } catch (SdpException e) { + Log.e(TAG, "createSessionDescription()", e); + } + } + + @Override + public void onCallEstablished(ISipSession session, + byte[] sessionDescription) { + establishCall(sessionDescription); + Listener listener = mListener; + if (listener != null) { + try { + if (mHold) { + listener.onCallHeld(SipAudioCallImpl.this); + } else { + listener.onCallEstablished(SipAudioCallImpl.this); + } + } catch (Throwable t) { + Log.e(TAG, "onCallEstablished()", t); + } + } + } + + @Override + public void onCallEnded(ISipSession session) { + Log.d(TAG, "sip call ended: " + session); + close(); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCallEnded(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCallEnded()", t); + } + } + } + + @Override + public void onCallBusy(ISipSession session) { + Log.d(TAG, "sip call busy: " + session); + close(false); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCallBusy(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCallBusy()", t); + } + } + } + + @Override + public void onCallChangeFailed(ISipSession session, + String className, String message) { + Log.d(TAG, "sip call change failed: " + message); + Listener listener = mListener; + if (listener != null) { + try { + listener.onError(SipAudioCallImpl.this, + className + ": " + message); + } catch (Throwable t) { + Log.e(TAG, "onCallBusy()", t); + } + } + } + + @Override + public void onError(ISipSession session, String className, + String message) { + Log.d(TAG, "sip session error: " + className + ": " + message); + synchronized (this) { + if (!isInCall()) close(true); + } + Listener listener = mListener; + if (listener != null) { + try { + listener.onError(SipAudioCallImpl.this, + className + ": " + message); + } catch (Throwable t) { + Log.e(TAG, "onError()", t); + } + } + } + + public synchronized void attachCall(ISipSession session, + SdpSessionDescription sdp) throws SipException { + mSipSession = session; + mPeerSd = sdp; + try { + session.setListener(this); + } catch (Throwable e) { + Log.e(TAG, "attachCall()", e); + throwSipException(e); + } + } + + public synchronized void makeCall(SipProfile peerProfile, + SipManager sipManager) throws SipException { + try { + mSipSession = sipManager.createSipSession(mLocalProfile, this); + if (mSipSession == null) { + throw new SipException( + "Failed to create SipSession; network available?"); + } + mSipSession.makeCall(peerProfile, createOfferSessionDescription()); + } catch (Throwable e) { + if (e instanceof SipException) { + throw (SipException) e; + } else { + throwSipException(e); + } + } + } + + public synchronized void endCall() throws SipException { + try { + stopRinging(); + if (mSipSession != null) mSipSession.endCall(); + stopCall(true); + } catch (Throwable e) { + throwSipException(e); + } + } + + public synchronized void holdCall() throws SipException { + if (mHold) return; + try { + mSipSession.changeCall(createHoldSessionDescription()); + mHold = true; + } catch (Throwable e) { + throwSipException(e); + } + + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_ON_HOLD); + } + + public synchronized void answerCall() throws SipException { + try { + stopRinging(); + mSipSession.answerCall(createAnswerSessionDescription()); + } catch (Throwable e) { + Log.e(TAG, "answerCall()", e); + throwSipException(e); + } + } + + public synchronized void continueCall() throws SipException { + if (!mHold) return; + try { + mHold = false; + mSipSession.changeCall(createContinueSessionDescription()); + } catch (Throwable e) { + throwSipException(e); + } + + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_NORMAL); + } + + private SessionDescription createOfferSessionDescription() { + AudioCodec[] codecs = AudioCodec.getSystemSupportedCodecs(); + return createSdpBuilder(true, convert(codecs)).build(); + } + + private SessionDescription createAnswerSessionDescription() { + try { + // choose an acceptable media from mPeerSd to answer + SdpSessionDescription.AudioCodec codec = getCodec(mPeerSd); + SdpSessionDescription.Builder sdpBuilder = + createSdpBuilder(false, codec); + if (mPeerSd.isSendOnly(AUDIO)) { + sdpBuilder.addMediaAttribute(AUDIO, "recvonly", (String) null); + } else if (mPeerSd.isReceiveOnly(AUDIO)) { + sdpBuilder.addMediaAttribute(AUDIO, "sendonly", (String) null); + } + return sdpBuilder.build(); + } catch (SdpException e) { + throw new RuntimeException(e); + } + } + + private SessionDescription createHoldSessionDescription() { + try { + return createSdpBuilder(false, mCodec) + .addMediaAttribute(AUDIO, "sendonly", (String) null) + .build(); + } catch (SdpException e) { + throw new RuntimeException(e); + } + } + + private SessionDescription createContinueSessionDescription() { + return createSdpBuilder(true, mCodec).build(); + } + + private String getMediaDescription(SdpSessionDescription.AudioCodec codec) { + return String.format("%d %s/%d", codec.payloadType, codec.name, + codec.sampleRate); + } + + private long getSessionId() { + if (mSessionId < 0) { + mSessionId = System.currentTimeMillis(); + } + return mSessionId; + } + + private SdpSessionDescription.Builder createSdpBuilder( + boolean addTelephoneEvent, + SdpSessionDescription.AudioCodec... codecs) { + String localIp = getLocalIp(); + SdpSessionDescription.Builder sdpBuilder; + try { + long sessionVersion = System.currentTimeMillis(); + sdpBuilder = new SdpSessionDescription.Builder("SIP Call") + .setOrigin(mLocalProfile, getSessionId(), sessionVersion, + SDPKeywords.IN, SDPKeywords.IPV4, localIp) + .setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4, + localIp); + List codecIds = new ArrayList(); + for (SdpSessionDescription.AudioCodec codec : codecs) { + codecIds.add(codec.payloadType); + } + if (addTelephoneEvent) codecIds.add(DTMF); + sdpBuilder.addMedia(AUDIO, getLocalMediaPort(), 1, "RTP/AVP", + codecIds.toArray(new Integer[codecIds.size()])); + for (SdpSessionDescription.AudioCodec codec : codecs) { + sdpBuilder.addMediaAttribute(AUDIO, "rtpmap", + getMediaDescription(codec)); + } + if (addTelephoneEvent) { + sdpBuilder.addMediaAttribute(AUDIO, "rtpmap", + DTMF + " telephone-event/8000"); + } + // FIXME: deal with vbr codec + sdpBuilder.addMediaAttribute(AUDIO, "ptime", "20"); + } catch (SdpException e) { + throw new RuntimeException(e); + } + return sdpBuilder; + } + + public synchronized void toggleMute() { + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) { + audioGroup.setMode( + mMuted ? AudioGroup.MODE_NORMAL : AudioGroup.MODE_MUTED); + mMuted = !mMuted; + } + } + + public synchronized boolean isMuted() { + return mMuted; + } + + public synchronized void setInCallMode() { + ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) + .setSpeakerphoneOn(false); + ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) + .setMode(AudioManager.MODE_NORMAL); + } + + public synchronized void setSpeakerMode() { + ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) + .setSpeakerphoneOn(true); + } + + public void sendDtmf(int code) { + sendDtmf(code, null); + } + + public synchronized void sendDtmf(int code, Message result) { + AudioGroup audioGroup = getAudioGroup(); + if ((audioGroup != null) && (mSipSession != null) + && (SipSessionState.IN_CALL == getState())) { + Log.v(TAG, "send DTMF: " + code); + audioGroup.sendDtmf(code); + } + if (result != null) result.sendToTarget(); + } + + public synchronized AudioStream getAudioStream() { + return mRtpSession; + } + + public synchronized AudioGroup getAudioGroup() { + return ((mRtpSession == null) ? null : mRtpSession.getAudioGroup()); + } + + private SdpSessionDescription.AudioCodec getCodec(SdpSessionDescription sd) { + HashMap acceptableCodecs = + new HashMap(); + for (AudioCodec codec : AudioCodec.getSystemSupportedCodecs()) { + acceptableCodecs.put(codec.name, codec); + } + for (SdpSessionDescription.AudioCodec codec : sd.getAudioCodecs()) { + AudioCodec matchedCodec = acceptableCodecs.get(codec.name); + if (matchedCodec != null) return codec; + } + Log.w(TAG, "no common codec is found, use PCM/0"); + return convert(AudioCodec.ULAW); + } + + private AudioCodec convert(SdpSessionDescription.AudioCodec codec) { + AudioCodec c = AudioCodec.getSystemSupportedCodec(codec.name); + return ((c == null) ? AudioCodec.ULAW : c); + } + + private SdpSessionDescription.AudioCodec convert(AudioCodec codec) { + return new SdpSessionDescription.AudioCodec(codec.defaultType, + codec.name, codec.sampleRate, codec.sampleCount); + } + + private SdpSessionDescription.AudioCodec[] convert(AudioCodec[] codecs) { + SdpSessionDescription.AudioCodec[] copies = + new SdpSessionDescription.AudioCodec[codecs.length]; + for (int i = 0, len = codecs.length; i < len; i++) { + copies[i] = convert(codecs[i]); + } + return copies; + } + + private void startCall(SdpSessionDescription peerSd) { + stopCall(DONT_RELEASE_SOCKET); + + mPeerSd = peerSd; + String peerMediaAddress = peerSd.getPeerMediaAddress(AUDIO); + // TODO: handle multiple media fields + int peerMediaPort = peerSd.getPeerMediaPort(AUDIO); + Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort); + + int localPort = getLocalMediaPort(); + int sampleRate = 8000; + int frameSize = sampleRate / 50; // 160 + try { + // TODO: get sample rate from sdp + mCodec = getCodec(peerSd); + + AudioStream audioStream = mRtpSession; + audioStream.associate(InetAddress.getByName(peerMediaAddress), + peerMediaPort); + audioStream.setCodec(convert(mCodec), mCodec.payloadType); + audioStream.setDtmfType(DTMF); + Log.d(TAG, "start media: localPort=" + localPort + ", peer=" + + peerMediaAddress + ":" + peerMediaPort); + + audioStream.setMode(RtpStream.MODE_NORMAL); + if (!mHold) { + // FIXME: won't work if peer is not sending nor receiving + if (!peerSd.isSending(AUDIO)) { + Log.d(TAG, " not receiving"); + audioStream.setMode(RtpStream.MODE_SEND_ONLY); + } + if (!peerSd.isReceiving(AUDIO)) { + Log.d(TAG, " not sending"); + audioStream.setMode(RtpStream.MODE_RECEIVE_ONLY); + } + } + setInCallMode(); + + AudioGroup audioGroup = new AudioGroup(); + audioStream.join(audioGroup); + if (mHold) { + audioGroup.setMode(AudioGroup.MODE_ON_HOLD); + } else if (mMuted) { + audioGroup.setMode(AudioGroup.MODE_MUTED); + } else { + audioGroup.setMode(AudioGroup.MODE_NORMAL); + } + } catch (Exception e) { + Log.e(TAG, "call()", e); + } + } + + private void stopCall(boolean releaseSocket) { + Log.d(TAG, "stop audiocall"); + if (mRtpSession != null) { + mRtpSession.join(null); + + if (releaseSocket) { + mRtpSession.release(); + mRtpSession = null; + } + } + setInCallMode(); + } + + private int getLocalMediaPort() { + if (mRtpSession != null) return mRtpSession.getLocalPort(); + try { + AudioStream s = mRtpSession = + new AudioStream(InetAddress.getByName(getLocalIp())); + return s.getLocalPort(); + } catch (IOException e) { + Log.w(TAG, "getLocalMediaPort(): " + e); + throw new RuntimeException(e); + } + } + + private String getLocalIp() { + try { + return mSipSession.getLocalIp(); + } catch (RemoteException e) { + // FIXME + return "127.0.0.1"; + } + } + + public synchronized void setRingbackToneEnabled(boolean enabled) { + mRingbackToneEnabled = enabled; + } + + public synchronized void setRingtoneEnabled(boolean enabled) { + mRingtoneEnabled = enabled; + } + + private void startRingbackTone() { + if (!mRingbackToneEnabled) return; + if (mRingbackTone == null) { + // The volume relative to other sounds in the stream + int toneVolume = 80; + mRingbackTone = new ToneGenerator( + AudioManager.STREAM_VOICE_CALL, toneVolume); + } + mRingbackTone.startTone(ToneGenerator.TONE_CDMA_LOW_PBX_L); + } + + private void stopRingbackTone() { + if (mRingbackTone != null) { + mRingbackTone.stopTone(); + mRingbackTone.release(); + mRingbackTone = null; + } + } + + private void startRinging() { + if (!mRingtoneEnabled) return; + ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)) + .vibrate(new long[] {0, 1000, 1000}, 1); + AudioManager am = (AudioManager) + mContext.getSystemService(Context.AUDIO_SERVICE); + if (am.getStreamVolume(AudioManager.STREAM_RING) > 0) { + String ringtoneUri = + Settings.System.DEFAULT_RINGTONE_URI.toString(); + mRingtone = RingtoneManager.getRingtone(mContext, + Uri.parse(ringtoneUri)); + mRingtone.play(); + } + } + + private void stopRinging() { + ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)) + .cancel(); + if (mRingtone != null) mRingtone.stop(); + } + + private void throwSipException(Throwable throwable) throws SipException { + if (throwable instanceof SipException) { + throw (SipException) throwable; + } else { + throw new SipException("", throwable); + } + } + + private SipProfile getPeerProfile(ISipSession session) { + try { + return session.getPeerProfile(); + } catch (RemoteException e) { + return null; + } + } +} diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java new file mode 100644 index 0000000..f28b41c --- /dev/null +++ b/voip/java/android/net/sip/SipManager.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; + +import java.text.ParseException; +import javax.sip.SipException; + +/** + * The class provides API for various SIP related tasks. Specifically, the API + * allows the application to: + *
    + *
  • register a {@link SipProfile} to have the background SIP service listen + * to incoming calls and broadcast them with registered command string. See + * {@link #open(SipProfile, String, SipRegistrationListener)}, + * {@link #open(SipProfile)}, {@link #close(String)}, + * {@link #isOpened(String)} and {@link isRegistered(String)}. It also + * facilitates handling of the incoming call broadcast intent. See + * {@link #isIncomingCallIntent(Intent)}, {@link #getCallId(Intent)}, + * {@link #getOfferSessionDescription(Intent)} and + * {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.
  • + *
  • make/take SIP-based audio calls. See + * {@link #makeAudioCall(Context, SipProfile, SipProfile, SipAudioCall.Listener)} + * and {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener}.
  • + *
  • register/unregister with a SIP service provider. See + * {@link #register(SipProfile, int, ISipSessionListener)} and + * {@link #unregister(SipProfile, ISipSessionListener)}.
  • + *
  • process SIP events directly with a {@link ISipSession} created by + * {@link createSipSession(SipProfile, ISipSessionListener)}.
  • + *
+ * @hide + */ +public class SipManager { + /** @hide */ + public static final String SIP_INCOMING_CALL_ACTION = + "com.android.phone.SIP_INCOMING_CALL"; + /** @hide */ + public static final String SIP_ADD_PHONE_ACTION = + "com.android.phone.SIP_ADD_PHONE"; + /** @hide */ + public static final String SIP_REMOVE_PHONE_ACTION = + "com.android.phone.SIP_REMOVE_PHONE"; + /** @hide */ + public static final String LOCAL_URI_KEY = "LOCAL SIPURI"; + + private static final String CALL_ID_KEY = "CallID"; + private static final String OFFER_SD_KEY = "OfferSD"; + + private ISipService mSipService; + + // Will be removed once the SIP service is integrated into framework + private BinderHelper mBinderHelper; + + /** + * Creates a manager instance and initializes the background SIP service. + * Will be removed once the SIP service is integrated into framework. + * + * @param context context to start the SIP service + * @return the manager instance + */ + public static SipManager getInstance(final Context context) { + final SipManager manager = new SipManager(); + manager.createSipService(context); + return manager; + } + + private SipManager() { + } + + private void createSipService(Context context) { + if (mSipService != null) return; + IBinder b = ServiceManager.getService(Context.SIP_SERVICE); + mSipService = ISipService.Stub.asInterface(b); + } + + /** + * Opens the profile for making calls and/or receiving calls. Subsequent + * SIP calls can be made through the default phone UI. The caller may also + * make subsequent calls through + * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}. + * If the receiving-call option is enabled in the profile, the SIP service + * will register the profile to the corresponding server periodically in + * order to receive calls from the server. + * + * @param localProfile the SIP profile to make calls from + * @throws SipException if the profile contains incorrect settings or + * calling the SIP service results in an error + */ + public void open(SipProfile localProfile) throws SipException { + try { + mSipService.open(localProfile); + } catch (RemoteException e) { + throw new SipException("open()", e); + } + } + + /** + * Opens the profile for making calls and/or receiving calls. Subsequent + * SIP calls can be made through the default phone UI. The caller may also + * make subsequent calls through + * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}. + * If the receiving-call option is enabled in the profile, the SIP service + * will register the profile to the corresponding server periodically in + * order to receive calls from the server. + * + * @param localProfile the SIP profile to receive incoming calls for + * @param incomingCallBroadcastAction the action to be broadcast when an + * incoming call is received + * @param listener to listen to registration events; can be null + * @throws SipException if the profile contains incorrect settings or + * calling the SIP service results in an error + */ + public void open(SipProfile localProfile, + String incomingCallBroadcastAction, + SipRegistrationListener listener) throws SipException { + try { + mSipService.open3(localProfile, incomingCallBroadcastAction, + createRelay(listener)); + } catch (RemoteException e) { + throw new SipException("open()", e); + } + } + + /** + * Sets the listener to listen to registration events. No effect if the + * profile has not been opened to receive calls + * (see {@link #open(SipProfile, String, SipRegistrationListener)} and + * {@link #open(SipProfile)}). + * + * @param localProfileUri the URI of the profile + * @param listener to listen to registration events; can be null + * @throws SipException if calling the SIP service results in an error + */ + public void setRegistrationListener(String localProfileUri, + SipRegistrationListener listener) throws SipException { + try { + mSipService.setRegistrationListener( + localProfileUri, createRelay(listener)); + } catch (RemoteException e) { + throw new SipException("setRegistrationListener()", e); + } + } + + /** + * Closes the specified profile to not make/receive calls. All the resources + * that were allocated to the profile are also released. + * + * @param localProfileUri the URI of the profile to close + * @throws SipException if calling the SIP service results in an error + */ + public void close(String localProfileUri) throws SipException { + try { + mSipService.close(localProfileUri); + } catch (RemoteException e) { + throw new SipException("close()", e); + } + } + + /** + * Checks if the specified profile is enabled to receive calls. + * + * @param localProfileUri the URI of the profile in question + * @return true if the profile is enabled to receive calls + * @throws SipException if calling the SIP service results in an error + */ + public boolean isOpened(String localProfileUri) throws SipException { + try { + return mSipService.isOpened(localProfileUri); + } catch (RemoteException e) { + throw new SipException("isOpened()", e); + } + } + + /** + * Checks if the specified profile is registered to the server for + * receiving calls. + * + * @param localProfileUri the URI of the profile in question + * @return true if the profile is registered to the server + * @throws SipException if calling the SIP service results in an error + */ + public boolean isRegistered(String localProfileUri) throws SipException { + try { + return mSipService.isRegistered(localProfileUri); + } catch (RemoteException e) { + throw new SipException("isRegistered()", e); + } + } + + /** + * Creates a {@link SipAudioCall} to make a call. + * + * @param context context to create a {@link SipAudioCall} object + * @param localProfile the SIP profile to make the call from + * @param peerProfile the SIP profile to make the call to + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall makeAudioCall(Context context, SipProfile localProfile, + SipProfile peerProfile, SipAudioCall.Listener listener) + throws SipException { + SipAudioCall call = new SipAudioCallImpl(context, localProfile); + call.setListener(listener); + call.makeCall(peerProfile, this); + return call; + } + + /** + * Creates a {@link SipAudioCall} to make a call. To use this method, one + * must call {@link #open(SipProfile)} first. + * + * @param context context to create a {@link SipAudioCall} object + * @param localProfileUri URI of the SIP profile to make the call from + * @param peerProfileUri URI of the SIP profile to make the call to + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall makeAudioCall(Context context, String localProfileUri, + String peerProfileUri, SipAudioCall.Listener listener) + throws SipException { + try { + return makeAudioCall(context, + new SipProfile.Builder(localProfileUri).build(), + new SipProfile.Builder(peerProfileUri).build(), listener); + } catch (ParseException e) { + throw new SipException("build SipProfile", e); + } + } + + /** + * The method calls {@code takeAudioCall(context, incomingCallIntent, + * listener, true}. + * + * @see #takeAudioCall(Context, Intent, SipAudioCall.Listener, boolean) + */ + public SipAudioCall takeAudioCall(Context context, + Intent incomingCallIntent, SipAudioCall.Listener listener) + throws SipException { + return takeAudioCall(context, incomingCallIntent, listener, true); + } + + /** + * Creates a {@link SipAudioCall} to take an incoming call. Before the call + * is returned, the listener will receive a + * {@link SipAudioCall#Listener.onRinging(SipAudioCall, SipProfile)} + * callback. + * + * @param context context to create a {@link SipAudioCall} object + * @param incomingCallIntent the incoming call broadcast intent + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall takeAudioCall(Context context, + Intent incomingCallIntent, SipAudioCall.Listener listener, + boolean ringtoneEnabled) throws SipException { + if (incomingCallIntent == null) return null; + + String callId = getCallId(incomingCallIntent); + if (callId == null) { + throw new SipException("Call ID missing in incoming call intent"); + } + + byte[] offerSd = getOfferSessionDescription(incomingCallIntent); + if (offerSd == null) { + throw new SipException("Session description missing in incoming " + + "call intent"); + } + + try { + SdpSessionDescription sdp = new SdpSessionDescription(offerSd); + + ISipSession session = mSipService.getPendingSession(callId); + if (session == null) return null; + SipAudioCall call = new SipAudioCallImpl( + context, session.getLocalProfile()); + call.setRingtoneEnabled(ringtoneEnabled); + call.attachCall(session, sdp); + call.setListener(listener); + return call; + } catch (Throwable t) { + throw new SipException("takeAudioCall()", t); + } + } + + /** + * Checks if the intent is an incoming call broadcast intent. + * + * @param intent the intent in question + * @return true if the intent is an incoming call broadcast intent + */ + public static boolean isIncomingCallIntent(Intent intent) { + if (intent == null) return false; + String callId = getCallId(intent); + byte[] offerSd = getOfferSessionDescription(intent); + return ((callId != null) && (offerSd != null)); + } + + /** + * Gets the call ID from the specified incoming call broadcast intent. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the call ID or null if the intent does not contain it + */ + public static String getCallId(Intent incomingCallIntent) { + return incomingCallIntent.getStringExtra(CALL_ID_KEY); + } + + /** + * Gets the offer session description from the specified incoming call + * broadcast intent. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the offer session description or null if the intent does not + * have it + */ + public static byte[] getOfferSessionDescription(Intent incomingCallIntent) { + return incomingCallIntent.getByteArrayExtra(OFFER_SD_KEY); + } + + /** + * Creates an incoming call broadcast intent. + * + * @param action the action string to broadcast + * @param callId the call ID of the incoming call + * @param sessionDescription the session description of the incoming call + * @return the incoming call intent + * @hide + */ + public static Intent createIncomingCallBroadcast(String action, + String callId, byte[] sessionDescription) { + Intent intent = new Intent(action); + intent.putExtra(CALL_ID_KEY, callId); + intent.putExtra(OFFER_SD_KEY, sessionDescription); + return intent; + } + + /** + * Registers the profile to the corresponding server for receiving calls. + * {@link #open(SipProfile, String, SipRegistrationListener)} is still + * needed to be called at least once in order for the SIP service to + * broadcast an intent when an incoming call is received. + * + * @param localProfile the SIP profile to register with + * @param expiryTime registration expiration time (in second) + * @param listener to listen to the registration events + * @throws SipException if calling the SIP service results in an error + */ + public void register(SipProfile localProfile, int expiryTime, + SipRegistrationListener listener) throws SipException { + try { + ISipSession session = mSipService.createSession( + localProfile, createRelay(listener)); + session.register(expiryTime); + } catch (RemoteException e) { + throw new SipException("register()", e); + } + } + + /** + * Unregisters the profile from the corresponding server for not receiving + * further calls. + * + * @param localProfile the SIP profile to register with + * @param listener to listen to the registration events + * @throws SipException if calling the SIP service results in an error + */ + public void unregister(SipProfile localProfile, + SipRegistrationListener listener) throws SipException { + try { + ISipSession session = mSipService.createSession( + localProfile, createRelay(listener)); + session.unregister(); + } catch (RemoteException e) { + throw new SipException("unregister()", e); + } + } + + /** + * Gets the {@link ISipSession} that handles the incoming call. For audio + * calls, consider to use {@link SipAudioCall} to handle the incoming call. + * See {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}. + * Note that the method may be called only once for the same intent. For + * subsequent calls on the same intent, the method returns null. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the session object that handles the incoming call + */ + public ISipSession getSessionFor(Intent incomingCallIntent) + throws SipException { + try { + String callId = getCallId(incomingCallIntent); + return mSipService.getPendingSession(callId); + } catch (RemoteException e) { + throw new SipException("getSessionFor()", e); + } + } + + private static ISipSessionListener createRelay( + SipRegistrationListener listener) { + return ((listener == null) ? null : new ListenerRelay(listener)); + } + + /** + * Creates a {@link ISipSession} with the specified profile. Use other + * methods, if applicable, instead of interacting with {@link ISipSession} + * directly. + * + * @param localProfile the SIP profile the session is associated with + * @param listener to listen to SIP session events + */ + public ISipSession createSipSession(SipProfile localProfile, + ISipSessionListener listener) throws SipException { + try { + return mSipService.createSession(localProfile, listener); + } catch (RemoteException e) { + throw new SipException("createSipSession()", e); + } + } + + /** + * Gets the list of profiles hosted by the SIP service. The user information + * (username, password and display name) are crossed out. + * @hide + */ + public SipProfile[] getListOfProfiles() { + try { + return mSipService.getListOfProfiles(); + } catch (RemoteException e) { + return null; + } + } + + private static class ListenerRelay extends SipSessionAdapter { + private SipRegistrationListener mListener; + + // listener must not be null + public ListenerRelay(SipRegistrationListener listener) { + mListener = listener; + } + + private String getUri(ISipSession session) { + try { + return session.getLocalProfile().getUriString(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onRegistering(ISipSession session) { + mListener.onRegistering(getUri(session)); + } + + @Override + public void onRegistrationDone(ISipSession session, int duration) { + long expiryTime = duration; + if (duration > 0) expiryTime += System.currentTimeMillis(); + mListener.onRegistrationDone(getUri(session), expiryTime); + } + + @Override + public void onRegistrationFailed(ISipSession session, String className, + String message) { + mListener.onRegistrationFailed(getUri(session), className, message); + } + + @Override + public void onRegistrationTimeout(ISipSession session) { + mListener.onRegistrationFailed(getUri(session), + SipException.class.getName(), "registration timed out"); + } + } +} diff --git a/voip/java/android/net/sip/SipProfile.aidl b/voip/java/android/net/sip/SipProfile.aidl new file mode 100644 index 0000000..3b6f68f --- /dev/null +++ b/voip/java/android/net/sip/SipProfile.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +parcelable SipProfile; diff --git a/voip/java/android/net/sip/SipProfile.java b/voip/java/android/net/sip/SipProfile.java new file mode 100644 index 0000000..e71c293 --- /dev/null +++ b/voip/java/android/net/sip/SipProfile.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.io.Serializable; +import java.text.ParseException; +import javax.sip.InvalidArgumentException; +import javax.sip.ListeningPoint; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.AddressFactory; +import javax.sip.address.SipURI; +import javax.sip.address.URI; + +/** + * Class containing a SIP account, domain and server information. + * @hide + */ +public class SipProfile implements Parcelable, Serializable { + private static final long serialVersionUID = 1L; + private static final int DEFAULT_PORT = 5060; + private Address mAddress; + private String mProxyAddress; + private String mPassword; + private String mDomain; + private String mProtocol = ListeningPoint.UDP; + private String mProfileName; + private boolean mSendKeepAlive = false; + private boolean mAutoRegistration = true; + + /** @hide */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SipProfile createFromParcel(Parcel in) { + return new SipProfile(in); + } + + public SipProfile[] newArray(int size) { + return new SipProfile[size]; + } + }; + + /** + * Class to help create a {@link SipProfile}. + */ + public static class Builder { + private AddressFactory mAddressFactory; + private SipProfile mProfile = new SipProfile(); + private SipURI mUri; + private String mDisplayName; + private String mProxyAddress; + + { + try { + mAddressFactory = + SipFactory.getInstance().createAddressFactory(); + } catch (PeerUnavailableException e) { + throw new RuntimeException(e); + } + } + + /** + * Constructor. + * + * @param uriString the URI string as "sip:@" + * @throws ParseException if the string is not a valid URI + */ + public Builder(String uriString) throws ParseException { + if (uriString == null) { + throw new NullPointerException("uriString cannot be null"); + } + URI uri = mAddressFactory.createURI(fix(uriString)); + if (uri instanceof SipURI) { + mUri = (SipURI) uri; + } else { + throw new ParseException(uriString + " is not a SIP URI", 0); + } + mProfile.mDomain = mUri.getHost(); + } + + /** + * Constructor. + * + * @param username username of the SIP account + * @param serverDomain the SIP server domain; if the network address + * is different from the domain, use + * {@link #setOutboundProxy(String)} to set server address + * @throws ParseException if the parameters are not valid + */ + public Builder(String username, String serverDomain) + throws ParseException { + if ((username == null) || (serverDomain == null)) { + throw new NullPointerException( + "username and serverDomain cannot be null"); + } + mUri = mAddressFactory.createSipURI(username, serverDomain); + mProfile.mDomain = serverDomain; + } + + private String fix(String uriString) { + return (uriString.trim().toLowerCase().startsWith("sip:") + ? uriString + : "sip:" + uriString); + } + + /** + * Sets the name of the profile. This name is given by user. + * + * @param name name of the profile + * @return this builder object + */ + public Builder setProfileName(String name) { + mProfile.mProfileName = name; + return this; + } + + /** + * Sets the password of the SIP account + * + * @param password password of the SIP account + * @return this builder object + */ + public Builder setPassword(String password) { + mUri.setUserPassword(password); + return this; + } + + /** + * Sets the port number of the server. By default, it is 5060. + * + * @param port port number of the server + * @return this builder object + * @throws InvalidArgumentException if the port number is out of range + */ + public Builder setPort(int port) throws InvalidArgumentException { + mUri.setPort(port); + return this; + } + + /** + * Sets the protocol used to connect to the SIP server. Currently, + * only "UDP" and "TCP" are supported. + * + * @param protocol the protocol string + * @return this builder object + * @throws InvalidArgumentException if the protocol is not recognized + */ + public Builder setProtocol(String protocol) + throws InvalidArgumentException { + if (protocol == null) { + throw new NullPointerException("protocol cannot be null"); + } + protocol = protocol.toUpperCase(); + if (!protocol.equals("UDP") && !protocol.equals("TCP")) { + throw new InvalidArgumentException( + "unsupported protocol: " + protocol); + } + mProfile.mProtocol = protocol; + return this; + } + + /** + * Sets the outbound proxy of the SIP server. + * + * @param outboundProxy the network address of the outbound proxy + * @return this builder object + */ + public Builder setOutboundProxy(String outboundProxy) { + mProxyAddress = outboundProxy; + return this; + } + + /** + * Sets the display name of the user. + * + * @param displayName display name of the user + * @return this builder object + */ + public Builder setDisplayName(String displayName) { + mDisplayName = displayName; + return this; + } + + /** + * Sets the send keep-alive flag. + * + * @param flag true if sending keep-alive message is required, + * false otherwise + * @return this builder object + */ + public Builder setSendKeepAlive(boolean flag) { + mProfile.mSendKeepAlive = flag; + return this; + } + + + /** + * Sets the auto. registration flag. + * + * @param flag true if the profile will be registered automatically, + * false otherwise + * @return this builder object + */ + public Builder setAutoRegistration(boolean flag) { + mProfile.mAutoRegistration = flag; + return this; + } + + /** + * Builds and returns the SIP profile object. + * + * @return the profile object created + */ + public SipProfile build() { + // remove password from URI + mProfile.mPassword = mUri.getUserPassword(); + mUri.setUserPassword(null); + try { + mProfile.mAddress = mAddressFactory.createAddress( + mDisplayName, mUri); + if (!TextUtils.isEmpty(mProxyAddress)) { + SipURI uri = (SipURI) + mAddressFactory.createURI(fix(mProxyAddress)); + mProfile.mProxyAddress = uri.getHost(); + } + } catch (ParseException e) { + // must not occur + throw new RuntimeException(e); + } + return mProfile; + } + } + + private SipProfile() { + } + + private SipProfile(Parcel in) { + mAddress = (Address) in.readSerializable(); + mProxyAddress = in.readString(); + mPassword = in.readString(); + mDomain = in.readString(); + mProtocol = in.readString(); + mProfileName = in.readString(); + mSendKeepAlive = (in.readInt() == 0) ? false : true; + mAutoRegistration = (in.readInt() == 0) ? false : true; + } + + /** @hide */ + public void writeToParcel(Parcel out, int flags) { + out.writeSerializable(mAddress); + out.writeString(mProxyAddress); + out.writeString(mPassword); + out.writeString(mDomain); + out.writeString(mProtocol); + out.writeString(mProfileName); + out.writeInt(mSendKeepAlive ? 1 : 0); + out.writeInt(mAutoRegistration ? 1 : 0); + } + + /** @hide */ + public int describeContents() { + return 0; + } + + /** + * Gets the SIP URI of this profile. + * + * @return the SIP URI of this profile + */ + public SipURI getUri() { + return (SipURI) mAddress.getURI(); + } + + /** + * Gets the SIP URI string of this profile. + * + * @return the SIP URI string of this profile + */ + public String getUriString() { + return mAddress.getURI().toString(); + } + + /** + * Gets the SIP address of this profile. + * + * @return the SIP address of this profile + */ + public Address getSipAddress() { + return mAddress; + } + + /** + * Gets the display name of the user. + * + * @return the display name of the user + */ + public String getDisplayName() { + return mAddress.getDisplayName(); + } + + /** + * Gets the username. + * + * @return the username + */ + public String getUserName() { + return getUri().getUser(); + } + + /** + * Gets the password. + * + * @return the password + */ + public String getPassword() { + return mPassword; + } + + /** + * Gets the SIP domain. + * + * @return the SIP domain + */ + public String getSipDomain() { + return mDomain; + } + + /** + * Gets the port number of the SIP server. + * + * @return the port number of the SIP server + */ + public int getPort() { + int port = getUri().getPort(); + return (port == -1) ? DEFAULT_PORT : port; + } + + /** + * Gets the protocol used to connect to the server. + * + * @return the protocol + */ + public String getProtocol() { + return mProtocol; + } + + /** + * Gets the network address of the server outbound proxy. + * + * @return the network address of the server outbound proxy + */ + public String getProxyAddress() { + return mProxyAddress; + } + + /** + * Gets the (user-defined) name of the profile. + * + * @return name of the profile + */ + public String getProfileName() { + return mProfileName; + } + + /** + * Gets the flag of 'Sending keep-alive'. + * + * @return the flag of sending SIP keep-alive messages. + */ + public boolean getSendKeepAlive() { + return mSendKeepAlive; + } + + /** + * Gets the flag of 'Auto Registration'. + * + * @return the flag of registering the profile automatically. + */ + public boolean getAutoRegistration() { + return mAutoRegistration; + } +} diff --git a/voip/java/android/net/sip/SipRegistrationListener.java b/voip/java/android/net/sip/SipRegistrationListener.java new file mode 100644 index 0000000..63faaf8 --- /dev/null +++ b/voip/java/android/net/sip/SipRegistrationListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Listener class to listen to SIP registration events. + * @hide + */ +public interface SipRegistrationListener { + /** + * Called when a registration request is sent. + * + * @param localProfileUri the URI string of the SIP profile to register with + */ + void onRegistering(String localProfileUri); + + /** + * Called when registration is successfully done. + * + * @param localProfileUri the URI string of the SIP profile to register with + * @param expiryTime duration in second before the registration expires + */ + void onRegistrationDone(String localProfileUri, long expiryTime); + + /** + * Called when the registration fails. + * + * @param localProfileUri the URI string of the SIP profile to register with + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onRegistrationFailed(String localProfileUri, String errorClass, + String errorMessage); +} diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java new file mode 100644 index 0000000..cfb71d7 --- /dev/null +++ b/voip/java/android/net/sip/SipSessionAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Adapter class for {@link ISipSessionListener}. Default implementation of all + * callback methods is no-op. + * @hide + */ +public class SipSessionAdapter extends ISipSessionListener.Stub { + public void onCalling(ISipSession session) { + } + + public void onRinging(ISipSession session, SipProfile caller, + byte[] sessionDescription) { + } + + public void onRingingBack(ISipSession session) { + } + + public void onCallEstablished( + ISipSession session, byte[] sessionDescription) { + } + + public void onCallEnded(ISipSession session) { + } + + public void onCallBusy(ISipSession session) { + } + + public void onCallChanged(ISipSession session, byte[] sessionDescription) { + } + + public void onCallChangeFailed(ISipSession session, String className, + String message) { + } + + public void onError(ISipSession session, String className, String message) { + } + + public void onRegistering(ISipSession session) { + } + + public void onRegistrationDone(ISipSession session, int duration) { + } + + public void onRegistrationFailed(ISipSession session, String className, + String message) { + } + + public void onRegistrationTimeout(ISipSession session) { + } +} diff --git a/voip/java/android/net/sip/SipSessionState.java b/voip/java/android/net/sip/SipSessionState.java new file mode 100644 index 0000000..5bab112 --- /dev/null +++ b/voip/java/android/net/sip/SipSessionState.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Defines {@link ISipSession} states. + * @hide + */ +public enum SipSessionState { + /** When session is ready to initiate a call or transaction. */ + READY_TO_CALL, + + /** When the registration request is sent out. */ + REGISTERING, + + /** When the unregistration request is sent out. */ + DEREGISTERING, + + /** When an INVITE request is received. */ + INCOMING_CALL, + + /** When an OK response is sent for the INVITE request received. */ + INCOMING_CALL_ANSWERING, + + /** When an INVITE request is sent. */ + OUTGOING_CALL, + + /** When a RINGING response is received for the INVITE request sent. */ + OUTGOING_CALL_RING_BACK, + + /** When a CANCEL request is sent for the INVITE request sent. */ + OUTGOING_CALL_CANCELING, + + /** When a call is established. */ + IN_CALL, + + /** Some error occurs when making a remote call to {@link ISipSession}. */ + REMOTE_ERROR, + + /** When an OPTIONS request is sent. */ + PINGING; + + /** + * Checks if the specified string represents the same state as this object. + * + * @return true if the specified string represents the same state as this + * object + */ + public boolean equals(String state) { + return toString().equals(state); + } +} -- cgit v1.1