1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.sip;
18
19import android.app.PendingIntent;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.os.IBinder;
24import android.os.Looper;
25import android.os.RemoteException;
26import android.os.ServiceManager;
27import android.util.Log;
28
29import java.text.ParseException;
30
31/**
32 * Provides APIs for SIP tasks, such as initiating SIP connections, and provides access to related
33 * SIP services. This class is the starting point for any SIP actions. You can acquire an instance
34 * of it with {@link #newInstance newInstance()}.</p>
35 * <p>The APIs in this class allows you to:</p>
36 * <ul>
37 * <li>Create a {@link SipSession} to get ready for making calls or listen for incoming calls. See
38 * {@link #createSipSession createSipSession()} and {@link #getSessionFor getSessionFor()}.</li>
39 * <li>Initiate and receive generic SIP calls or audio-only SIP calls. Generic SIP calls may
40 * be video, audio, or other, and are initiated with {@link #open open()}. Audio-only SIP calls
41 * should be handled with a {@link SipAudioCall}, which you can acquire with {@link
42 * #makeAudioCall makeAudioCall()} and {@link #takeAudioCall takeAudioCall()}.</li>
43 * <li>Register and unregister with a SIP service provider, with
44 *      {@link #register register()} and {@link #unregister unregister()}.</li>
45 * <li>Verify session connectivity, with {@link #isOpened isOpened()} and
46 *      {@link #isRegistered isRegistered()}.</li>
47 * </ul>
48 * <p class="note"><strong>Note:</strong> Not all Android-powered devices support VOIP calls using
49 * SIP. You should always call {@link android.net.sip.SipManager#isVoipSupported
50 * isVoipSupported()} to verify that the device supports VOIP calling and {@link
51 * android.net.sip.SipManager#isApiSupported isApiSupported()} to verify that the device supports
52 * the SIP APIs. Your application must also request the {@link
53 * android.Manifest.permission#INTERNET} and {@link android.Manifest.permission#USE_SIP}
54 * permissions.</p>
55 *
56 * <div class="special reference">
57 * <h3>Developer Guides</h3>
58 * <p>For more information about using SIP, read the
59 * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
60 * developer guide.</p>
61 * </div>
62 */
63public class SipManager {
64    /**
65     * The result code to be sent back with the incoming call
66     * {@link PendingIntent}.
67     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
68     */
69    public static final int INCOMING_CALL_RESULT_CODE = 101;
70
71    /**
72     * Key to retrieve the call ID from an incoming call intent.
73     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
74     */
75    public static final String EXTRA_CALL_ID = "android:sipCallID";
76
77    /**
78     * Key to retrieve the offered session description from an incoming call
79     * intent.
80     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
81     */
82    public static final String EXTRA_OFFER_SD = "android:sipOfferSD";
83
84    /**
85     * Action to broadcast when SipService is up.
86     * Internal use only.
87     * @hide
88     */
89    public static final String ACTION_SIP_SERVICE_UP =
90            "android.net.sip.SIP_SERVICE_UP";
91    /**
92     * Action string for the incoming call intent for the Phone app.
93     * Internal use only.
94     * @hide
95     */
96    public static final String ACTION_SIP_INCOMING_CALL =
97            "com.android.phone.SIP_INCOMING_CALL";
98    /**
99     * Action string for the add-phone intent.
100     * Internal use only.
101     * @hide
102     */
103    public static final String ACTION_SIP_ADD_PHONE =
104            "com.android.phone.SIP_ADD_PHONE";
105    /**
106     * Action string for the remove-phone intent.
107     * Internal use only.
108     * @hide
109     */
110    public static final String ACTION_SIP_REMOVE_PHONE =
111            "com.android.phone.SIP_REMOVE_PHONE";
112    /**
113     * Part of the ACTION_SIP_ADD_PHONE and ACTION_SIP_REMOVE_PHONE intents.
114     * Internal use only.
115     * @hide
116     */
117    public static final String EXTRA_LOCAL_URI = "android:localSipUri";
118
119    private static final String TAG = "SipManager";
120
121    private ISipService mSipService;
122    private Context mContext;
123
124    /**
125     * Creates a manager instance. Returns null if SIP API is not supported.
126     *
127     * @param context application context for creating the manager object
128     * @return the manager instance or null if SIP API is not supported
129     */
130    public static SipManager newInstance(Context context) {
131        return (isApiSupported(context) ? new SipManager(context) : null);
132    }
133
134    /**
135     * Returns true if the SIP API is supported by the system.
136     */
137    public static boolean isApiSupported(Context context) {
138        return context.getPackageManager().hasSystemFeature(
139                PackageManager.FEATURE_SIP);
140    }
141
142    /**
143     * Returns true if the system supports SIP-based VOIP API.
144     */
145    public static boolean isVoipSupported(Context context) {
146        return context.getPackageManager().hasSystemFeature(
147                PackageManager.FEATURE_SIP_VOIP) && isApiSupported(context);
148    }
149
150    /**
151     * Returns true if SIP is only available on WIFI.
152     */
153    public static boolean isSipWifiOnly(Context context) {
154        return context.getResources().getBoolean(
155                com.android.internal.R.bool.config_sip_wifi_only);
156    }
157
158    private SipManager(Context context) {
159        mContext = context;
160        createSipService();
161    }
162
163    private void createSipService() {
164        IBinder b = ServiceManager.getService(Context.SIP_SERVICE);
165        mSipService = ISipService.Stub.asInterface(b);
166    }
167
168    /**
169     * Opens the profile for making generic SIP calls. The caller may make subsequent calls
170     * through {@link #makeAudioCall}. If one also wants to receive calls on the
171     * profile, use
172     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)}
173     * instead.
174     *
175     * @param localProfile the SIP profile to make calls from
176     * @throws SipException if the profile contains incorrect settings or
177     *      calling the SIP service results in an error
178     */
179    public void open(SipProfile localProfile) throws SipException {
180        try {
181            mSipService.open(localProfile);
182        } catch (RemoteException e) {
183            throw new SipException("open()", e);
184        }
185    }
186
187    /**
188     * Opens the profile for making calls and/or receiving generic SIP calls. The caller may
189     * make subsequent calls through {@link #makeAudioCall}. If the
190     * auto-registration option is enabled in the profile, the SIP service
191     * will register the profile to the corresponding SIP provider periodically
192     * in order to receive calls from the provider. When the SIP service
193     * receives a new call, it will send out an intent with the provided action
194     * string. The intent contains a call ID extra and an offer session
195     * description string extra. Use {@link #getCallId} and
196     * {@link #getOfferSessionDescription} to retrieve those extras.
197     *
198     * @param localProfile the SIP profile to receive incoming calls for
199     * @param incomingCallPendingIntent When an incoming call is received, the
200     *      SIP service will call
201     *      {@link PendingIntent#send(Context, int, Intent)} to send back the
202     *      intent to the caller with {@link #INCOMING_CALL_RESULT_CODE} as the
203     *      result code and the intent to fill in the call ID and session
204     *      description information. It cannot be null.
205     * @param listener to listen to registration events; can be null
206     * @see #getCallId
207     * @see #getOfferSessionDescription
208     * @see #takeAudioCall
209     * @throws NullPointerException if {@code incomingCallPendingIntent} is null
210     * @throws SipException if the profile contains incorrect settings or
211     *      calling the SIP service results in an error
212     * @see #isIncomingCallIntent
213     * @see #getCallId
214     * @see #getOfferSessionDescription
215     */
216    public void open(SipProfile localProfile,
217            PendingIntent incomingCallPendingIntent,
218            SipRegistrationListener listener) throws SipException {
219        if (incomingCallPendingIntent == null) {
220            throw new NullPointerException(
221                    "incomingCallPendingIntent cannot be null");
222        }
223        try {
224            mSipService.open3(localProfile, incomingCallPendingIntent,
225                    createRelay(listener, localProfile.getUriString()));
226        } catch (RemoteException e) {
227            throw new SipException("open()", e);
228        }
229    }
230
231    /**
232     * Sets the listener to listen to registration events. No effect if the
233     * profile has not been opened to receive calls (see
234     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)}).
235     *
236     * @param localProfileUri the URI of the profile
237     * @param listener to listen to registration events; can be null
238     * @throws SipException if calling the SIP service results in an error
239     */
240    public void setRegistrationListener(String localProfileUri,
241            SipRegistrationListener listener) throws SipException {
242        try {
243            mSipService.setRegistrationListener(
244                    localProfileUri, createRelay(listener, localProfileUri));
245        } catch (RemoteException e) {
246            throw new SipException("setRegistrationListener()", e);
247        }
248    }
249
250    /**
251     * Closes the specified profile to not make/receive calls. All the resources
252     * that were allocated to the profile are also released.
253     *
254     * @param localProfileUri the URI of the profile to close
255     * @throws SipException if calling the SIP service results in an error
256     */
257    public void close(String localProfileUri) throws SipException {
258        try {
259            mSipService.close(localProfileUri);
260        } catch (RemoteException e) {
261            throw new SipException("close()", e);
262        }
263    }
264
265    /**
266     * Checks if the specified profile is opened in the SIP service for
267     * making and/or receiving calls.
268     *
269     * @param localProfileUri the URI of the profile in question
270     * @return true if the profile is enabled to receive calls
271     * @throws SipException if calling the SIP service results in an error
272     */
273    public boolean isOpened(String localProfileUri) throws SipException {
274        try {
275            return mSipService.isOpened(localProfileUri);
276        } catch (RemoteException e) {
277            throw new SipException("isOpened()", e);
278        }
279    }
280
281    /**
282     * Checks if the SIP service has successfully registered the profile to the
283     * SIP provider (specified in the profile) for receiving calls. Returning
284     * true from this method also implies the profile is opened
285     * ({@link #isOpened}).
286     *
287     * @param localProfileUri the URI of the profile in question
288     * @return true if the profile is registered to the SIP provider; false if
289     *        the profile has not been opened in the SIP service or the SIP
290     *        service has not yet successfully registered the profile to the SIP
291     *        provider
292     * @throws SipException if calling the SIP service results in an error
293     */
294    public boolean isRegistered(String localProfileUri) throws SipException {
295        try {
296            return mSipService.isRegistered(localProfileUri);
297        } catch (RemoteException e) {
298            throw new SipException("isRegistered()", e);
299        }
300    }
301
302    /**
303     * Creates a {@link SipAudioCall} to make a call. The attempt will be timed
304     * out if the call is not established within {@code timeout} seconds and
305     * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
306     * will be called.
307     *
308     * @param localProfile the SIP profile to make the call from
309     * @param peerProfile the SIP profile to make the call to
310     * @param listener to listen to the call events from {@link SipAudioCall};
311     *      can be null
312     * @param timeout the timeout value in seconds. Default value (defined by
313     *        SIP protocol) is used if {@code timeout} is zero or negative.
314     * @return a {@link SipAudioCall} object
315     * @throws SipException if calling the SIP service results in an error or
316     *      VOIP API is not supported by the device
317     * @see SipAudioCall.Listener#onError
318     * @see #isVoipSupported
319     */
320    public SipAudioCall makeAudioCall(SipProfile localProfile,
321            SipProfile peerProfile, SipAudioCall.Listener listener, int timeout)
322            throws SipException {
323        if (!isVoipSupported(mContext)) {
324            throw new SipException("VOIP API is not supported");
325        }
326        SipAudioCall call = new SipAudioCall(mContext, localProfile);
327        call.setListener(listener);
328        SipSession s = createSipSession(localProfile, null);
329        call.makeCall(peerProfile, s, timeout);
330        return call;
331    }
332
333    /**
334     * Creates a {@link SipAudioCall} to make an audio call. The attempt will be
335     * timed out if the call is not established within {@code timeout} seconds
336     * and
337     * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
338     * will be called.
339     *
340     * @param localProfileUri URI of the SIP profile to make the call from
341     * @param peerProfileUri URI of the SIP profile to make the call to
342     * @param listener to listen to the call events from {@link SipAudioCall};
343     *      can be null
344     * @param timeout the timeout value in seconds. Default value (defined by
345     *        SIP protocol) is used if {@code timeout} is zero or negative.
346     * @return a {@link SipAudioCall} object
347     * @throws SipException if calling the SIP service results in an error or
348     *      VOIP API is not supported by the device
349     * @see SipAudioCall.Listener#onError
350     * @see #isVoipSupported
351     */
352    public SipAudioCall makeAudioCall(String localProfileUri,
353            String peerProfileUri, SipAudioCall.Listener listener, int timeout)
354            throws SipException {
355        if (!isVoipSupported(mContext)) {
356            throw new SipException("VOIP API is not supported");
357        }
358        try {
359            return makeAudioCall(
360                    new SipProfile.Builder(localProfileUri).build(),
361                    new SipProfile.Builder(peerProfileUri).build(), listener,
362                    timeout);
363        } catch (ParseException e) {
364            throw new SipException("build SipProfile", e);
365        }
366    }
367
368    /**
369     * Creates a {@link SipAudioCall} to take an incoming call. Before the call
370     * is returned, the listener will receive a
371     * {@link SipAudioCall.Listener#onRinging}
372     * callback.
373     *
374     * @param incomingCallIntent the incoming call broadcast intent
375     * @param listener to listen to the call events from {@link SipAudioCall};
376     *      can be null
377     * @return a {@link SipAudioCall} object
378     * @throws SipException if calling the SIP service results in an error
379     */
380    public SipAudioCall takeAudioCall(Intent incomingCallIntent,
381            SipAudioCall.Listener listener) throws SipException {
382        if (incomingCallIntent == null) {
383            throw new SipException("Cannot retrieve session with null intent");
384        }
385
386        String callId = getCallId(incomingCallIntent);
387        if (callId == null) {
388            throw new SipException("Call ID missing in incoming call intent");
389        }
390
391        String offerSd = getOfferSessionDescription(incomingCallIntent);
392        if (offerSd == null) {
393            throw new SipException("Session description missing in incoming "
394                    + "call intent");
395        }
396
397        try {
398            ISipSession session = mSipService.getPendingSession(callId);
399            if (session == null) {
400                throw new SipException("No pending session for the call");
401            }
402            SipAudioCall call = new SipAudioCall(
403                    mContext, session.getLocalProfile());
404            call.attachCall(new SipSession(session), offerSd);
405            call.setListener(listener);
406            return call;
407        } catch (Throwable t) {
408            throw new SipException("takeAudioCall()", t);
409        }
410    }
411
412    /**
413     * Checks if the intent is an incoming call broadcast intent.
414     *
415     * @param intent the intent in question
416     * @return true if the intent is an incoming call broadcast intent
417     */
418    public static boolean isIncomingCallIntent(Intent intent) {
419        if (intent == null) return false;
420        String callId = getCallId(intent);
421        String offerSd = getOfferSessionDescription(intent);
422        return ((callId != null) && (offerSd != null));
423    }
424
425    /**
426     * Gets the call ID from the specified incoming call broadcast intent.
427     *
428     * @param incomingCallIntent the incoming call broadcast intent
429     * @return the call ID or null if the intent does not contain it
430     */
431    public static String getCallId(Intent incomingCallIntent) {
432        return incomingCallIntent.getStringExtra(EXTRA_CALL_ID);
433    }
434
435    /**
436     * Gets the offer session description from the specified incoming call
437     * broadcast intent.
438     *
439     * @param incomingCallIntent the incoming call broadcast intent
440     * @return the offer session description or null if the intent does not
441     *      have it
442     */
443    public static String getOfferSessionDescription(Intent incomingCallIntent) {
444        return incomingCallIntent.getStringExtra(EXTRA_OFFER_SD);
445    }
446
447    /**
448     * Creates an incoming call broadcast intent.
449     *
450     * @param callId the call ID of the incoming call
451     * @param sessionDescription the session description of the incoming call
452     * @return the incoming call intent
453     * @hide
454     */
455    public static Intent createIncomingCallBroadcast(String callId,
456            String sessionDescription) {
457        Intent intent = new Intent();
458        intent.putExtra(EXTRA_CALL_ID, callId);
459        intent.putExtra(EXTRA_OFFER_SD, sessionDescription);
460        return intent;
461    }
462
463    /**
464     * Manually registers the profile to the corresponding SIP provider for
465     * receiving calls.
466     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)} is
467     * still needed to be called at least once in order for the SIP service to
468     * notify the caller with the {@link android.app.PendingIntent} when an incoming call is
469     * received.
470     *
471     * @param localProfile the SIP profile to register with
472     * @param expiryTime registration expiration time (in seconds)
473     * @param listener to listen to the registration events
474     * @throws SipException if calling the SIP service results in an error
475     */
476    public void register(SipProfile localProfile, int expiryTime,
477            SipRegistrationListener listener) throws SipException {
478        try {
479            ISipSession session = mSipService.createSession(localProfile,
480                    createRelay(listener, localProfile.getUriString()));
481            if (session == null) {
482                throw new SipException(
483                        "SipService.createSession() returns null");
484            }
485            session.register(expiryTime);
486        } catch (RemoteException e) {
487            throw new SipException("register()", e);
488        }
489    }
490
491    /**
492     * Manually unregisters the profile from the corresponding SIP provider for
493     * stop receiving further calls. This may interference with the auto
494     * registration process in the SIP service if the auto-registration option
495     * in the profile is enabled.
496     *
497     * @param localProfile the SIP profile to register with
498     * @param listener to listen to the registration events
499     * @throws SipException if calling the SIP service results in an error
500     */
501    public void unregister(SipProfile localProfile,
502            SipRegistrationListener listener) throws SipException {
503        try {
504            ISipSession session = mSipService.createSession(localProfile,
505                    createRelay(listener, localProfile.getUriString()));
506            if (session == null) {
507                throw new SipException(
508                        "SipService.createSession() returns null");
509            }
510            session.unregister();
511        } catch (RemoteException e) {
512            throw new SipException("unregister()", e);
513        }
514    }
515
516    /**
517     * Gets the {@link SipSession} that handles the incoming call. For audio
518     * calls, consider to use {@link SipAudioCall} to handle the incoming call.
519     * See {@link #takeAudioCall}. Note that the method may be called only once
520     * for the same intent. For subsequent calls on the same intent, the method
521     * returns null.
522     *
523     * @param incomingCallIntent the incoming call broadcast intent
524     * @return the session object that handles the incoming call
525     */
526    public SipSession getSessionFor(Intent incomingCallIntent)
527            throws SipException {
528        try {
529            String callId = getCallId(incomingCallIntent);
530            ISipSession s = mSipService.getPendingSession(callId);
531            return ((s == null) ? null : new SipSession(s));
532        } catch (RemoteException e) {
533            throw new SipException("getSessionFor()", e);
534        }
535    }
536
537    private static ISipSessionListener createRelay(
538            SipRegistrationListener listener, String uri) {
539        return ((listener == null) ? null : new ListenerRelay(listener, uri));
540    }
541
542    /**
543     * Creates a {@link SipSession} with the specified profile. Use other
544     * methods, if applicable, instead of interacting with {@link SipSession}
545     * directly.
546     *
547     * @param localProfile the SIP profile the session is associated with
548     * @param listener to listen to SIP session events
549     */
550    public SipSession createSipSession(SipProfile localProfile,
551            SipSession.Listener listener) throws SipException {
552        try {
553            ISipSession s = mSipService.createSession(localProfile, null);
554            if (s == null) {
555                throw new SipException(
556                        "Failed to create SipSession; network unavailable?");
557            }
558            return new SipSession(s, listener);
559        } catch (RemoteException e) {
560            throw new SipException("createSipSession()", e);
561        }
562    }
563
564    /**
565     * Gets the list of profiles hosted by the SIP service. The user information
566     * (username, password and display name) are crossed out.
567     * @hide
568     */
569    public SipProfile[] getListOfProfiles() {
570        try {
571            return mSipService.getListOfProfiles();
572        } catch (RemoteException e) {
573            return new SipProfile[0];
574        }
575    }
576
577    private static class ListenerRelay extends SipSessionAdapter {
578        private SipRegistrationListener mListener;
579        private String mUri;
580
581        // listener must not be null
582        public ListenerRelay(SipRegistrationListener listener, String uri) {
583            mListener = listener;
584            mUri = uri;
585        }
586
587        private String getUri(ISipSession session) {
588            try {
589                return ((session == null)
590                        ? mUri
591                        : session.getLocalProfile().getUriString());
592            } catch (Throwable e) {
593                // SipService died? SIP stack died?
594                Log.w(TAG, "getUri(): " + e);
595                return null;
596            }
597        }
598
599        @Override
600        public void onRegistering(ISipSession session) {
601            mListener.onRegistering(getUri(session));
602        }
603
604        @Override
605        public void onRegistrationDone(ISipSession session, int duration) {
606            long expiryTime = duration;
607            if (duration > 0) expiryTime += System.currentTimeMillis();
608            mListener.onRegistrationDone(getUri(session), expiryTime);
609        }
610
611        @Override
612        public void onRegistrationFailed(ISipSession session, int errorCode,
613                String message) {
614            mListener.onRegistrationFailed(getUri(session), errorCode, message);
615        }
616
617        @Override
618        public void onRegistrationTimeout(ISipSession session) {
619            mListener.onRegistrationFailed(getUri(session),
620                    SipErrorCode.TIME_OUT, "registration timed out");
621        }
622    }
623}
624