monorepo/dialer/lib/presentation/features/voicemail/voicemail_page.dart
AlexisDanlos ec3f13a34b
All checks were successful
/ mirror (push) Successful in 4s
/ build (push) Successful in 9m58s
/ build-stealth (push) Successful in 10m3s
refactor: add voicemail functionality with permissions handling and UI updates
2025-04-09 16:25:13 +02:00

446 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../domain/models/voicemail.dart';
import '../../../domain/services/voicemail_service.dart';
import '../../../domain/services/call_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../core/utils/color_utils.dart';
class VoicemailPage extends StatefulWidget {
const VoicemailPage({super.key});
@override
State<VoicemailPage> createState() => _VoicemailPageState();
}
class _VoicemailPageState extends State<VoicemailPage> {
final VoicemailService _voicemailService = VoicemailService();
final CallService _callService = CallService();
final ObfuscateService _obfuscateService = ObfuscateService();
final AudioPlayer _audioPlayer = AudioPlayer();
List<Voicemail> _voicemails = [];
bool _loading = true;
String? _currentPlayingId;
int? _expandedIndex;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
bool _isPlaying = false;
@override
void initState() {
super.initState();
_fetchVoicemails();
_audioPlayer.onDurationChanged.listen((Duration d) {
setState(() => _duration = d);
});
_audioPlayer.onPositionChanged.listen((Duration p) {
setState(() => _position = p);
});
_audioPlayer.onPlayerComplete.listen((event) {
setState(() {
_isPlaying = false;
_position = Duration.zero;
_currentPlayingId = null;
});
});
}
Future<void> _fetchVoicemails() async {
setState(() => _loading = true);
try {
final voicemails = await _voicemailService.getVoicemails();
setState(() {
_voicemails = voicemails;
_loading = false;
});
// Show explanation if no voicemails found
if (voicemails.isEmpty && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No voicemails found. Make sure this app is set as the default dialer and has all required permissions.'),
duration: Duration(seconds: 5),
),
);
}
} catch (e) {
debugPrint('Error fetching voicemails: $e');
setState(() => _loading = false);
String errorMessage = 'Failed to load voicemails';
if (e.toString().contains('NOT_DEFAULT_DIALER')) {
errorMessage = 'Please set this app as your default phone app to access voicemails';
} else if (e.toString().contains('MISSING_PERMISSIONS')) {
errorMessage = 'Missing permissions needed to access voicemails';
} else if (e.toString().contains('ADD_VOICEMAIL')) {
errorMessage = 'Samsung device detected. Additional voicemail permissions are required';
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage)),
);
}
}
}
Future<void> _togglePlayPause(Voicemail voicemail) async {
if (_currentPlayingId == voicemail.id && _isPlaying) {
await _audioPlayer.pause();
setState(() => _isPlaying = false);
} else {
// If we're playing a different voicemail, stop current one
if (_isPlaying && _currentPlayingId != voicemail.id) {
await _audioPlayer.stop();
}
// Start playing the new voicemail
if (voicemail.filePath != null) {
try {
await _audioPlayer.play(DeviceFileSource(voicemail.filePath!));
setState(() {
_isPlaying = true;
_currentPlayingId = voicemail.id;
});
// Mark as read if not already
if (!voicemail.isRead) {
await _voicemailService.markAsRead(voicemail.id);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error playing voicemail: $e')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Voicemail file not available')),
);
}
}
}
void _makeCall(String phoneNumber) {
_callService.makeGsmCall(context, phoneNumber: phoneNumber);
}
void _sendMessage(String phoneNumber) async {
final Uri smsUri = Uri(scheme: 'sms', path: phoneNumber);
try {
await launchUrl(smsUri);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send message')),
);
}
}
}
Future<void> _deleteVoicemail(String id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Voicemail'),
content: const Text('Are you sure you want to delete this voicemail?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
if (confirmed == true) {
try {
final success = await _voicemailService.deleteVoicemail(id);
if (success) {
setState(() {
_voicemails.removeWhere((vm) => vm.id == id);
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to delete voicemail')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error deleting voicemail: $e')),
);
}
}
}
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(child: CircularProgressIndicator()),
);
}
if (_voicemails.isEmpty) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.voicemail, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'No voicemails found',
style: TextStyle(color: Colors.white, fontSize: 20),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
'Make sure this app is set as your default dialer and has all required permissions',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchVoicemails,
child: const Text('Refresh'),
),
],
),
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: RefreshIndicator(
onRefresh: _fetchVoicemails,
child: ListView.builder(
itemCount: _voicemails.length,
itemBuilder: (context, index) {
final voicemail = _voicemails[index];
final isExpanded = _expandedIndex == index;
final isPlaying = _isPlaying && _currentPlayingId == voicemail.id;
return FutureBuilder<String?>(
future: _voicemailService.getSenderName(voicemail.phoneNumber),
builder: (context, snapshot) {
final senderName = snapshot.data ?? voicemail.sender ?? voicemail.phoneNumber;
final initials = senderName.isNotEmpty ? senderName[0].toUpperCase() : '?';
final avatarColor = generateColorFromName(senderName);
return GestureDetector(
onTap: () {
setState(() {
_expandedIndex = isExpanded ? null : index;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.all(16),
child: isExpanded
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: avatarColor,
child: Text(
initials,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_obfuscateService.obfuscateData(senderName),
style: const TextStyle(color: Colors.white),
),
Text(
'${DateFormat('MMM dd, h:mm a').format(voicemail.timestamp)} - ${voicemail.duration.inMinutes}:${(voicemail.duration.inSeconds % 60).toString().padLeft(2, '0')} min',
style: const TextStyle(color: Colors.grey),
),
],
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: () => _togglePlayPause(voicemail),
),
if (isPlaying)
Expanded(
child: Slider(
min: 0,
max: _duration.inSeconds.toDouble(),
value: _position.inSeconds.toDouble(),
onChanged: (value) async {
final newPos = Duration(seconds: value.toInt());
await _audioPlayer.seek(newPos);
},
activeColor: Colors.blue,
inactiveColor: Colors.grey,
),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 12,
children: [
ActionButton(
icon: Icons.call,
label: 'Call',
color: Colors.green,
onTap: () => _makeCall(voicemail.phoneNumber),
),
ActionButton(
icon: Icons.message,
label: 'Text',
color: Colors.blue,
onTap: () => _sendMessage(voicemail.phoneNumber),
),
ActionButton(
icon: Icons.delete,
label: 'Delete',
color: Colors.red,
onTap: () => _deleteVoicemail(voicemail.id),
),
ActionButton(
icon: Icons.share,
label: 'Share',
color: Colors.white,
onTap: () {
// Implement share functionality
},
),
],
),
],
)
: Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: avatarColor,
child: Text(
initials,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_obfuscateService.obfuscateData(senderName),
style: TextStyle(
color: Colors.white,
fontWeight: voicemail.isRead ? FontWeight.normal : FontWeight.bold,
),
),
Text(
'${DateFormat('MMM dd, h:mm a').format(voicemail.timestamp)} - ${voicemail.duration.inMinutes}:${(voicemail.duration.inSeconds % 60).toString().padLeft(2, '0')} min',
style: const TextStyle(color: Colors.grey),
),
],
),
),
if (!voicemail.isRead)
Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
),
],
),
),
);
}
);
},
),
),
);
}
}
class ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final VoidCallback onTap;
const ActionButton({
Key? key,
required this.icon,
required this.label,
required this.color,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Column(
children: [
Icon(icon, color: color),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.white)),
],
),
);
}
}