AgoraSDKを使ってAndroidの音声通話アプリを作ってみる

# Agoraとは
リアルタイムの音声・ビデオ通話やライブストリーミング機能を提供するライブラリです。
Agora SDKをアプリに組み込むことで簡単に音声と映像の送受信が可能となります。

https://www.agora.io/en/

# 音声通話ができるようになるまでの流れ
1. Agora Project作成
1. Agora SDKの依存関係を追加
1. マイク等の必要なPermissionの許可設定をする
1. Rtc Engineを生成
1. Rtc Channelに参加
1. 音声の送受信が可能となる

# 事前準備
### 1. Agora ConsoleでProject作成
①アカウント作成
②Create Project

https://console.agora.io/projects

![スクリーンショット 2025-02-12 22.17.11.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979045/c0f8e47f-e837-47ec-a61b-f4deaa208716.jpeg)

### 2. 依存関係を追加
SDKは2025/2月時点で最新の4.5.0を使用

https://central.sonatype.com/artifact/io.agora.rtc/full-sdk/4.5.0/dependencies

```libs.versions.toml
[versions]
agora = "4.5.0"

[libraries]
agora = { module = "io.agora.rtc:full-sdk", version.ref = "agora" }
```
```build.gradle
implementation(libs.agora)
```

### 3. 必要なPermissionを追加
下記をマニフェストファイルに追加します。(ドキュメントからの引用です)
```Xml
<!--Required permissions-->
<uses-permission android:name="android.permission.INTERNET"/>

<!--Optional permissions-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- For devices running Android 12 (API level 32) or higher and integrating Agora Voice SDK version v4.1.0 or lower, you also need to add the following permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- For Android 12 or higher, the following permissions are also required -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
```

# 音声通話機能を実装する
`App Id`と`RTC Token`が必要になるのでAgora ConsoleのProjectページで確認する。

```Kotlin
/**
 * Agora Projectの情報
 */
object AgoraConfig {
    const val APP_ID = "xxx" // Agora Consoleで作成したProjectのApp Idを使用する
    const val RTC_TOKEN = "xxx" // Agora Consoleで作成したProjectのRTC Tokenを使用する
    const val CHANNEL_NAME = "DemoChannel"
}
```
Agoraを利用した音声通話のロジックを実装するInterface。
```Kotlin
import android.content.Context
import android.util.Log
import io.agora.rtc2.ChannelMediaOptions
import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtc2.RtcEngineConfig

/**
 * Agora のAPIクライアント
 */
interface AgoraService {
    fun initializeRtcEngine(context: Context)
    fun joinChannel()
    fun leaveChannel()
}

class  AgoraServiceImpl: AgoraService {
    companion object {
        private const val TAG = "AgoraServiceImpl"
    }

    var mRtcEngine: RtcEngine? = null

    /**
     * Rtc Engine を生成
     */
    override fun initializeRtcEngine(context: Context) {
        // 古いRtcEngineの影響を受けないようにするため初期化前に解放する
        RtcEngine.destroy()

        // RtcEngineConfigを生成
        val rtcConfig = RtcEngineConfig().apply {
            mAppId = AgoraConfig.APP_ID
            mContext = context
            mEventHandler = object : IRtcEngineEventHandler() {
                // Channelへの参加が成功した契機で通知されるコールバック
                override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
                    super.onJoinChannelSuccess(channel, uid, elapsed)
                    println("onJoinChannelSuccess")
                }

                // 他のユーザーがChannelへ参加した契機で通知されるコールバック
                override fun onUserJoined(uid: Int, elapsed: Int) {
                    super.onUserJoined(uid, elapsed)
                    println("onUserJoined")
                }
            }
        }

        // RtcEngineを生成
        try {
            mRtcEngine = RtcEngine.create(rtcConfig)
        } catch (e: Exception) {
            Log.e(TAG, "creating rtc engine is fail.")
        }
    }

    /**
     * Rtc Channelに参加
     */
    override fun joinChannel() {
        if (mRtcEngine == null) {
            Log.e(TAG, "joinChannel, mRtcEngine is null")
            return
        }

        // ChannelMediaOptionsを生成
        val channelMediaOptions = ChannelMediaOptions().apply {
            clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
            channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION
            publishMicrophoneTrack = true;
        }

        // Channelに参加
        mRtcEngine?.joinChannel(
            AgoraConfig.RTC_TOKEN,
            AgoraConfig.CHANNEL_NAME,
            0,
            channelMediaOptions
        )
    }

    /**
     * Channelから退出
     * RtcEngineインスタンスを解放
     */
    override fun leaveChannel() {
        mRtcEngine?.leaveChannel()
        mRtcEngine = null
    }
}
```
今回は音声通話ができさえすればいいので、画面はMainActivityのみ。
MainActivityでは音声通話に必要な端末の許可設定とAgoraServiceのAPI呼び出しを実装する。
```Kotlin
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.Manifest.permission.CAMERA
import android.Manifest.permission.READ_PHONE_STATE
import android.Manifest.permission.RECORD_AUDIO
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.agora.rtc2.RtcEngine

class MainActivity : ComponentActivity() {
    companion object {
        private const val TAG = "MainActivity"
    }

    // AgoraのAPIクライアント
    private val agoraService = AgoraServiceImpl()

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

        /**
         * 通話に必要なPremissionが許可されているかどうかチェックする
         * 
         * 許可されている -> 音声通話を開始
         * 許可されていない -> Permissionを許可してもらうようにリクエスト
         */
        if (isGrantedSelfPermission(getRequiredPermissions())) {
            startVoiceCalling()
        } else {
            ActivityCompat.requestPermissions(this, getRequiredPermissions(), 22)
        }
    }

    /**
     * Activityが破棄される契機でChannelからの退出とRtcEngineを解放する
     */
    override fun onDestroy() {
        super.onDestroy()
        agoraService.leaveChannel()
        RtcEngine.destroy()
    }

    /**
     * ActivityCompat.requestPermissions()が呼ばれ後に通知されるコールバック
     * Permissionの許可設定を再度チェックする
     */
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
        deviceId: Int
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults, deviceId)

        if (isGrantedSelfPermission(getRequiredPermissions())) {
            startVoiceCalling()
        }
    }

    /**
     * 音声通話を開始
     */
    private fun startVoiceCalling() {
        agoraService.initializeRtcEngine(applicationContext)
        agoraService.joinChannel()
    }

    /**
     * ユーザーの許可設定が必要なPermissionを取得
     */
    private fun getRequiredPermissions(): Array<String> {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(
                RECORD_AUDIO,
                BLUETOOTH_CONNECT,
                READ_PHONE_STATE
            )
        } else {
            arrayOf(
                RECORD_AUDIO
            )
        }
    }

    /**
     * Permissionが許可されているかどうかチェック
     */
    private fun isGrantedSelfPermission(permissions: Array<String>): Boolean {
        permissions.forEach { permission ->
            val granted = ContextCompat.checkSelfPermission(application, permission)
            if (granted != PackageManager.PERMISSION_GRANTED) return false
        }

        return true
    }
}
```

# 実際に通話の動作確認をする方法
1. Android端末を2台用意する。
1. 上記のソースコードを実装したAPKを2台の端末にインストールしてビルドする。
1. アプリ起動後にPermissionの許可を促すダイアログが表示される場合は許可する。
1. 音声通話ができる状態となる。