概説SipDemo(Android2.3 SIP APIを試す)

[,w400,h300]
[Nexus SからHTC Desireに電話してる様子]

Android2.3(gingerbread)で追加されたAPIの一つであるSIP APIを試してみました。
Android SDK 2.3に同梱されているSipDemoというプロジェクトを使ってどの様な手順でSIP通話を実現しているか解説します。

実施環境

発信側:
  端末:Nexus S
  アプリ:SipDemo
着信側:
  端末:HTC Desire (android2.2)
  アプリ:Sipdroid version 2.0.1 beta

準備

まずSIPで通話する為にSIPアカウントを取得しました。Android2.3ではSIP通話の為の各種APIを提供してくれますが、SIPアカウントやSIPプロバイダまでは提供してくれません。いっそGoogleSIPプロバイダ買収して統合しちゃってくれるとうれしいんですが・・・。
今回はiptel.orgという無料のSIPプロバイダを利用しました。
ここで発信、着信用の二つのアカウントを作成しました。iptel.orgのページの下部の「Subscribe!」から新規アカウントの作成ができます。

通話してみる

とりあえず通話をしてみます。
SipDemoをビルドしてNexus Sにインストールし、起動すると以下の様な画面が現れます。
[,w320,h480]

自分のSIPアカウントを設定していない場合は以下のダイアログが表示されます。
OKを選択するとアカウント設定画面が開きます。
[,w320,h480]

アカウント設定画面で、SIPアカウント、パスワード、SIPプロバイダのドメインを設定します。
[,w320,h480]

設定が完了すると、SIPプロバイダにREGISTERを試みます。成功すると、「Ready」の文字列が表示されます。
[,w320,h480]

「Ready」の状態でメニューを表示し、「Call someone」を選択します。
[,w320,h480]

発信する相手先を入力するダイアログが表示されます。
ここに相手のSIPURI(例:moge@sip.hoge.jp)を入力し、「OK」ボタンを押下すると相手にINVAITEします。
[,w320,h480]

通話に成功した場合、マイクをタッチしている間、自分の声を相手に送信します。

[,w320,h480]


SipDemoおよびAPIの説明

■SipDemoの場所
 「Android SDK Samples for Android API 9, revision 1」がインストールされていれば下記の場所にプロジェクトがあるはずです。
    /samples/android-9/SipDemo


■SipDemoの構成
 SipDemoのファイル構成です。XMLファイル類は特に気にするようなものは無かったので割愛します。
  IncomingCallReceiver.java //着信を受けるBroadcastReceiver
  SipSettings.java //SIPプロファイルの設定を行うPreferenceActivity
  WalkieTalkieActivity.java //SIPプロファイルの初期化、REGISTER、発信等を行うActivity


■AndroidManifest.xml
 AndroidでSIPを利用する場合、AndroidManifest.xmlに以下のuses-permissionを追加してあげる必要があります。
  android.permission.USE_SIP
  android.permission.INTERNET
  android.permission.ACCESS_WIFI_STATE
  android.permission.WAKE_LOCK
  android.permission.RECORD_AUDIO
  android.permission.MODIFY_AUDIO_SETTINGS

 SipDemoではこのうち「android.permission.MODIFY_AUDIO_SETTINGS」の設定が抜けていますので追記してやる必要があります。「android.permission.MODIFY_AUDIO_SETTINGS」が無くても動作しますが、音声設定周りが無効になってしまいます。


■利用するクラス
 SIP通話を行うために利用するクラスです。以下のクラスを使うだけでSIP通話を実現する事ができます。
  android.net.sip.SipManager       //SIPプロバイダへのREGISTERや発信、着信を管理するクラス
  android.net.sip.SipProfile        //自分のSIPプロファイル
  android.net.sip.SipRegistrationListener //SIPプロバイダにREGISTERする際のリスナ
  android.net.sip.SipAudioCall      //SIP通話の音声を管理する
  android.net.sip.SipAudioCall.Listener  //SIP通話の確立や切断、エラー等のリスナ
  android.net.sip.SipException      //SIP周りの例外


発信までの手順

 発信の処理はWalkieTalkieActivity.javaで行っています。

  1. SipManagerを作成する
  2. SipProfileを作成する
  3. SipManager#open(String, SipRegistrationListener)でSIPプロバイダにREGISTERする
  4. SipManager#makeAudioCall(String ,String, SipAudioCall.Listener, int)で相手先に電話をかける
  5. SipAudioCall.Listener#onCallEstablished(SipAudioCall)で音声通話を開始する
1.SipManagerを作成する

 SipManagerはWalkieTalkieActivity.javaの106行目、initializeManager()で作成しています。
 単純にメンバ変数managerがnullの場合SipManager#newInstance(Context)を呼び出しているだけです。

