Core-Telecom

Core-Telecom ライブラリは、堅牢で一貫性のある API セットを提供することで、通話アプリを Android プラットフォームに統合するプロセスを効率化します。

実際の実装を確認する場合は、GitHub でサンプル アプリケーションをご覧ください。

Core-Telecom を設定する

アプリの build.gradle ファイルに androidx.core:core-telecom 依存関係を追加します。

dependencies {
    implementation ("androidx.core:core-telecom:1.0.0")
}

AndroidManifest.xmlMANAGE_OWN_CALLS 権限を宣言します。

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

アプリの登録

CallsManager を使用して Android に通話アプリを登録し、システムへの通話の追加を開始します。登録時に、アプリの機能(音声、動画のサポートなど)を指定します。

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

通話管理

Core-Telecom API を使用して、通話のライフサイクルを作成、管理します。

通話を作成する

CallAttributesCompat オブジェクトは、一意の呼び出しのプロパティを定義します。このプロパティには次の特性があります。

  • displayName: 呼び出し元の名前。
  • address: 通話アドレス(電話番号、会議リンクなど)。
  • direction: 着信または発信。
  • callType: 音声または動画。
  • callCapabilities: 転送と保留をサポートします。

着信を作成する方法の例を次に示します。

fun createIncomingCallAttributes(
    callerName: String,
    callerNumber: String,
    isVideoCall: Boolean): CallAttributesCompat {
    val addressUri = Uri.parse("YourAppScheme:$callerNumber")

    // Define capabilities supported by your call.
    val callCapabilities = CallAttributesCompat.CallCapability(
        supportsSetInactive = CallAttributesCompat.SUPPORTS_SET_INACTIVE // Call can be made inactive (implies hold)
    )

    return CallAttributesCompat(
        displayName = callerName,
        address = addressUri,
        direction = CallAttributesCompat.DIRECTION_INCOMING,
        callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
        callCapabilitiesCompat = callCapabilities
    )
}

通話を追加する

CallAttributesCompat とコールバックで callsManager.addCall を使用して、システムに新しい通話を追加し、リモート サーフェスの更新を管理します。addCall ブロック内の callControlScope は、主にアプリが通話状態を移行して音声の更新を受信できるようにします。

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onAnswerCall, // Watch needs to know if it can answer the call.
        onSetCallDisconnected,
        onSetCallActive,
        onSetCallInactive
    ) {
        // The call was successfully added once this scope runs.
        callControlScope = this
    }
}
catch(addCallException: Exception){
   // Handle the addCall failure.
}

着信に応答する

CallControlScope 内で着信に応答します。

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

通話を拒否する

CallControlScope 内で DisconnectCause.REJECTED を使用して disconnect() で通話を拒否します。

disconnect(DisconnectCause(DisconnectCause.REJECTED))

発信中の通話をアクティブにする

相手が応答したら、発信通話をアクティブに設定します。

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

通話を保留にする

setInactive() を使用して通話を保留にします。

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

通話を切る

DisconnectCause を使用して disconnect() で通話を切断します。

disconnect(DisconnectCause(DisconnectCause.LOCAL))

通話音声エンドポイントを管理する

CallControlScope 内の currentCallEndpointavailableEndpointsisMuted Flow を使用して、音声エンドポイントをモニタリングして管理する

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

requestEndpointChange() を使用してアクティブなオーディオ機器を変更します。

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

フォアグラウンドのサポート

このライブラリは、フォアグラウンド サポートに ConnectionService(Android 13 API レベル 33 以前)または foregroundtypes(Android 14 API レベル 34 以降)を使用します。

フォアグラウンドの要件の一環として、アプリがフォアグラウンドで実行されていることをユーザーに知らせる通知を投稿する必要があります。

アプリがフォアグラウンド実行の優先度を取得できるようにするには、プラットフォームで呼び出しを追加したら、通知を作成します。アプリが通話を終了したとき、または通知が無効になったときに、フォアグラウンドの優先度が削除されます。

