feat: integrate flutter_local_notifications for enhanced voice call notifications
- Added flutter_local_notifications dependency to manage notifications during voice calls. - Implemented notification handling in VoiceCallService to update call status and manage user interactions. - Enabled wake lock functionality to keep the screen on during calls and prevent audio interruptions. - Updated AndroidManifest.xml to include necessary permissions for Bluetooth and foreground services. - Enhanced notification actions to allow users to mute, unmute, or end calls directly from notifications.
This commit is contained in:
@@ -31,6 +31,8 @@ android {
|
|||||||
// Align with modern Android Gradle Plugin requirements
|
// Align with modern Android Gradle Plugin requirements
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
// Enable core library desugaring for flutter_local_notifications
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -73,6 +75,9 @@ dependencies {
|
|||||||
implementation("androidx.activity:activity:1.9.2")
|
implementation("androidx.activity:activity:1.9.2")
|
||||||
implementation("androidx.core:core:1.13.1")
|
implementation("androidx.core:core:1.13.1")
|
||||||
implementation("androidx.activity:activity-ktx:1.9.2")
|
implementation("androidx.activity:activity-ktx:1.9.2")
|
||||||
|
|
||||||
|
// Core library desugaring for flutter_local_notifications
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|||||||
@@ -4,11 +4,15 @@
|
|||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<!-- No broad media/storage permissions; use Android Photo Picker (API 33+) and SAF/share intents -->
|
<!-- No broad media/storage permissions; use Android Photo Picker (API 33+) and SAF/share intents -->
|
||||||
|
|
||||||
|
<!-- Bluetooth permissions for audio routing (Android 12+) -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:maxSdkVersion="32" />
|
||||||
|
|
||||||
<!-- Background streaming permissions -->
|
<!-- Background streaming permissions -->
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
@@ -88,7 +92,7 @@
|
|||||||
android:name=".BackgroundStreamingService"
|
android:name=".BackgroundStreamingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync"/>
|
android:foregroundServiceType="dataSync|microphone"/>
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
|||||||
9
android/app/src/main/res/drawable/ic_call_end.xml
Normal file
9
android/app/src/main/res/drawable/ic_call_end.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"/>
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_mic_off.xml
Normal file
9
android/app/src/main/res/drawable/ic_mic_off.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9.01,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z"/>
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_mic_on.xml
Normal file
9
android/app/src/main/res/drawable/ic_mic_on.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
||||||
|
</vector>
|
||||||
225
lib/features/chat/services/voice_call_notification_service.dart
Normal file
225
lib/features/chat/services/voice_call_notification_service.dart
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
/// Service to manage persistent notifications for voice calls
|
||||||
|
class VoiceCallNotificationService {
|
||||||
|
static final VoiceCallNotificationService _instance =
|
||||||
|
VoiceCallNotificationService._internal();
|
||||||
|
factory VoiceCallNotificationService() => _instance;
|
||||||
|
VoiceCallNotificationService._internal();
|
||||||
|
|
||||||
|
final FlutterLocalNotificationsPlugin _notifications =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
// Notification IDs and channels
|
||||||
|
static const String _channelId = 'voice_call_channel';
|
||||||
|
static const String _channelName = 'Voice Call';
|
||||||
|
static const String _channelDescription =
|
||||||
|
'Ongoing voice call notifications';
|
||||||
|
static const int _notificationId = 2001;
|
||||||
|
|
||||||
|
// Action IDs
|
||||||
|
static const String _actionMute = 'mute_call';
|
||||||
|
static const String _actionUnmute = 'unmute_call';
|
||||||
|
static const String _actionEndCall = 'end_call';
|
||||||
|
|
||||||
|
// Callback for handling notification actions
|
||||||
|
void Function(String action)? onActionPressed;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_initialized) return;
|
||||||
|
|
||||||
|
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
const iosSettings = DarwinInitializationSettings(
|
||||||
|
requestAlertPermission: false,
|
||||||
|
requestBadgePermission: false,
|
||||||
|
requestSoundPermission: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = InitializationSettings(
|
||||||
|
android: androidSettings,
|
||||||
|
iOS: iosSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.initialize(
|
||||||
|
settings,
|
||||||
|
onDidReceiveNotificationResponse: _handleNotificationResponse,
|
||||||
|
onDidReceiveBackgroundNotificationResponse:
|
||||||
|
_handleBackgroundNotificationResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create notification channel for Android
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await _createAndroidNotificationChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createAndroidNotificationChannel() async {
|
||||||
|
const androidChannel = AndroidNotificationChannel(
|
||||||
|
_channelId,
|
||||||
|
_channelName,
|
||||||
|
description: _channelDescription,
|
||||||
|
importance: Importance.high,
|
||||||
|
playSound: false,
|
||||||
|
enableVibration: false,
|
||||||
|
showBadge: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(androidChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNotificationResponse(NotificationResponse response) {
|
||||||
|
final action = response.actionId;
|
||||||
|
if (action != null && onActionPressed != null) {
|
||||||
|
onActionPressed!(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static void _handleBackgroundNotificationResponse(
|
||||||
|
NotificationResponse response,
|
||||||
|
) {
|
||||||
|
// Background action handling
|
||||||
|
// Note: This runs in an isolate, so we can't directly call instance methods
|
||||||
|
// Actions will be handled when app returns to foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show ongoing voice call notification
|
||||||
|
Future<void> showCallNotification({
|
||||||
|
required String modelName,
|
||||||
|
required bool isMuted,
|
||||||
|
required bool isSpeaking,
|
||||||
|
}) async {
|
||||||
|
if (!_initialized) {
|
||||||
|
print('VoiceCallNotification: Initializing...');
|
||||||
|
await initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
print('VoiceCallNotification: Showing notification for $modelName (muted: $isMuted, speaking: $isSpeaking)');
|
||||||
|
|
||||||
|
final status = isSpeaking ? 'Speaking...' : 'Listening...';
|
||||||
|
final muteAction = isMuted ? 'Unmute' : 'Mute';
|
||||||
|
final muteActionId = isMuted ? _actionUnmute : _actionMute;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final androidDetails = AndroidNotificationDetails(
|
||||||
|
_channelId,
|
||||||
|
_channelName,
|
||||||
|
channelDescription: _channelDescription,
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
ongoing: true,
|
||||||
|
autoCancel: false,
|
||||||
|
playSound: false,
|
||||||
|
enableVibration: false,
|
||||||
|
showWhen: true,
|
||||||
|
usesChronometer: true,
|
||||||
|
chronometerCountDown: false,
|
||||||
|
category: AndroidNotificationCategory.call,
|
||||||
|
visibility: NotificationVisibility.public,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
colorized: false,
|
||||||
|
actions: <AndroidNotificationAction>[
|
||||||
|
AndroidNotificationAction(
|
||||||
|
muteActionId,
|
||||||
|
muteAction,
|
||||||
|
icon: DrawableResourceAndroidBitmap(
|
||||||
|
isMuted ? '@drawable/ic_mic_on' : '@drawable/ic_mic_off',
|
||||||
|
),
|
||||||
|
showsUserInterface: false,
|
||||||
|
cancelNotification: false,
|
||||||
|
),
|
||||||
|
AndroidNotificationAction(
|
||||||
|
_actionEndCall,
|
||||||
|
'End Call',
|
||||||
|
icon: DrawableResourceAndroidBitmap('@drawable/ic_call_end'),
|
||||||
|
showsUserInterface: true,
|
||||||
|
cancelNotification: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
_notificationId,
|
||||||
|
'Voice Call with $modelName',
|
||||||
|
status,
|
||||||
|
NotificationDetails(android: androidDetails),
|
||||||
|
);
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
// iOS doesn't support action buttons for ongoing notifications
|
||||||
|
// Use a simpler persistent notification
|
||||||
|
const iosDetails = DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: false,
|
||||||
|
presentSound: false,
|
||||||
|
interruptionLevel: InterruptionLevel.timeSensitive,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
_notificationId,
|
||||||
|
'Voice Call with $modelName',
|
||||||
|
status,
|
||||||
|
const NotificationDetails(iOS: iosDetails),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update notification status
|
||||||
|
Future<void> updateCallStatus({
|
||||||
|
required String modelName,
|
||||||
|
required bool isMuted,
|
||||||
|
required bool isSpeaking,
|
||||||
|
}) async {
|
||||||
|
await showCallNotification(
|
||||||
|
modelName: modelName,
|
||||||
|
isMuted: isMuted,
|
||||||
|
isSpeaking: isSpeaking,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the voice call notification
|
||||||
|
Future<void> cancelNotification() async {
|
||||||
|
await _notifications.cancel(_notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if notifications are enabled
|
||||||
|
Future<bool> areNotificationsEnabled() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final androidImpl = _notifications.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
return await androidImpl?.areNotificationsEnabled() ?? false;
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
// iOS doesn't have a direct check, assume enabled if initialized
|
||||||
|
return _initialized;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request notification permissions
|
||||||
|
Future<bool> requestPermissions() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final androidImpl = _notifications.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
final granted = await androidImpl?.requestNotificationsPermission();
|
||||||
|
return granted ?? false;
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
final iosImpl = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||||
|
final granted = await iosImpl?.requestPermissions(
|
||||||
|
alert: true,
|
||||||
|
badge: false,
|
||||||
|
sound: false,
|
||||||
|
);
|
||||||
|
return granted ?? false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/services/socket_service.dart';
|
import '../../../core/services/socket_service.dart';
|
||||||
import '../providers/chat_providers.dart';
|
import '../providers/chat_providers.dart';
|
||||||
import 'text_to_speech_service.dart';
|
import 'text_to_speech_service.dart';
|
||||||
import 'voice_input_service.dart';
|
import 'voice_input_service.dart';
|
||||||
|
import 'voice_call_notification_service.dart';
|
||||||
|
|
||||||
part 'voice_call_service.g.dart';
|
part 'voice_call_service.g.dart';
|
||||||
|
|
||||||
@@ -25,13 +27,17 @@ class VoiceCallService {
|
|||||||
final TextToSpeechService _tts;
|
final TextToSpeechService _tts;
|
||||||
final SocketService _socketService;
|
final SocketService _socketService;
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
|
final VoiceCallNotificationService _notificationService =
|
||||||
|
VoiceCallNotificationService();
|
||||||
|
|
||||||
VoiceCallState _state = VoiceCallState.idle;
|
VoiceCallState _state = VoiceCallState.idle;
|
||||||
String? _sessionId;
|
String? _sessionId;
|
||||||
|
String? _activeConversationId;
|
||||||
StreamSubscription<String>? _transcriptSubscription;
|
StreamSubscription<String>? _transcriptSubscription;
|
||||||
StreamSubscription<int>? _intensitySubscription;
|
StreamSubscription<int>? _intensitySubscription;
|
||||||
String _accumulatedTranscript = '';
|
String _accumulatedTranscript = '';
|
||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
|
bool _isMuted = false;
|
||||||
SocketEventSubscription? _socketSubscription;
|
SocketEventSubscription? _socketSubscription;
|
||||||
|
|
||||||
final StreamController<VoiceCallState> _stateController =
|
final StreamController<VoiceCallState> _stateController =
|
||||||
@@ -57,6 +63,9 @@ class VoiceCallService {
|
|||||||
onComplete: _handleTtsComplete,
|
onComplete: _handleTtsComplete,
|
||||||
onError: _handleTtsError,
|
onError: _handleTtsError,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set up notification action handler
|
||||||
|
_notificationService.onActionPressed = _handleNotificationAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
VoiceCallState get state => _state;
|
VoiceCallState get state => _state;
|
||||||
@@ -68,6 +77,16 @@ class VoiceCallService {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
|
|
||||||
|
// Initialize notification service
|
||||||
|
await _notificationService.initialize();
|
||||||
|
|
||||||
|
// Request notification permissions if needed
|
||||||
|
final notificationsEnabled =
|
||||||
|
await _notificationService.areNotificationsEnabled();
|
||||||
|
if (!notificationsEnabled) {
|
||||||
|
await _notificationService.requestPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize voice input
|
// Initialize voice input
|
||||||
final voiceInitialized = await _voiceInput.initialize();
|
final voiceInitialized = await _voiceInput.initialize();
|
||||||
if (!voiceInitialized) {
|
if (!voiceInitialized) {
|
||||||
@@ -97,8 +116,15 @@ class VoiceCallService {
|
|||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Set conversation ID first before updating state
|
||||||
|
_activeConversationId = conversationId;
|
||||||
|
|
||||||
|
// Update state (this will trigger notification)
|
||||||
_updateState(VoiceCallState.connecting);
|
_updateState(VoiceCallState.connecting);
|
||||||
|
|
||||||
|
// Enable wake lock to keep screen on and prevent audio interruption
|
||||||
|
await WakelockPlus.enable();
|
||||||
|
|
||||||
// Ensure socket connection
|
// Ensure socket connection
|
||||||
await _socketService.ensureConnected();
|
await _socketService.ensureConnected();
|
||||||
_sessionId = _socketService.sessionId;
|
_sessionId = _socketService.sessionId;
|
||||||
@@ -119,6 +145,8 @@ class VoiceCallService {
|
|||||||
await _startListening();
|
await _startListening();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_updateState(VoiceCallState.error);
|
_updateState(VoiceCallState.error);
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
await _notificationService.cancelNotification();
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,8 +326,16 @@ class VoiceCallService {
|
|||||||
await _voiceInput.stopListening();
|
await _voiceInput.stopListening();
|
||||||
await _tts.stop();
|
await _tts.stop();
|
||||||
|
|
||||||
|
// Cancel notification
|
||||||
|
await _notificationService.cancelNotification();
|
||||||
|
|
||||||
|
// Disable wake lock when call ends
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
|
_activeConversationId = null;
|
||||||
_accumulatedTranscript = '';
|
_accumulatedTranscript = '';
|
||||||
|
_isMuted = false;
|
||||||
_updateState(VoiceCallState.disconnected);
|
_updateState(VoiceCallState.disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +362,60 @@ class VoiceCallService {
|
|||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
_state = newState;
|
_state = newState;
|
||||||
_stateController.add(newState);
|
_stateController.add(newState);
|
||||||
|
|
||||||
|
// Update notification when state changes (fire and forget)
|
||||||
|
_updateNotification().catchError((e) {
|
||||||
|
// Ignore notification errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateNotification() async {
|
||||||
|
// Skip notification for idle, error, and disconnected states
|
||||||
|
if (_state == VoiceCallState.idle ||
|
||||||
|
_state == VoiceCallState.error ||
|
||||||
|
_state == VoiceCallState.disconnected) {
|
||||||
|
print('VoiceCall: Skipping notification - state: $_state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final selectedModel = _ref.read(selectedModelProvider);
|
||||||
|
final modelName = selectedModel?.name ?? 'Assistant';
|
||||||
|
|
||||||
|
print('VoiceCall: Updating notification - model: $modelName, muted: $_isMuted, state: $_state');
|
||||||
|
|
||||||
|
await _notificationService.updateCallStatus(
|
||||||
|
modelName: modelName,
|
||||||
|
isMuted: _isMuted,
|
||||||
|
isSpeaking: _state == VoiceCallState.speaking,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('VoiceCall: Failed to update notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNotificationAction(String action) {
|
||||||
|
switch (action) {
|
||||||
|
case 'mute_call':
|
||||||
|
_toggleMute();
|
||||||
|
break;
|
||||||
|
case 'unmute_call':
|
||||||
|
_toggleMute();
|
||||||
|
break;
|
||||||
|
case 'end_call':
|
||||||
|
stopCall();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleMute() {
|
||||||
|
_isMuted = !_isMuted;
|
||||||
|
if (_isMuted) {
|
||||||
|
pauseListening();
|
||||||
|
} else {
|
||||||
|
resumeListening();
|
||||||
|
}
|
||||||
|
_updateNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
@@ -338,6 +428,12 @@ class VoiceCallService {
|
|||||||
_voiceInput.dispose();
|
_voiceInput.dispose();
|
||||||
await _tts.dispose();
|
await _tts.dispose();
|
||||||
|
|
||||||
|
// Cancel notification
|
||||||
|
await _notificationService.cancelNotification();
|
||||||
|
|
||||||
|
// Ensure wake lock is disabled on dispose
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
|
||||||
await _stateController.close();
|
await _stateController.close();
|
||||||
await _transcriptController.close();
|
await _transcriptController.close();
|
||||||
await _responseController.close();
|
await _responseController.close();
|
||||||
|
|||||||
40
pubspec.lock
40
pubspec.lock
@@ -438,6 +438,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "19.4.2"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.0"
|
||||||
|
flutter_local_notifications_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_windows
|
||||||
|
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1514,6 +1546,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.11"
|
version: "0.6.11"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ dependencies:
|
|||||||
share_plus: ^12.0.0
|
share_plus: ^12.0.0
|
||||||
share_handler: ^0.0.19
|
share_handler: ^0.0.19
|
||||||
riverpod_annotation: ^3.0.0
|
riverpod_annotation: ^3.0.0
|
||||||
|
flutter_local_notifications: ^19.4.2
|
||||||
|
|
||||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user