public void initializeManager() {
     if(manager == null) {
          manager = SipManager.newInstance(this);
     }
     initializeLocalProfile();
}
2.SipProfileを作成する

 WalkieTalkieActivity.javaの138行目、initializeLocalProfile()内でSipProfileを作成しています。
 SipProfileの作成はSipProfile.Builderクラスを使って行います。コンストラクタにSIPプロバイダで取得したアカウント名、SIPプロバイダのドメイン名を設定します。(例:hoge@sip.moge.jpの場合、「hoge」がアカウント名で「sip.moge.jp」がドメイン名となります。)
 BuilderをnewしたあとsetPassword(String)でアカウントのパスワードをセットします。最後にSipProfile.Builder#build()を呼び出しSipProfileを作成します。

SipProfile.Builder builder = new SipProfile.Builder(username, domain);
builder.setPassword(password);
me = builder.build();          //meはメンバ変数。SipProfile型
3.SipManager#open(SipProfile, String, SipRegistrationListener)でSIPプロバイダにREGISTERする

 WalkieTalkieActivity.javaの142行目、initializeLocalProfile()内でSipProfileを作成した後、SipManager#openでSIPプロバイダに対してREGISTERを行います。
 openする際にSipRegistrationListenerでREGISTERの結果を受け取ります。何故かリスナのセットはSipManager#openを呼び出した後にしろ、との事。
 SipRegistrationListenerは以下の3つのコールバックを持ちます。

  • public void onRegistering(String localProfileUri)                       //REGISTER中
  • public void onRegistrationDone(String localProfileUri, long expiryTime)            //REGISTER成功
  • public void onRegistrationFailed(String localProfileUri, int errorCode, String errorMessage)  //REGISTER失敗

 SipDemoではこれらのコールバック時に、画面に各種状態の文字列を表示させています。
 SipManager#openをする際にPendingIntentをセットしていますが、これは着信の為に利用します。説明は後ほどします。

Intent i = new Intent();i.setAction("android.SipDemo.INCOMING_CALL");
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA);
manager.open(me, pi, null);


// This listener must be added AFTER manager.open is called,
// Otherwise the methods aren't guaranteed to fire.

manager.setRegistrationListener(me.getUriString(), new SipRegistrationListener() {
          public void onRegistering(String localProfileUri) {
               updateStatus("Registering with SIP Server...");
          }

          public void onRegistrationDone(String localProfileUri, long expiryTime) {
               updateStatus("Ready");
          }

          public void onRegistrationFailed(String localProfileUri, int errorCode,
                    String errorMessage) {
               updateStatus("Registration failed.  Please check settings.");
          }
     });
4.SipManager#makeAudioCall(String ,String, SipAudioCall.Listener, int)で相手先に電話をかける

 WalkieTalkieActivity.javaの197行目、initiateCall()内でSipManager#makeAudioCall(String, String, SipAudioCall.Listener, int)を呼び出します。引数は自分のSIPURI, 相手のSIPURI,SipAudioCall.Listener,タイムアウト秒です。
 SipAudioCall.Listenerはリスナという名前のくせにクラスです。コールバックの種類が結構多いのでinterfaceだと煩雑だと考えたのでしょうか?必要なコールバックだけオーバーライドして利用します。SipDemoでは相手が着信後200 OKを返し、通信が確立された際に呼び出されるonCallEstablished(SipAudioCall)と、通話を切断した場合に呼び出されるonCallEnded(SipAudioCall)をオーバーライドしています。

SipAudioCall.Listener listener = new SipAudioCall.Listener() {
     // Much of the client's interaction with the SIP Stack will
     // happen via listeners.  Even making an outgoing call, don't
     // forget to set up a listener to set things up once the call is established.
     @Override
     public void onCallEstablished(SipAudioCall call) {
          call.startAudio();
          call.setSpeakerMode(true);
          call.toggleMute();
          updateStatus(call);
     }

     @Override
     public void onCallEnded(SipAudioCall call) {
          updateStatus("Ready.");
     }
};

call = manager.makeAudioCall(me.getUriString(), sipAddress, listener, 30);
5.SipAudioCall.Listener#onCallEstablished(SipAudioCall)で音声通話を開始する

 WalkieTalkieActivity.javaの202行目、SipAudioCall.Listener#onCallEstablished(SipAudioCall)でSipAudioCall#startAudio()を呼び出し音声通話を開始します。
 SipAudioCall#setSpeakerMode(boolean)にtrueをセットしています。これでハンズオンの状態で通話が開始されます。また、SipAudipCall#toggleMute()を呼び出しています。これで自分の音声はミュートされます。このミュートはタッチ処理の中で「タッチされている間だけオン」といった感じで実装されています。普通の電話の様な動作にしたい場合はcall.toggleMute()の行とタッチイベントの処理を削除するだけで実現できます。

     @Override
     public void onCallEstablished(SipAudioCall call) {
          call.startAudio();
          call.setSpeakerMode(true);
          call.toggleMute();
          updateStatus(call);
     }

着信-通話までの手順

 着信はIncomingCallReceiver.javaで行います。IncomingCallReceiverはBroadcastReceiverを継承しており、WalkieTalkieActivityが実行されている間だけ着信を受けます。

  1. IncomingCallReceiverをシステムに登録する
  2. SipManagerに着信時に実行されるPendingIntentを登録する
  3. SipManagerを作成する
  4. SipProfileを作成する
  5. SipManager#open(String, SipRegistrationListener)でSIPプロバイダにREGISTERする
  6. IncomingCallReceiver#onReceive(Context, Intent)で通話を開始する
1.IncomingCallReceiverをシステムに登録する

 WalkieTalkieActivity.javaの69行目、onCreate(Bundle)内でIncomingCallReceiverをシステムに登録します。
 IntentFilterには"android.SipDemo.INCOMING_CALL"を指定しています。"android.SipDemo.INCOMING_CALL"のアクションを持つIntentがBroadcastされた場合に受け取る、という事です。

// Set up the intent filter.  This will be used to fire an
// IncomingCallReceiver when someone calls the SIP address used by this
// application.
IntentFilter filter = new IntentFilter();
filter.addAction("android.SipDemo.INCOMING_CALL");
callReceiver = new IncomingCallReceiver();
this.registerReceiver(callReceiver, filter);
2.SipManagerに着信時に実行されるPendingIntentを登録する

 WalkieTalkieActivity.javaの142行目、initializeLocalProfile()内でPendingIntentを作成し、SipManager#openで登録しています。このPendingIntentは着信があった場合に実行されます。
 SipDemoでは"android.SipDemo.INCOMING_CALL"のアクションをBroadcastするPendingIntentを登録しています。このアクションはIncomingCallReceiverをシステムに登録する際に指定したIntentFilterと一致します。

Intent i = new Intent();i.setAction("android.SipDemo.INCOMING_CALL");
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA);
manager.open(me, pi, null);
3.SipManagerを作成する

発信と同じなので省略

4.SipProfileを作成する

発信と同じなので省略

5.SipManager#open(String, SipRegistrationListener)でSIPプロバイダにREGISTERする

発信と同じなので省略

6.IncomingCallReceiver#onReceive(Context, Intent)で通話を開始する

 IncomingCallReceiver.javaの37行目,onReceive(Context, Intent)で着信の処理を行っています。
ContextをWalkieTalkieActivityにキャストし、SipManager#takeAudioCall(Intent, SipAudioCall.Listener)を呼び出しています。SipManager#takeAudioCallを実行したら、次にSipAudioCall.Listener#onRinging(SipAudioCall, SipProfile)が呼び出されます。おそらく内部で100 TRYING、180 RINGINGを発信側に返してonRinging(SipAudioCall, SipProfile)を呼び出しているのだと思います。ここでSipAudioCall#answerCall(int)を呼び出します。javadocを読んでもいまいち動作が不明ですが、200 OKを返しているのでは無いかと思います。onRingingの後にincamingCall.answerCall(30)を実行しています。未確認ですが、この二回目のanswerCallは不要なんじゃないかなーと思います。
 その後SipAudioCall#startAudio()で通話を開始しています。
 wtActivity.call = incomingCall;でWalkieTalkieActivityのメンバ変数にSipAudioCallを設定していますが、WalkieTalkieActivityを終了したり、メニューの「End Current Call」を選択した時にSipAudioCall#endCall()を実行させる為です。

@Override
public void onReceive(Context context, Intent intent) {
     SipAudioCall incomingCall = null;
     try {

          SipAudioCall.Listener listener = new SipAudioCall.Listener() {
               @Override
               public void onRinging(SipAudioCall call, SipProfile caller) {
                    try {
                         call.answerCall(30);
                    } catch (Exception e) {
                         e.printStackTrace();
                    }
               }
          };

          WalkieTalkieActivity wtActivity = (WalkieTalkieActivity) context;

          incomingCall = wtActivity.manager.takeAudioCall(intent, listener);
          incomingCall.answerCall(30);
          incomingCall.startAudio();
          incomingCall.setSpeakerMode(true);
          if(incomingCall.isMuted()) {
               incomingCall.toggleMute();
          }

          wtActivity.call = incomingCall;

          wtActivity.updateStatus(incomingCall);

     } catch (Exception e) {
          if (incomingCall != null) {
               incomingCall.close();
          }
     }
}

最後に

SIPに関する面倒なほとんどの処理をSIP APIが抽象化してくれました。
上記で説明した手順だけでSIP通話が行えます。SIP APIを使って、例えばAccountManagerに登録されているGMailのアドレスから自動的にSIPアカウント払い出しを行ってすぐ通話できるアプリ、とか割と簡単に作成できそうですね。

文責:技術部 八木 俊広 (新人/Androidアプリケーション技術者認定試験ベーシック合格(笑)/品川)