import 'dart:async'; import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/widgets/color_darkener.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/widgets/username_color_generator.dart'; import '../../services/block_service.dart'; import '../contacts/widgets/contact_modal.dart'; import '../../services/call_service.dart'; class History { final Contact contact; final DateTime date; final String callType; // 'incoming' or 'outgoing' final String callStatus; // 'missed' or 'answered' final int attempts; History( this.contact, this.date, this.callType, this.callStatus, this.attempts, ); } class HistoryPage extends StatefulWidget { const HistoryPage({Key? key}) : super(key: key); @override _HistoryPageState createState() => _HistoryPageState(); } class _HistoryPageState extends State with SingleTickerProviderStateMixin { List histories = []; bool loading = true; int? _expandedIndex; final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); // Create a MethodChannel instance. static const MethodChannel _channel = MethodChannel('com.example.calllog'); @override void didChangeDependencies() { super.didChangeDependencies(); if (loading) { _buildHistories(); } } Future _refreshContacts() async { final contactState = ContactState.of(context); try { await contactState.fetchContacts(); } catch (e) { print('Error refreshing contacts: $e'); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Failed to refresh contacts'))); } } void _toggleFavorite(Contact contact) async { try { if (await FlutterContacts.requestPermission()) { Contact? fullContact = await FlutterContacts.getContact(contact.id, withProperties: true, withAccounts: true, withPhoto: true, withThumbnail: true); if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } await _refreshContacts(); } else { print("Could not fetch contact details"); } } catch (e) { print("Error updating favorite status: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to update favorite status'))); } } /// Helper: Remove all non-digit characters for simple matching. String sanitizeNumber(String number) { return number.replaceAll(RegExp(r'\D'), ''); } /// Helper: Find a contact from our list by matching phone numbers. Contact? findContactForNumber(String number, List contacts) { final sanitized = sanitizeNumber(number); for (var contact in contacts) { for (var phone in contact.phones) { if (sanitizeNumber(phone.number) == sanitized) { return contact; } } } return null; } /// Request permission for reading call logs. Future _requestCallLogPermission() async { var status = await Permission.phone.status; if (!status.isGranted) { status = await Permission.phone.request(); } return status.isGranted; } /// Build histories from the native call log using the method channel. Future _buildHistories() async { // Request permission. bool hasPermission = await _requestCallLogPermission(); if (!hasPermission) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Call log permission not granted'))); setState(() { loading = false; }); return; } // Retrieve call logs from native code. List nativeLogs = []; try { nativeLogs = await _channel.invokeMethod('getCallLogs'); } on PlatformException catch (e) { print("Error fetching call logs: ${e.message}"); } // Ensure contacts are loaded. final contactState = ContactState.of(context); if (contactState.loading) { await Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 100)); return contactState.loading; }); } List contacts = contactState.contacts; List callHistories = []; // Process each log entry. for (var entry in nativeLogs) { // Each entry is a Map with keys: number, type, date, duration. final String number = entry['number'] ?? ''; if (number.isEmpty) continue; // Convert timestamp to DateTime. DateTime callDate = DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0); int typeInt = entry['type'] ?? 0; int duration = entry['duration'] ?? 0; String callType; String callStatus; // Map integer values to call type/status. // Commonly: 1 = incoming, 2 = outgoing, 3 = missed. switch (typeInt) { case 1: callType = "incoming"; callStatus = (duration == 0) ? "missed" : "answered"; break; case 2: callType = "outgoing"; callStatus = "answered"; break; case 3: callType = "incoming"; callStatus = "missed"; break; default: callType = "unknown"; callStatus = "unknown"; } // Try to find a matching contact. Contact? matchedContact = findContactForNumber(number, contacts); if (matchedContact == null) { // Create a dummy contact if not found. matchedContact = Contact( id: "dummy-$number", displayName: number, phones: [Phone(number)], ); } callHistories .add(History(matchedContact, callDate, callType, callStatus, 1)); } // Sort histories by most recent. callHistories.sort((a, b) => b.date.compareTo(a.date)); setState(() { histories = callHistories; loading = false; }); } List _buildGroupedList(List historyList) { historyList.sort((a, b) => b.date.compareTo(a.date)); final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final yesterday = today.subtract(const Duration(days: 1)); List todayHistories = []; List yesterdayHistories = []; List olderHistories = []; for (var history in historyList) { final callDate = DateTime(history.date.year, history.date.month, history.date.day); if (callDate == today) { todayHistories.add(history); } else if (callDate == yesterday) { yesterdayHistories.add(history); } else { olderHistories.add(history); } } final items = []; if (todayHistories.isNotEmpty) { items.add('Today'); items.addAll(todayHistories); } if (yesterdayHistories.isNotEmpty) { items.add('Yesterday'); items.addAll(yesterdayHistories); } if (olderHistories.isNotEmpty) { items.add('Older'); items.addAll(olderHistories); } return items; } /// Returns an icon reflecting call type and status. Icon _getCallIcon(History history) { IconData iconData; Color iconColor; if (history.callType == 'incoming') { if (history.callStatus == 'missed') { iconData = Icons.call_missed; iconColor = Colors.red; } else { iconData = Icons.call_received; iconColor = Colors.green; } } else if (history.callType == 'outgoing') { iconData = Icons.call_made; iconColor = Colors.green; } else { iconData = Icons.phone; iconColor = Colors.white; } return Icon(iconData, color: iconColor); } @override Widget build(BuildContext context) { final contactState = ContactState.of(context); if (loading || contactState.loading) { return Scaffold( backgroundColor: Colors.black, body: const Center(child: CircularProgressIndicator()), ); } if (histories.isEmpty) { return Scaffold( backgroundColor: Colors.black, body: const Center( child: Text( 'No call history available.', style: TextStyle(color: Colors.white), ), ), ); } List missedCalls = histories.where((h) => h.callStatus == 'missed').toList(); final allItems = _buildGroupedList(histories); final missedItems = _buildGroupedList(missedCalls); return DefaultTabController( length: 2, child: Scaffold( backgroundColor: Colors.black, appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: Container( color: Colors.black, child: const TabBar( tabs: [ Tab(text: 'All Calls'), Tab(text: 'Missed Calls'), ], indicatorColor: Colors.white, ), ), ), body: TabBarView( children: [ _buildListView(allItems), _buildListView(missedItems), ], ), ), ); } Widget _buildListView(List items) { return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; if (item is String) { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.grey[900], child: Text( item, style: const TextStyle( color: Colors.white70, fontWeight: FontWeight.bold, ), ), ); } else if (item is History) { final history = item; final contact = history.contact; final isExpanded = _expandedIndex == index; Color avatarColor = generateColorFromName(contact.displayName); return Column( children: [ ListTile( leading: GestureDetector( onTap: () { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { return ContactModal( contact: contact, onEdit: () async { if (await FlutterContacts.requestPermission()) { final updatedContact = await FlutterContacts.openExternalEdit( contact.id); if (updatedContact != null) { await _refreshContacts(); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${contact.displayName} updated successfully!'), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Edit canceled or failed.'), ), ); } } }, onToggleFavorite: () { _toggleFavorite(contact); }, isFavorite: contact.isStarred, ); }, ); }, child: ObfuscatedAvatar( imageBytes: contact.thumbnail, radius: 25, backgroundColor: avatarColor, fallbackInitial: contact.displayName, ), ), title: Text( _obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white), ), subtitle: Text( DateFormat('MMM dd, hh:mm a').format(history.date), style: const TextStyle(color: Colors.grey), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ _getCallIcon(history), const SizedBox(width: 8), Text( '${history.attempts}x', style: const TextStyle(color: Colors.white), ), IconButton( icon: const Icon(Icons.phone, color: Colors.green), onPressed: () async { if (contact.phones.isNotEmpty) { _callService.makeGsmCall(context, phoneNumber: contact.phones.first.number); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Contact has no phone number')), ); } }, ), ], ), onTap: () { setState(() { _expandedIndex = isExpanded ? null : index; }); }, ), if (isExpanded) Container( color: Colors.grey[850], child: FutureBuilder( future: BlockService().isNumberBlocked( contact.phones.isNotEmpty ? contact.phones.first.number : ''), builder: (context, snapshot) { final isBlocked = snapshot.data ?? false; return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextButton.icon( onPressed: () async { if (history.contact.phones.isNotEmpty) { final Uri smsUri = Uri( scheme: 'sms', path: history.contact.phones.first.number); if (await canLaunchUrl(smsUri)) { await launchUrl(smsUri); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Could not send message')), ); } } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Contact has no phone number')), ); } }, icon: const Icon(Icons.message, color: Colors.white), label: const Text('Message', style: TextStyle(color: Colors.white)), ), TextButton.icon( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (_) => CallDetailsPage(history: history), ), ); }, icon: const Icon(Icons.info, color: Colors.white), label: const Text('Details', style: TextStyle(color: Colors.white)), ), TextButton.icon( onPressed: () async { final phoneNumber = contact.phones.isNotEmpty ? contact.phones.first.number : null; if (phoneNumber == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Contact has no phone number')), ); return; } if (isBlocked) { await BlockService().unblockNumber(phoneNumber); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$phoneNumber unblocked')), ); } else { await BlockService().blockNumber(phoneNumber); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$phoneNumber blocked')), ); } setState(() {}); }, icon: Icon( isBlocked ? Icons.lock_open : Icons.block, color: Colors.white), label: Text(isBlocked ? 'Unblock' : 'Block', style: const TextStyle(color: Colors.white)), ), ], ); }, ), ), ], ); } return const SizedBox.shrink(); }, ); } } class CallDetailsPage extends StatelessWidget { final History history; final ObfuscateService _obfuscateService = ObfuscateService(); CallDetailsPage({super.key, required this.history}); @override Widget build(BuildContext context) { final contact = history.contact; final contactBg = generateColorFromName(contact.displayName); final contactLetter = darken(contactBg); return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('Call Details'), backgroundColor: Colors.black, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ // Display Contact Name and Thumbnail. Row( children: [ (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) ? ObfuscatedAvatar( imageBytes: contact.thumbnail, radius: 30, backgroundColor: contactBg, fallbackInitial: contact.displayName, ) : CircleAvatar( backgroundColor: generateColorFromName(contact.displayName), radius: 30, child: Text( contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', style: TextStyle(color: contactLetter), ), ), const SizedBox(width: 16), Expanded( child: Text( _obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white, fontSize: 24), ), ), ], ), const SizedBox(height: 24), // Display call details. DetailRow( label: 'Call Type:', value: history.callType, ), DetailRow( label: 'Call Status:', value: history.callStatus, ), DetailRow( label: 'Date:', value: DateFormat('MMM dd, yyyy - hh:mm a').format(history.date), ), DetailRow( label: 'Attempts:', value: '${history.attempts}', ), const SizedBox(height: 24), if (contact.phones.isNotEmpty) DetailRow( label: 'Number:', value: _obfuscateService .obfuscateData(contact.phones.first.number), ), ], ), ), ); } } class DetailRow extends StatelessWidget { final String label; final String value; const DetailRow({Key? key, required this.label, required this.value}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ Text( label, style: const TextStyle( color: Colors.white70, fontWeight: FontWeight.bold), ), const SizedBox(width: 8), Expanded( child: Text( value, style: const TextStyle(color: Colors.white), textAlign: TextAlign.right, ), ), ], ), ); } }