詳しくは、フォアグラウンド サービスについての記事をご覧ください。

リモート Surface サポート

リモート デバイス(スマートウォッチ、Bluetooth ヘッドセット、Android Auto)は、スマートフォンを直接操作しなくても通話管理が可能です。アプリは、これらのデバイスによって開始されたアクションを処理するために、CallsManager.addCall に提供されるコールバック ラムダ(onAnswerCallonSetCallDisconnectedonSetCallActiveonSetCallInactive)を実装する必要があります。

リモート アクションが発生すると、対応するラムダが呼び出されます。

ラムダが正常に完了すると、コマンドが処理されたことが示されます。コマンドを実行できない場合、ラムダは例外をスローします。

適切に実装することで、さまざまなデバイスでシームレスな通話制御が可能になります。さまざまなリモコンの表面で徹底的にテストします。

電話番号表示オプション

このライブラリは、通話の状態と音声ルートの管理に加えて、通話拡張機能もサポートしています。通話拡張機能は、Android Auto などのリモート サーフェスでより豊かな通話体験を実現するためにアプリで実装できるオプション機能です。会議室、通話のミュート、通話アイコンの追加などの機能があります。アプリが拡張機能を実装すると、アプリが提供する情報は、UI でこれらの拡張機能の表示をサポートするすべての接続デバイスと同期されます。つまり、これらの機能はリモート デバイスでも利用可能になり、ユーザーが操作できるようになります。

拡張機能を使用して通話を作成する

通話を作成する際に、CallManager#addCall を使用して通話を作成する代わりに、CallManager#addCallWithExtensions を使用できます。これにより、アプリは ExtensionInitializationScope という別のスコープにアクセスできるようになります。このスコープにより、アプリケーションはサポートするオプションの拡張機能のセットを初期化できます。また、このスコープには、拡張機能の capability 交換と初期化が完了した後にアプリに CallControlScope を提供する onCall という追加メソッドがあります。

scope.launch {
    mCallsManager.addCallWithExtensions(
        attributes,
        onAnswer,
        onDisconnect,
        onSetActive,
        onSetInactive
    ) {
        // Initialize extension-specific code...

        // After the call has been initialized, perform in-call actions
        onCall {
            // Example: process call state updates
            callStateFlow.onEach { newState ->
                // handle call state updates and notify telecom
            }.launchIn(this)

            // Use initialized extensions...
        }
    }
}

サポート通話の参加者

アプリが会議やグループ通話の通話参加者をサポートしている場合は、addParticipantExtension を使用してこの拡張機能のサポートを宣言し、関連する API を使用して参加者が変更されたときにリモート サーフェスを更新します。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial participants state in the call.
        val participantExtension = addParticipantExtension(
            initialParticipants,
            initialActiveParticipant
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
        }
    }

ParticipantExtension#updateActiveParticipant を使用して、通話中の参加者をリモート サーフェスに通知するとともに、アクティブな参加者を更新することもできます。

通話の参加者に関連するオプションのアクションもサポートしています。アプリは ParticipantExtension#addRaiseHandSupport を使用して、通話中に参加者が挙手したことをサポートし、他の参加者が挙手しているかどうかを確認できます。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial list of participants in the call.
        val participantExtension = addParticipantExtension(initialParticipants)
        // Notifies Jetpack that this app supports the notion of participants
        // being able to raise and lower their hands.
        val raiseHandState = participantExtension.addRaiseHandSupport(
                initialRaisedHands
            ) { onHandRaisedStateChanged ->
                // handle this user's raised hand state changed updates from
                // remote surfaces.
            }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
            // notify remote surfaces of which of the participants have their
            // hands raised
            raisedHandsFlow.onEach { newRaisedHands ->
                raiseHandState.updateRaisedHands(newRaisedHands)
            }.launchIn(this)
        }
    }

サポート通話のミュート

