Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 83 additions & 46 deletions android/app/src/main/kotlin/com/rtirl/chat/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
Expand All @@ -24,9 +29,14 @@ import io.flutter.plugin.common.MethodChannel.Result
import java.util.Locale
import java.util.UUID

class MainActivity : FlutterActivity() {
class MainActivity : FlutterActivity(), AudioManager.OnAudioFocusChangeListener {

private var sharedData: String = ""
private lateinit var audioManager: AudioManager
private lateinit var focusRequest: AudioFocusRequest
private val handler = Handler(Looper.getMainLooper())
private var playbackDelayed = false
private var wasDucking = false

companion object {
var methodChannel: MethodChannel? = null
Expand All @@ -36,6 +46,16 @@ class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent()
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK).run {
setAudioAttributes(AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
setAcceptsDelayedFocusGain(true)
setOnAudioFocusChangeListener(this@MainActivity, handler)
build()
}
}

private fun startNotificationService() {
Expand All @@ -48,17 +68,38 @@ class MainActivity : FlutterActivity() {
}
}

override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
if (wasDucking) {
methodChannel?.invokeMethod("audioVolume", 1.0)
wasDucking = false
}

if (playbackDelayed) {
playbackDelayed = false
} else {
methodChannel?.invokeMethod("audioVolume", 1.0)
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
methodChannel?.invokeMethod("audioVolume", 0.3)
wasDucking = true
}
AudioManager.AUDIOFOCUS_LOSS,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
if (!wasDucking) {
// handle other cases if necessary
}
}
}
}

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
val ttsPlugin = TextToSpeechPlugin(this)
val ttsChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"ttsPlugin"
)

val notificationChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"tts_notifications"
)
val ttsChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "ttsPlugin")
val notificationChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tts_notifications")
val volumeChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "volume_channel")

methodChannel = notificationChannel

Expand All @@ -81,18 +122,36 @@ class MainActivity : FlutterActivity() {
}
}

volumeChannel.setMethodCallHandler { call, result ->
when (call.method) {
"tts_on" -> {
val res = audioManager.requestAudioFocus(focusRequest)
when (res) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
methodChannel?.invokeMethod("audioFocus", "gained")
Log.d("Permission granted", "response granted")
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
playbackDelayed = true
methodChannel?.invokeMethod("audioFocus", "delayed")
}
else -> methodChannel?.invokeMethod("audioFocus", "failed")
}
}
"tts_off" -> {
audioManager.abandonAudioFocusRequest(focusRequest)
methodChannel?.invokeMethod("audioFocus", "lost")
}
else -> result.notImplemented()
}
}

ttsChannel.setMethodCallHandler(ttsPlugin)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.rtirl.chat/audio"
).setMethodCallHandler { call, result ->
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.rtirl.chat/audio").setMethodCallHandler { call, result ->
when (call.method) {
"set" -> {
val intent = Intent(this, AudioService::class.java)
intent.putStringArrayListExtra(
"urls",
ArrayList(call.argument<List<String>>("urls") ?: listOf())
)
intent.putStringArrayListExtra("urls", ArrayList(call.argument<List<String>>("urls") ?: listOf()))
intent.action = AudioService.ACTION_START_SERVICE
startService(intent)
result.success(true)
Expand All @@ -104,35 +163,19 @@ class MainActivity : FlutterActivity() {
result.success(true)
}
"hasPermission" -> {
result.success(
Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
Settings.canDrawOverlays(this)
)
result.success(Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this))
}
"requestPermission" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
!Settings.canDrawOverlays(this)
) {
startActivityForResult(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
), 8675309
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 8675309)
}
result.success(
Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
Settings.canDrawOverlays(this)
)
result.success(Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this))
}
else -> result.notImplemented()
}
}

MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.rtirl.chat/share"
).setMethodCallHandler { call, result ->
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.rtirl.chat/share").setMethodCallHandler { call, result ->
if (call.method == "getSharedData") {
result.success(sharedData)
sharedData = ""
Expand All @@ -143,7 +186,6 @@ class MainActivity : FlutterActivity() {
}

private fun handleIntent() {
// Handle the received text share intent
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { intentData ->
sharedData = intentData
Expand All @@ -152,7 +194,6 @@ class MainActivity : FlutterActivity() {
}
}


class TextToSpeechPlugin(private val context: Context) : MethodCallHandler, TextToSpeech.OnInitListener {
private var tts: TextToSpeech? = null
private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Expand Down Expand Up @@ -228,11 +269,7 @@ class TextToSpeechPlugin(private val context: Context) : MethodCallHandler, Text
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String) {
if (volume != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * volume).toInt(),
0
)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * volume).toInt(), 0)
}
}

