# Android での利用方法

## 概要

Sora Flutter SDK で Android アプリケーションを実装する際のカメラやマイクの権限設定、注意点について説明します。
対応バージョンは Android 10 (API 29) 以降です。
Sora への接続やメディアデバイスの操作といったプラットフォーム共通の Dart API については、他ページをご確認ください。

## 動作環境

- Android 10 (API 29) 以降 (arm64-v8a)
- Flutter 3.44.0 以上
- Android SDK / Android NDK 28.2.13676358
- Sora 2025.2.0 以降

## 依存関係

Android 向けビルドでは、Gradle の `fetchNativeDeps` タスクが `libwebrtc-c` と `webrtc` の配布物を自動取得します。
取得対象のバージョン、URL、SHA-256 は `scripts/native_deps.json` で管理しています。

## ビルド

アプリケーションのディレクトリで依存関係を取得し、APK をビルドします。

```bash
flutter pub get
flutter build apk --debug
flutter build apk --release
```


## 権限

Android で音声や映像を配信する場合は、配信するメディアに応じてカメラまたはマイクの権限が必要になります。
音声のみの場合は `RECORD_AUDIO` 、映像のみの場合は `CAMERA` だけで十分です。
`CAMERA` と `RECORD_AUDIO` はランタイム権限で、マニフェストへの宣言だけでは付与されません。
また Sora への接続には `INTERNET` 権限が必要です。こちらはインストール時に自動で付与される通常の権限のため、マニフェストへの宣言のみで十分です。
SDK は権限リクエストを行わないため、アプリケーション側で `AndroidManifest.xml` の宣言とネイティブ側の権限リクエスト、および Dart 側からの呼び出しを実装してください。

### AndroidManifest.xml の宣言

シグナリング接続用の `INTERNET` と、配信用の `CAMERA` / `RECORD_AUDIO` を宣言します。

```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```

### ランタイム権限のリクエスト

ランタイム権限を取得するには、Dart 側から `MethodChannel` 経由でプラットフォームの権限 API を呼び出します。

#### Dart 側の実装例

Dart 側の `ensureMediaPermissions()` は `MethodChannel` に `camera` / `microphone` の要否を渡すラッパーです。
この呼び出しは `Android` / `iOS` / `macOS` で共通で利用することができます。

```dart
import 'dart:io';

import 'package:flutter/services.dart';

/// プラットフォームのメディア権限（カメラ・マイク）を要求します。
///
/// ``MethodChannel`` 経由で各プラットフォームのネイティブ権限 API を呼び出します。
/// この Dart コードは ``Android`` / ``iOS`` / ``macOS`` で共通です。
///
/// プラットフォームが ``Android`` / ``iOS`` / ``macOS`` 以外の場合は
/// 常に ``true`` を返します。
///
/// ネイティブ側では同じチャンネル名 ``"example/permissions"`` (チャンネル名は例)で
/// ``ensureMediaPermissions`` メソッドをハンドリングする必要があります。
///
/// - Android: ``ActivityCompat.requestPermissions`` で
///   ``CAMERA`` / ``RECORD_AUDIO`` を要求
/// - iOS / macOS: ``AVCaptureDevice.requestAccess`` で
///   ``.video`` / ``.audio`` を要求
///
/// [needsCamera]: カメラ権限が必要な場合に ``true``
/// [needsMicrophone]: マイク権限が必要な場合に ``true``
///
/// 戻り値: すべての要求された権限が許可された場合に ``true``
Future<bool> ensureMediaPermissions({
  required bool needsCamera,
  required bool needsMicrophone,
}) async {
  // Android / iOS / macOS 以外では権限要求の仕組みが異なるため
  // 常に許可扱いとします。
  if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) {
    return true;
  }

  // MethodChannel 経由でネイティブ側の権限 API を呼び出します。
  // チャンネル名 "example/permissions" はネイティブ側と一致させる必要があります。
  // (実際の実装時はアプリケーション名等に合わせて命名してください)
  const channel = MethodChannel('example/permissions');

  // 戻り値が null の場合は false として扱います。
  return await channel.invokeMethod<bool>(
        'ensureMediaPermissions',
        <String, Object?>{
          'camera': needsCamera,
          'microphone': needsMicrophone,
        },
      ) ??
      false;
}
```

接続前の呼び出し例:

```dart
final granted = await ensureMediaPermissions(
  needsCamera: true,
  needsMicrophone: true,
);
if (!granted) {
  return;
}
final stream = await MediaDevices.getUserMedia(
  const GetUserMediaOptions(audio: true, video: true),
);
final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: <String>['wss://sora.example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendrecv,
    audio: true,
    video: true,
  ),
);
await conn.connect(stream);
```

> **注釈**
>
> `permission_handler` パッケージを利用する方法もありますが、 Sora Flutter SDK の DevTools ではプラットフォーム側の細かい制御と依存の最小化のため、
> 独自の `MethodChannel` を採用しています。

#### Android ネイティブ側の実装例

`MainActivity.kt` で `MethodChannel` のハンドラを設定し、
`ActivityCompat.requestPermissions` でカメラ・マイクの許可を要求します。

```kotlin
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private var permissionChannel: MethodChannel? = null
    private var pendingPermissionResult: MethodChannel.Result? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        permissionChannel =
            MethodChannel(
                flutterEngine.dartExecutor.binaryMessenger,
                "example/permissions",
            ).also { channel ->
                channel.setMethodCallHandler { call, result ->
                    when (call.method) {
                        "ensureMediaPermissions" -> {
                            val args = call.arguments as? Map<*, *> ?: emptyMap<String, Any>()
                            val needsCamera = (args["camera"] as? Boolean) ?: false
                            val needsMicrophone = (args["microphone"] as? Boolean) ?: false
                            ensureMediaPermissions(
                                needsCamera = needsCamera,
                                needsMicrophone = needsMicrophone,
                                result = result,
                            )
                        }
                        else -> result.notImplemented()
                    }
                }
            }
    }

    private fun ensureMediaPermissions(
        needsCamera: Boolean,
        needsMicrophone: Boolean,
        result: MethodChannel.Result,
    ) {
        val permissions = mutableListOf<String>()
        if (needsCamera &&
            ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
        ) {
            permissions.add(Manifest.permission.CAMERA)
        }
        if (needsMicrophone &&
            ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
        ) {
            permissions.add(Manifest.permission.RECORD_AUDIO)
        }
        if (permissions.isEmpty()) {
            result.success(true)
            return
        }

        pendingPermissionResult = result
        ActivityCompat.requestPermissions(
            this,
            permissions.toTypedArray(),
            REQUEST_MEDIA_PERMISSIONS,
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode != REQUEST_MEDIA_PERMISSIONS) {
            return
        }

        val result = pendingPermissionResult ?: return
        pendingPermissionResult = null
        result.success(grantResults.all { it == PackageManager.PERMISSION_GRANTED })
    }

    companion object {
        private const val REQUEST_MEDIA_PERMISSIONS = 1001
    }
}
```

#### 実装の参照先

devtools の実装全体は以下を参照してください。

- Dart: `devtools/lib/src/devtools_connection_controller.dart` ( `ensureMediaPermissions` )
- Android ネイティブ: `devtools/android/app/src/main/kotlin/com/example/devtools/MainActivity.kt`

> **注釈**
>
> 視聴のみ( `recvonly` ロール)で利用する場合はカメラ・マイクを使わないため、ランタイム権限の取得は不要です。

## その他

- カメラ送信を含む動作確認には実機が必要です（エミュレーターではカメラデバイスを利用できません）。音声のみの送信はエミュレーターでも確認できます
- リリースビルドでは縮小や難読化の影響を受けるため、 `release` でも必ず起動・動作確認をしてください
- R8 や ProGuard の設定を独自に上書きする場合は、SDK が前提にしている keep 設定を壊さないように注意してください。JNI 初期化に必要なクラスが削除または難読化されると、起動直後にクラッシュする可能性があります。詳細は Android の [ネイティブ ライブラリの最適化](https://developer.android.com/topic/performance/app-optimization/library-optimization?hl=ja) をご確認ください
- NDK や Gradle 周りを大きく変更する場合は、ネイティブライブラリの読み込み条件が変わらないか確認してください
