SkyWay Android SDK クイックスタートが残念な件

SkyWay Android SDKを使ってみました。

まずは[SkyWay Android SDK クイックスタート](https://skyway.ntt.com/ja/docs/user-guide/android-sdk/quickstart/)に従って、小さい実装を組みましたが、クイックスタートの内容が残念だったで記します。

まず
![スクリーンショット 2023-07-21 13.33.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103548/ecec0720-746e-3381-1f88-06da572a1f72.png)
とありますが、これは不要です。
使っているのはAndroid Studio Flamingoですが
```
    buildFeatures {
        compose true
    }　
```
のままで良いです。
このオプションはJetpack Composeを使うかどうかのオプションですが、そもそもJetpack Compose と layout.xmlなどレイアウトファイルを使った手法は共存できます。
またFlamingoだとプロジェクトウィザードで新規プロジェクトを作る際にJetpack Composeが前提で作成されます。
最近Androidアプリ開発を始めた人は、UIの作り方をまずJetpack Composeで学ぶでしょう。そのような時流の中で、これを`false`にすることを案内するのは気が知れません。

次に、
![スクリーンショット 2023-07-22 22.09.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103548/01d50432-b30b-8622-d142-92051094f8cc.png)
activity_main.xmlで無意味にConstraintLayoutを使っています。
外側のConstraintLayoutは不要なので、

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/videos"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal">

        <!-- ローカルのビデオ -->
        <com.ntt.skyway.core.content.sink.SurfaceViewRenderer
            android:id="@+id/local_renderer"
            android:layout_width="150dp"
            android:layout_height="150dp">

        </com.ntt.skyway.core.content.sink.SurfaceViewRenderer>

        <!-- リモートのビデオ -->
        <com.ntt.skyway.core.content.sink.SurfaceViewRenderer
            android:id="@+id/remote_renderer"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:layout_marginStart="20dp">

        </com.ntt.skyway.core.content.sink.SurfaceViewRenderer>
    </LinearLayout>

    <!-- ルーム名の表示枠 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:text=" room: "
            android:textAlignment="textEnd"
            android:textColor="#888888"
            android:textSize="13sp" />
        <EditText
            android:id="@+id/roomName"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:textAlignment="textStart"
            android:textColor="#000000"
            android:textSize="15sp"
            />
    </LinearLayout>

    <!-- 参加ボタン -->
    <Button
        android:id="@+id/joinButton"
        android:layout_width="150dp"
        android:layout_height="70dp"
        android:layout_gravity="center"
        android:text="Join"
        android:textSize="15sp"/>
</LinearLayout>
```
で十分です。

![スクリーンショット 2023-07-21 21.41.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103548/29685dd7-8c6a-31e8-f3d9-5c2fb19feda3.png)
トークンの有効期限が1日となっているので、このクイックスタートを読んでる1日辺りまでしか使えません。別の日では毎回作り直しです。

ここまで書きましたが、他はクイックスタートの記述を読み進め、Android Studioの出すエラーをクリアすれば大丈夫です。スタートの実装はできます。
ちなみにクイックスタートを参照した最小実装で、アプリケーションを実行すると以下のスクショです。
![Screenshot_20230720_230350.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103548/b1952715-dd99-9f25-f107-97e0bbe4c690.png)

MainActivityはこんな感じです。
```MainActivity.kt
package com.example.skywayapplication

import android.Manifest
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import com.ntt.skyway.core.SkyWayContext
import com.ntt.skyway.core.content.Stream
import com.ntt.skyway.core.content.local.LocalAudioStream
import com.ntt.skyway.core.content.local.LocalVideoStream
import com.ntt.skyway.core.content.local.source.AudioSource
import com.ntt.skyway.core.content.local.source.CameraSource
import com.ntt.skyway.core.content.remote.RemoteVideoStream
import com.ntt.skyway.core.content.sink.SurfaceViewRenderer
import com.ntt.skyway.core.util.Logger
import com.ntt.skyway.room.RoomPublication
import com.ntt.skyway.room.member.LocalRoomMember
import com.ntt.skyway.room.member.RoomMember
import com.ntt.skyway.room.p2p.P2PRoom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.UUID

class MainActivity : ComponentActivity() {
    // SkyWayContext.Optionsの設定
    private val option = SkyWayContext.Options(
        authToken = "TOKEN",
        logLevel = Logger.LogLevel.VERBOSE
    )

    // メンバの宣言
    private val scope = CoroutineScope(Dispatchers.IO)
    private var localRoomMember: LocalRoomMember? = null
    private var room: P2PRoom? = null
    private var localVideoStream: LocalVideoStream? = null
    private var localAudioStream: LocalAudioStream? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUI()

        // 権限の要求
        if (ContextCompat.checkSelfPermission(
                applicationContext,
                Manifest.permission.CAMERA
            ) != PermissionChecker.PERMISSION_GRANTED ||
            ContextCompat.checkSelfPermission(
                applicationContext,
                Manifest.permission.RECORD_AUDIO
            ) != PermissionChecker.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO
                ),
                0
            )
        }

        // JOINボタンの動作を設定
        val btnJoinRoom = findViewById<Button>(R.id.joinButton)
        btnJoinRoom.setOnClickListener {
            joinAndPublish()
        }
    }

    // roomNameの初期値を生成する
    private fun initUI() {
        val roomName = findViewById<TextView>(R.id.roomName)
        roomName.text = UUID.randomUUID().toString()
    }

    private fun joinAndPublish() {
        scope.launch {
            val result = SkyWayContext.setup(applicationContext, option)
            if (result) {
                Log.d("App", "Setup succeed")
            }

            // cameraリソースの取得
            val device = CameraSource.getFrontCameras(applicationContext).first()

            // camera映像のキャプチャを開始します
            val cameraOption = CameraSource.CapturingOptions(800, 800)
            CameraSource.startCapturing(applicationContext, device, cameraOption)

            // 描画やpublishが可能なStreamを作成します
            localVideoStream = CameraSource.createStream()

            // SurfaceViewRenderer を取得して描画します。
            runOnUiThread {
                val localVideoRenderer = findViewById<SurfaceViewRenderer>(R.id.local_renderer)
                localVideoRenderer.setup()
                localVideoStream!!.addRenderer(localVideoRenderer)
            }

            // 音声入力を開始します
            AudioSource.start()

            // publishが可能なStreamを作成します
            val localAudioStream = AudioSource.createStream()

            room = P2PRoom.findOrCreate(name = findViewById<EditText>(R.id.roomName).toString())

            val memberInit = RoomMember.Init(name = "member_" + UUID.randomUUID())
            localRoomMember = room?.join(memberInit)

            val resultMessage = if (localRoomMember == null) "Join failed" else "Joined room"
            runOnUiThread {
                Toast.makeText(applicationContext, resultMessage, Toast.LENGTH_SHORT)
                    .show()
            }

            // 入室時に他のメンバーのStreamを購読する
            room?.publications?.forEach {
                if (it.publisher?.id == localRoomMember?.id) return@forEach
                subscribe(it)
            }

            // 誰かがStreamを公開したときに購読する
            room?.onStreamPublishedHandler = Any@{
                Log.d("room", "onStreamPublished: ${it.id}")
                if (it.publisher?.id == localRoomMember?.id) {
                    return@Any
                }
                subscribe(it)
            }

            // 映像、音声のPublish
            localRoomMember?.publish(localVideoStream!!)
            localRoomMember?.publish(localAudioStream!!)
        }
    }

    // 購読(subscribe)の実装部分
    private fun subscribe(publication: RoomPublication) {
        scope.launch {
            // Publicationをsubscribeします
            val subscription = localRoomMember?.subscribe(publication)
            runOnUiThread {
                val remoteVideoRenderer =
                    findViewById<SurfaceViewRenderer>(R.id.remote_renderer)
                remoteVideoRenderer.setup()
                val remoteStream = subscription?.stream
                when (remoteStream?.contentType) {
                    // コンポーネントへの描画
                    Stream.ContentType.VIDEO -> (remoteStream as RemoteVideoStream).addRenderer(
                        remoteVideoRenderer
                    )

                    else -> {}
                }
            }
        }
    }
}
```
ソースは[GitHub](https://github.com/ochim/SkyWayApplication)に上げました

MainActivityで部分的にJetpack Composeを取り入れようかと思いましたが、時間がなかったので、ここでひとまず終了です。
ありがとうございました。

~ 追記 ~
Jetpack ComposeでUIのみ大雑把に書いてみました。

```MainScreen.kt
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.ntt.skyway.core.content.sink.SurfaceViewRenderer

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
    Surface {
        val fieldValue = remember { mutableStateOf("ルーム") }
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Row {
                SurfaceView(Modifier.size(150.dp))
                SurfaceView(Modifier.size(150.dp))
            }
            Row(modifier = Modifier.fillMaxWidth()) {
                Text(" room : ")
                TextField(value = fieldValue.value,
                    onValueChange = { fieldValue.value = it }
                )
            }
            Spacer(modifier = Modifier.height(8.dp))
            Button(onClick = {}) {
                Text(text = "Join")
            }
        }
    }
}

@Composable
fun SurfaceView(modifier: Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context -> SurfaceViewRenderer(context) },
        update = {}
    )
}

@Preview
@Composable
fun Preview() {
    MainScreen()
}
```