Expand Down
12 changes: 12 additions & 0 deletions lib/models/tts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:rtchat/models/messages/twitch/user.dart';
import 'package:rtchat/models/tts/language.dart';
import 'package:rtchat/models/tts/bytes_audio_source.dart';
import 'package:rtchat/models/user.dart';
import 'package:rtchat/volume_plugin.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand Down Expand Up @@ -173,6 +174,10 @@ class TtsModel extends ChangeNotifier {
if (value) {
_lastMessageTime = DateTime.now();
}
if (value) {
VolumePlugin.reduceVolumeOnTtsStart();
}

say(
localizations,
SystemMessageModel(
Expand Down Expand Up @@ -384,6 +389,9 @@ class TtsModel extends ChangeNotifier {
await audioPlayer.setAudioSource(BytesAudioSource(bytes));
await audioPlayer.play();
await Future.delayed(audioPlayer.duration ?? const Duration());
if (_pending.isEmpty) {
VolumePlugin.increaseVolumeOnTtsStop();
}
}
}

Expand All @@ -395,10 +403,14 @@ class TtsModel extends ChangeNotifier {

void unsay(String messageId) {
_pending.remove(messageId);
if (_pending.isEmpty) {
VolumePlugin.increaseVolumeOnTtsStop();
}
}

void stop() {
_pending.clear();
VolumePlugin.increaseVolumeOnTtsStop();
}

void updateFromJson(Map<String, dynamic> json) {
Expand Down
3 changes: 3 additions & 0 deletions lib/screens/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'package:rtchat/models/tts.dart';
import 'package:rtchat/models/user.dart';
import 'package:rtchat/notifications_plugin.dart';
import 'package:rtchat/tts_plugin.dart';
import 'package:rtchat/volume_plugin.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

class ResizableWidget extends StatefulWidget {
Expand Down Expand Up @@ -298,6 +299,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"Text to speech disabled");
await TextToSpeechPlugin.disableTTS();
NotificationsPlugin.cancelNotification();
VolumePlugin.reduceVolumeOnTtsStart();
} else {
// Start listening to the stream before toggling newTtsEnabled
channelStreamController.stream
Expand All @@ -312,6 +314,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"${userModel.activeChannel?.provider}:${userModel.activeChannel?.channelId}",
);
NotificationsPlugin.showNotification();
VolumePlugin.increaseVolumeOnTtsStop();
NotificationsPlugin.listenToTts(ttsModel);
}
}
Expand Down
10 changes: 10 additions & 0 deletions lib/tts_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:rtchat/main.dart';
import 'package:rtchat/notifications_plugin.dart';
import 'package:rtchat/volume_plugin.dart';

class TextToSpeechPlugin {
static const MethodChannel channel = MethodChannel('ttsPlugin');
Expand Down Expand Up @@ -93,6 +94,7 @@ class TTSQueue {
}

if (queue.isNotEmpty) {
VolumePlugin.reduceVolumeOnTtsStart();
final previous = queue.last;
queue.addLast(element);
await previous.completer.future;
Expand All @@ -101,10 +103,18 @@ class TTSQueue {
}
await TextToSpeechPlugin.speak(text, speed: speed ?? 1.5, volume: volume);
completer.complete();

if (isEmpty) {
VolumePlugin.increaseVolumeOnTtsStop();
}
} else {
queue.addLast(element);
await TextToSpeechPlugin.speak(text, speed: speed ?? 1.5, volume: volume);
completer.complete();

if (isEmpty) {
VolumePlugin.increaseVolumeOnTtsStop();
}
}
queue.remove(element);
}
Expand Down
13 changes: 13 additions & 0 deletions lib/volume_plugin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:flutter/services.dart';

class VolumePlugin {
static const MethodChannel channel = MethodChannel('volume_channel');

static Future<void> reduceVolumeOnTtsStart() async {
await channel.invokeMethod('tts_on');
}

static Future<void> increaseVolumeOnTtsStop() async {
await channel.invokeMethod('tts_off');
}
}
14 changes: 14 additions & 0 deletions test/models/tts_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rtchat/tts_plugin.dart';
import 'package:rtchat/volume_plugin.dart';

void main() {
final ttsQueue = TTSQueue();
Expand All @@ -16,11 +17,24 @@ void main() {
(MethodCall method) async {
return null;
});

TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(VolumePlugin.channel,
(MethodCall method) async {
if (method.method == 'tts_on' || method.method == 'tts_off') {
return null; // Mock response for volume channel methods
}
throw MissingPluginException(
'No implementation found for method ${method.method} on channel ${VolumePlugin.channel.name}');
});
});

tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(TextToSpeechPlugin.channel, null);

TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(VolumePlugin.channel, null);
});

test('Speak adds elements to the queue', () async {
Expand Down
Loading