Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android でのハンズフリーに対応する #69

Merged
merged 32 commits into from
Mar 6, 2024

Conversation

tnoho
Copy link
Contributor

@tnoho tnoho commented Mar 3, 2024

iOS のハンズフリーには #33 ですでに対応していますが、これに加えて Android のハンズフリーに対応します。

iOS の場合は C++ から容易に OS の API を利用することができますが、 Android では C++ からの OS の API 利用は煩雑ですので C++ での提供ではなく aar での提供とし利用するプロジェクトにおいてこれを取り込むこととしました。

Android でも iOS と同様の動きをになるようにしましたが、かなりのコード量となるため iOS とは異なり Unity SDK での利用だけでなく他でも使いやすいように意識してあります。

こんな感じで利用可能です

    private var audioManager: SoraAudioManager? = null
    private var isHandsfree = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ハンズフリーを切り替えるボタンの定義
        binding.handsfreeButton.setOnClickListener {
            // ハンズフリーを現状の逆に切り替える
            audioManager?.isHandsfree = !isHandsfree
        }
        // SoraAudioManager を生成する
        audioManager = SoraAudioManagerFactory.create(applicationContext)
    }

    override fun onResume() {
        super.onResume()
        // SoraAudioManager を開始する
        // AudioManager.requestAudioFocus を内部で呼んでいるので onResume が良い
        audioManager?.start {
            // ハンズフリーは OS により解除されることがあるので変化をイベントで受け取る
            // 現在の状態を知る
            isHandsfree = audioManager?.isHandsfree == true
            // 状態に応じて UI を更新する
            updateHandsfreeButtonText()
        }
    }

    override fun onPause() {
        super.onPause()
        // SoraAudioManager を停止する
        // AudioManager. abandonAudioFocusRequest を内部で呼んでいるので onPause が良い
        audioManager?.stop()
    }

    // 現在の状態に応じてハンズフリーを切り替えるボタンのテキストを変える
    private fun updateHandsfreeButtonText() {
        // UI の変更は UI のスレッドで行わなければならないので UI スレッドで実行する
        runOnUiThread {
            if (isHandsfree) {
                binding.handsfreeButton.text = "SET DEFAULT"
            } else {
                binding.handsfreeButton.text = "SET HANDSFREE"
            }
        }
    }

#33 の iOS と同様に

  • void isHandsfree()
    • 今ハンズフリーモードかを確認する。
  • void setHandsfree(bool enable)
    • ハンズフリーモードにする。もしくは解除する。

での切り替えが可能です。

しかしながら iOS とは異なりインスタンス生成が必要で、これは SoraAudioManagerFactory.create 関数により行います。

Android はオーディオデバイスの切り替えや Bluetooth ヘッドセットのスイッチングが 31 を境に大幅に変更されていることから、31 より前の Android に対応する SoraAudioManagerLegacy と以降に対応する SoraAudioManager2 を内部ために使い分けるためこのような実装になっています。

また Android OS においては Bluetooth ヘッドセットへの接続制御は OS が行なっていないため、これをライブラリ内で行なっています。
加えてバックグラウンド音楽再生の一時停止処理など VoIP 向けのオーディオ設定を行う必要があるため。以下の 2 関数を用意してあります。

  • void start(OnChangeRouteObserver observer)
    • SoraAudioManager による制御を開始する
    • バックグラウンド復帰時にも実行する必要があるため onResume で呼ぶことが望ましい
  • void stop()
    • SoraAudioManager による制御を停止する
    • バックグラウンドから戻ってきた際にも実行する必要があるため onPause で呼ぶことが望ましい

なお、ハンズフリーは iOS の場合には有線ヘッドセットや Bluetooth ヘッドセットを接続した際に強制解除されるため、同様の挙動を実装してあります。そのため、ユーザー操作起因でデバイスの変更があったことを通知する OnChangeRouteObserver を用意してあります。

以上、ご確認ください🙏