通話のミュートを使用すると、デバイスのマイクを物理的にミュートすることなく、通話の発信音をミュートするようアプリにリクエストできます。この機能は通話ごとに管理されるため、Jetpack は、VOIP 通話がアクティブな間、進行中の携帯電話通話のグローバル ミュート状態を管理する複雑さを処理します。これにより、複数通話のシナリオで発信音声のミュートがエラーになりにくくなり、通話のミュートが有効になっていることにユーザーが気づかずに発信している場合に「発信中ですか?」などの便利な機能も利用できます。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for locally silencing the call's outgoing audio and
        // register a handler for when the user changes the call silence state
        // from a remote surface.
        val callSilenceExtension = addLocalCallSilenceExtension(
            initialCallSilenceState = false
        ) { newCallSilenceStateRequest ->
            // handle the user's request to enable/disable call silence from
            // a remote surface
        }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's call silence state changes, update remote
            // surfaces of the new state.
            callSilenceState.onEach { isSilenced ->
                callSilenceExtension.updateIsLocallySilenced(isSilenced)
            }.launchIn(this)
        }
    }

サポート通話アイコン

通話アイコンを使用すると、アプリは通話中にリモート サーフェスに表示される通話を表すカスタム アイコンを指定できます。このアイコンは、通話のライフサイクル中に更新することもできます。

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for a custom call icon to be displayed during the
        // lifetime of the call.
        val callIconExtension = addCallIconExtension(
            initialCallIconUri = initialUri
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's icon changes, update remote surfaces by providing
            // the new URI.
            callIconUri.onEach { newIconUri ->
                callIconExtension.updateCallIconUri(newIconUri)
            }.launchIn(this)
        }
    }

システム通話履歴との統合

Core-Telecom を使用すると、アプリをシステムの通話履歴と統合できます。この機能により、VoIP アプリケーションを使用して発信または受信した通話がシステム ダイヤラーの通話履歴に表示され、ユーザーはダイヤラーを使用してコールバックを開始できます。

通話履歴の統合を有効にする

この機能を有効にするには、アプリケーションが AndroidManifest.xmlTelecomManager.ACTION_CALL_BACK インテントを処理するように登録し、コールバックを処理する Activity を提供する必要があります。たとえば、コールバックを処理するアクティビティを VoipCallbackActivity とすると、マニフェストは次のようになります。

マニフェストでインテントを登録する

<activity
    android:name=".VoipCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.action.CALL_BACK" />
    </intent-filter>
</activity>

コールバック インテントを処理する

ユーザーがシステム ダイヤルからコールバックを開始すると、登録されたアクティビティが TelecomManager.ACTION_CALL_BACK アクションで起動されます。インテントには、CallsManager を使用して通話を追加したときにアプリに提供された TelecomManager.EXTRA_UUID が含まれています。

CallControlScope メソッド getCallId は、この呼び出しの一意の ID を返します。

コールバックがトリガーされたときに通話を再構築して再度発信できるように、UUID で識別される通話の開始に必要なすべての情報(グループ通話の電話番号のリストなど)をアプリ内に保存する必要があります。

class VoipCallbackActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (intent?.action == TelecomManager.ACTION_CALL_BACK) {
            val uuidString = intent.getStringExtra(TelecomManager.EXTRA_UUID)
            if (uuidString != null) {
                val uuid = UUID.fromString(uuidString)
                
                // Retrieve your persisted call info using the UUID
                val callData = CallRepository.getCallByUuid(uuid)
                
                if (callData != null) {
                    // Place the call again using your app's logic
                    // ...
                }
            }
        }
        finish()
    }
}

通話履歴の統合をオプトアウトする

デフォルトでは、通話はシステム通話履歴に記録されます。CallAttributesCompat の作成時に isLogged パラメータを false に設定すると、この機能を呼び出しごとに無効にできます。

val callAttributes = CallAttributesCompat(
    displayName = callerName,
    address = addressUri,
    direction = CallAttributesCompat.DIRECTION_OUTGOING,
    isLogExcluded = true // Opt-out of system call logging
)