2012/03/15

Android:音声発信までのシーケンスと音声発信イベントのフック

音声発信時はやや複雑なシーケンスを経て行われます。
また、3rdパーティアプリが音声発信シーケンスに介入する余地も残されています。

今回は音声発信シーケンスを見ていきます。

音声発信時の大まかな処理は下記の流れになります。

android.intent.action.CALLが音声発信を開始する直接のIntentではないことがわかります。
音声発信には通常呼以外にSIP, OTAやvoicemail、緊急呼といった様々な種類があります。
Androidは3rdパーティに緊急呼発信を許可していませんが、通常呼は許可しています。
これらの仕様を満たすために音声発信シーケンスはやや複雑化しています。

今回は音声発信シーケンスの中でも"通常呼発信"をメインに扱います。
次回は"緊急呼発信"をメインに扱います。
いずれもIntent,BroadcastIntentやHandlerを巧みに利用したテクニックです。

# ソースコードの抜粋は重要箇所のみ抜き出しています
# コードはver.4.0.3ベースです。

●音声発信の開始
ダイヤル系アプリがaction.CALLで音声発信するとOutgoingCallBroadcasterが反応します。
その名の通り音声発信ブロードキャストを送信するための透明なActivityです。
OutgoingCallBroadcasterにはアクティビティ別名が存在します。
下記定義となります(一部省略)。

・com.android.phone AndroidManifest.xml
<activity android:name="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PHONE"
        android:theme="@android:style/Theme.NoDisplay">
    <!-- CALL action intent filters, for the various ways
         of initiating an outgoing call. -->
    <intent-filter>
        <action android:name="android.intent.action.CALL" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity>

<activity-alias android:name="EmergencyOutgoingCallBroadcaster"
        android:targetActivity="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PRIVILEGED"
        android:theme="@android:style/Theme.NoDisplay">
    <intent-filter>
        <action android:name="android.intent.action.CALL_EMERGENCY" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity-alias>

<activity-alias android:name="PrivilegedOutgoingCallBroadcaster"
        android:targetActivity="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PRIVILEGED">
    <intent-filter>
        <action android:name="android.intent.action.CALL_PRIVILEGED" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity-alias>

それぞれのActivityは
  • OutgoingCallBroadcaster : 通常呼
  • EmergencyOutgoingCallBroadcaster : 緊急呼
  • PrivilegedOutgoingCallBroadcaster : 上記2つのハイブリッド
となります。3rdパーティアプリは緊急呼を発信できないのでOutgoingCallBroadcaster
以外気にする必要はないでしょう。

OutgoingCallBroadcasterの処理を追っていきます。

・com.android.phone OutgoingCallBroadcaster.java
protected void onCreate(Bundle icicle) {
    // Intentから電話番号取得
    String number = PhoneNumberUtils.getNumberFromIntent(intent, this);

    // SIPや緊急呼の判定を経てのBroadcastIntent作成処理
    Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
    if (number != null) {
        broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
    }
    PhoneUtils.checkAndCopyPhoneProviderExtras(intent, broadcastIntent);
    broadcastIntent.putExtra(EXTRA_ALREADY_CALLED, callNow);
    broadcastIntent.putExtra(EXTRA_ORIGINAL_URI, uri.toString());
    if (DBG) Log.v(TAG, "Broadcasting intent: " + broadcastIntent + ".");
    sendOrderedBroadcast(broadcastIntent, PERMISSION, new OutgoingCallReceiver(),
                         null,  // scheduler
                         Activity.RESULT_OK,  // initialCode
                         number,  // initialData: initial value for the result data
                         null);  // initialExtras
}

送信されるブロードキャストはandroid.permission.PROCESS_OUTGOING_CALLSパーミッシ
ョン付きです。
これにより、ブロードキャスト受信側はパーミッションを宣言する必要があります。

resultDataには電話番号が格納されています。
ブロードキャスト種別はオーダーで、全てのレシーバが電話番号を処理した結果を
OutgoingCallReceiverが受け取る仕組みです。

ここで、ACTION_NEW_OUTGOING_CALLのブロードキャストIntentを投げることで、
3rdパーティアプリが音声発信をフックすることを可能にしています。

ブロードキャストの結果はOutgoingCallReceiverに届きます。
OutgoingCallReceiverの処理を追っていきます。

・com.android.phone OutgoingCallBroadcaster$OutgoingCallReceiver.java
public void doReceive(Context context, Intent intent) {
    // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData
    // is used as the actual number to call. (If null, no call will be
    // placed.)
    number = getResultData();

    if (number == null) {
        if (DBG) Log.v(TAG, "CALL cancelled (null number), returning...");
        return;
    }

    startSipCallOptionHandler(context, intent, uri, number);
}