tnoho added 27 commits January 25, 2024 01:33
@tnoho tnoho requested a review from melpon March 3, 2024 03:29
@tnoho tnoho self-assigned this Mar 3, 2024
throw new IllegalStateException("Not on main thread!");
}
}
public static boolean runOnMainThread(Runnable action) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分のイメージする runOnMainThread は

  • メインスレッド上で実行中なら即座に action を実行する
  • そうでなければメインスレッドに action を投げて実行を依頼し
  • 実行完了を待つ

です。1番目と3番目が自分のイメージと違います。

ただ Activity#runOnUiThread の場合、UI スレッド上で実行中なら actions を実行するのは同じですが、非 UI スレッドの場合は依頼を投げて実行完了を待たないようです。

どちらにせよ、メインスレッド上で実行中なら即座に action を実行するのはあった方が良さそうな感じです。

Copy link
Contributor Author

@tnoho tnoho Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activity#runOnUiThread の存在を知らなかったので、これで置き換える方向にしたいと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

と思ったのですが、 Activity をこのレイヤーまで下ろしてないので、 runOnMainThread を

  • メインスレッド上で実行中なら即座に action を実行する
  • そうでなければメインスレッドに action を投げて実行を依頼する

という作りに変更しました。MainThreadWrapper もこれに合わせた作りに変えました。

* すでに接続済みの場合は startBluetoothSco ではインテントは発火しないが
* registerReceiver を設定した際に発火する
* ただ、その場合も startBluetoothSco は呼んでおかないと、
* 他のプログラムが stopBluetoothSco した際に終了してします
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「終了してします」→「終了してしまう」

import java.util.List;
import java.util.Set;

public class SoraAudioManagerLegacy extends SoraAudioManagerBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SoraAudioManagerLegacy と SoraAudioManager2 は public class である必要は無さそうです。
ただユーザーの方でカスタマイズする時に使いやすいようにとかなら public でも良いかなとは思います。

}

public static SoraAudioManager createWithMainThreadWrapper(Context context) {
return new MainThreadWrapper(context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SoraAudioManager.createWithMainThreadWrapper が MainThreadWrapper を利用して、MainThreadWrapper の中で SoraAudioManager.create を利用して、依存が循環してるのが気になりますね。
new MainThreadWrapper(create(context)) にした方が、依存の向きが分かりやすくなるし、生成方法を外に出せて汎用性高くなりそうです。
(これだとユーザーが生成方法を指定できないから SoraAudioManager withMainThreadWrapper(SoraAudioManager manager) みたいな関数があった方がより便利なんでしょうけど)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createWithMainThreadWrapper の作りをご指摘の形に合わせて変更しました。ベースにした AppRTC の名残でコンストラクタにメインスレッド実行制約がかかっていたのですが、コンストラクタのコードを洗ったところ無くても問題ないようだったので、これを解除して対応しました。withMainThreadWrapper もおまけなので作っておきました。


registerWiredHeadsetReceiver();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Override

super.stop();
}

// ハンズフリーかを確認する
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ハンズフリーかを確認する
// ハンズフリーかを確認する
@Override

return new SoraBluetoothManager(context, soraAudioManagerLegacy, audioManager);
}

protected SoraBluetoothManager(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected SoraBluetoothManager(
protected SoraBluetoothManager(

@tnoho tnoho requested a review from melpon March 5, 2024 14:17
@tnoho
Copy link
Contributor Author

tnoho commented Mar 5, 2024

@melpon ご指摘ありがとうございます🙏ご指摘いただいたところは一通り修正しましたので確認いただければと思います。

Copy link
Contributor

@melpon melpon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

よさそう

@tnoho
Copy link
Contributor Author

tnoho commented Mar 5, 2024

ありがとうございます!🙏

@miosakuma miosakuma merged commit e1a0200 into develop Mar 6, 2024
8 checks passed
@miosakuma miosakuma deleted the feature/add-android-handsfree branch March 6, 2024 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants