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