ブロードキャストの結果を受信すると、ユーザが電話番号を編集している可能性がある
ため、緊急呼判定が再度行われます。
通常呼と判断されればSipCallOptionHandlerを呼び出し、処理を継続します。

・com.android.phone OutgoingCallBroadcaster.java
private void startSipCallOptionHandler(Context context, Intent intent,
            Uri uri, String number) {
    Intent selectPhoneIntent = new Intent(ACTION_SIP_SELECT_PHONE, uri);
    selectPhoneIntent.setClass(context, SipCallOptionHandler.class);
    selectPhoneIntent.putExtra(EXTRA_NEW_CALL_INTENT, newIntent);
    selectPhoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if (DBG) Log.v(TAG, "startSipCallOptionHandler(): " +
            "calling startActivity: " + selectPhoneIntent);
    context.startActivity(selectPhoneIntent);
    // ...and see SipCallOptionHandler.onCreate() for the next step of the sequence.
}

SipCallOptionHandlerはSIP通話の判定処理を行いますが、通常呼として処理すべき場合
は、そのまま通常呼処理を継続します。

#Android2.3以降よりSIP通話が標準サポートされました。

・com.android.phone SipCallOptionHandler.java
public void onCreate(Bundle savedInstanceState) {
    // SIP通話判定を経て...
    setResultAndFinish();
}

private void setResultAndFinish() {
    runOnUiThread(new Runnable() {
        public void run() {
            if (mUseSipPhone && mOutgoingSipProfile == null) {
                // SIP通話時処理
                return;
            } else {
                // Woo hoo -- it's finally OK to initiate the outgoing call!
                PhoneApp.getInstance().callController.placeCall(mIntent);
            }
            finish();
        }
    }
}

placeCallまでたどり着きました。これでようやく音声発信処理が開始されます。
(かなりのコードを省略しました。"Woo hoo"のコメントをみても発信処理チェックの
長いことがわかります...)

次に、placeCallの中を追っていきます。

・com.android.phone CallController.java
public void placeCall(Intent intent) {
    // 様々な状態チェックや前準備を経て、RILへ発信要求
    CallStatusCode status = placeCallInternal(intent);

    // 音声発信画面を表示
    mApp.displayCallScreen();
}

・com.android.phone PhoneApp.java
void displayCallScreen() {
    startActivity(createInCallIntent());
}

/* package */ static Intent createInCallIntent() {
    Intent intent = new Intent(Intent.ACTION_MAIN, null);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
            | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
            | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
    intent.setClassName("com.android.phone", getCallScreenClassName());
    return intent;
}

placeCallInternal(intent)はPhoneUtils.placeCall()を呼び出し、最終的にRILへ発信
要求を送信します。
発信要求を送信した後は、音声発信中であることをユーザへ伝えるためInCallScreen
アクティビティ(getCallScreenClassNameの戻り値)を起動してUIの表示に至ります。

3rdパーティアプリが音声発信シーケンスで関与するのは
android.intent.action.CALLとandroid.intent.action.NEW_OUTGOING_CALLでしょう。

繰り返しとなりますが、
3rdパーティアプリは緊急呼を発信することができませんが、
NEW_OUTGOING_CALLのブロードキャストインテントを受信することで、音声発信シーケンス
に介入することができます。


音声発信シーケンスを解析すると、
  • ブロードキャストによる他アプリ連携と拡張性の確保。
  • Intentによるモジュール間の連携
を垣間見ることができました。

コードを見るとわかりますが、3rdパーティアプリがNEW_OUTGOING_CALLを受信する場合は
細心の注意が必要です。
間違ってResultDataに意図しない値(nullや全く異なる電話番号)を代入してしまうと、
全く音声発信できない、あるいは予期しない電話番号に発信されてしまうことになります。
# Broadcastの性質上、ユーザはどのアプリが悪さをしているのか判断するのが困難です

OutgoingCallBroadcasterには、音声発信に関する様々な仕様がコードコメントとして記載
されています。
気になる方には一見の価値があります。


次回は、今回省略した"緊急呼"についてです。

緊急呼は極めて特殊な存在です。
この番号が発信されないという事態は、生命の危険に関わることなので絶対に避ける必要
があります。
そのため、緊急呼発信シーケンスは通常呼発信シーケンスとは異なるルートを通り、
あらゆる手段を使って発信を試みるように作られています。

3rdパーティアプリが緊急呼に関わることはほとんどないため、仕様に関する知識はそ
れほど役に立たないかもしれませんが、「あらゆる手段を使って発信を試みる」のロジッ
クは一見の価値があります。

以上です。