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