fix history
All checks were successful
/ mirror (push) Successful in 4s

This commit is contained in:
Bartosz 2025-02-15 16:41:39 +00:00
parent ecf4ea16d8
commit 23086d9645
5 changed files with 241 additions and 114 deletions

View File

@ -5,6 +5,10 @@
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" /> <uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" /> <uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application <application
android:label="Icing Dialer" android:label="Icing Dialer"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -1,19 +1,70 @@
package com.icing.dialer package com.icing.dialer
import android.database.Cursor
import android.os.Bundle import android.os.Bundle
import android.provider.CallLog
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import com.icing.dialer.KeystoreHelper import com.icing.dialer.KeystoreHelper
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.keystore" // Existing channel for keystore operations.
private val KEYSTORE_CHANNEL = "com.example.keystore"
// New channel for call log access.
private val CALLLOG_CHANNEL = "com.example.calllog"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
// Delegate method calls to KeystoreHelper // Set up the keystore channel.
KeystoreHelper(call, result).handleMethodCall() MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
.setMethodCallHandler { call, result ->
// Delegate method calls to KeystoreHelper.
KeystoreHelper(call, result).handleMethodCall()
}
// Set up the call log channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") {
val callLogs = getCallLogs()
result.success(callLogs)
} else {
result.notImplemented()
}
}
}
/**
* Queries the Android call log and returns a list of maps.
* Each map contains keys: "number", "type", "date", and "duration".
*/
private fun getCallLogs(): List<Map<String, Any?>> {
val logsList = mutableListOf<Map<String, Any?>>()
val cursor: Cursor? = contentResolver.query(
CallLog.Calls.CONTENT_URI,
null,
null,
null,
CallLog.Calls.DATE + " DESC"
)
if (cursor != null) {
while (cursor.moveToNext()) {
val number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
val type = cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION))
val map = HashMap<String, Any?>()
map["number"] = number
map["type"] = type // Typically: 1 for incoming, 2 for outgoing, 3 for missed.
map["date"] = date
map["duration"] = duration
logsList.add(map)
}
cursor.close()
} }
return logsList
} }
} }

View File

@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
dev.steenbakker.mobile_scanner.useUnbundled=true dev.steenbakker.mobile_scanner.useUnbundled=true
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 org.gradle.java.home=/usr/lib/jvm/java-17-openjdk

View File

@ -1,8 +1,11 @@
import 'dart:async';
import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/color_darkener.dart'; import 'package:dialer/widgets/color_darkener.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
@ -17,12 +20,12 @@ class History {
final int attempts; final int attempts;
History( History(
this.contact, this.contact,
this.date, this.date,
this.callType, this.callType,
this.callStatus, this.callStatus,
this.attempts, this.attempts,
); );
} }
class HistoryPage extends StatefulWidget { class HistoryPage extends StatefulWidget {
@ -37,9 +40,11 @@ class _HistoryPageState extends State<HistoryPage>
List<History> histories = []; List<History> histories = [];
bool loading = true; bool loading = true;
int? _expandedIndex; int? _expandedIndex;
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
// Create a MethodChannel instance.
static const MethodChannel _channel = MethodChannel('com.example.calllog');
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@ -51,19 +56,16 @@ class _HistoryPageState extends State<HistoryPage>
Future<void> _refreshContacts() async { Future<void> _refreshContacts() async {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);
try { try {
// Refresh contacts or fetch them again
await contactState.fetchContacts(); await contactState.fetchContacts();
} catch (e) { } catch (e) {
print('Error refreshing contacts: $e'); print('Error refreshing contacts: $e');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
SnackBar(content: Text('Failed to refresh contacts')), .showSnackBar(SnackBar(content: Text('Failed to refresh contacts')));
);
} }
} }
void _toggleFavorite(Contact contact) async { void _toggleFavorite(Contact contact) async {
try { try {
// Ensure you have the necessary permissions to fetch contact details
if (await FlutterContacts.requestPermission()) { if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id, Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true, withProperties: true,
@ -75,22 +77,68 @@ class _HistoryPageState extends State<HistoryPage>
fullContact.isStarred = !fullContact.isStarred; fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact); await FlutterContacts.updateContact(fullContact);
} }
await _refreshContacts(); // Refresh the contact list await _refreshContacts();
} else { } else {
print("Could not fetch contact details"); print("Could not fetch contact details");
} }
} catch (e) { } catch (e) {
print("Error updating favorite status: $e"); print("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
SnackBar(content: Text('Failed to update contact favorite status')), .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<Contact> 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<bool> _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<void> _buildHistories() async { Future<void> _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<dynamic> 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); final contactState = ContactState.of(context);
if (contactState.loading) { if (contactState.loading) {
// Wait for contacts to be loaded
await Future.doWhile(() async { await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
return contactState.loading; return contactState.loading;
@ -98,30 +146,66 @@ class _HistoryPageState extends State<HistoryPage>
} }
List<Contact> contacts = contactState.contacts; List<Contact> contacts = contactState.contacts;
if (contacts.isEmpty) { List<History> callHistories = [];
setState(() { // Process each log entry.
loading = false; for (var entry in nativeLogs) {
}); // Each entry is a Map with keys: number, type, date, duration.
return; 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(() { setState(() {
histories = List.generate( histories = callHistories;
contacts.length >= 10 ? 10 : contacts.length,
(index) => History(
contacts[index],
DateTime.now().subtract(Duration(hours: (index + 1) * 2)),
index % 2 == 0 ? 'outgoing' : 'incoming',
index % 3 == 0 ? 'missed' : 'answered',
index % 3 + 1,
),
);
loading = false; loading = false;
}); });
} }
List _buildGroupedList(List<History> historyList) { List _buildGroupedList(List<History> historyList) {
// Sort histories by date (most recent first)
historyList.sort((a, b) => b.date.compareTo(a.date)); historyList.sort((a, b) => b.date.compareTo(a.date));
final now = DateTime.now(); final now = DateTime.now();
@ -134,7 +218,7 @@ class _HistoryPageState extends State<HistoryPage>
for (var history in historyList) { for (var history in historyList) {
final callDate = final callDate =
DateTime(history.date.year, history.date.month, history.date.day); DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) { if (callDate == today) {
todayHistories.add(history); todayHistories.add(history);
} else if (callDate == yesterday) { } else if (callDate == yesterday) {
@ -144,7 +228,6 @@ class _HistoryPageState extends State<HistoryPage>
} }
} }
// Combine them with headers
final items = <dynamic>[]; final items = <dynamic>[];
if (todayHistories.isNotEmpty) { if (todayHistories.isNotEmpty) {
items.add('Today'); items.add('Today');
@ -162,6 +245,28 @@ class _HistoryPageState extends State<HistoryPage>
return items; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);
@ -169,9 +274,7 @@ class _HistoryPageState extends State<HistoryPage>
if (loading || contactState.loading) { if (loading || contactState.loading) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: const Center( body: const Center(child: CircularProgressIndicator()),
child: CircularProgressIndicator(),
),
); );
} }
@ -187,9 +290,8 @@ class _HistoryPageState extends State<HistoryPage>
); );
} }
// Filter missed calls
List<History> missedCalls = List<History> missedCalls =
histories.where((h) => h.callStatus == 'missed').toList(); histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories); final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls); final missedItems = _buildGroupedList(missedCalls);
@ -213,9 +315,7 @@ class _HistoryPageState extends State<HistoryPage>
), ),
body: TabBarView( body: TabBarView(
children: [ children: [
// All Calls
_buildListView(allItems), _buildListView(allItems),
// Missed Calls
_buildListView(missedItems), _buildListView(missedItems),
], ],
), ),
@ -228,9 +328,7 @@ class _HistoryPageState extends State<HistoryPage>
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = items[index]; final item = items[index];
if (item is String) { if (item is String) {
// This is a header item
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.grey[900], color: Colors.grey[900],
@ -246,16 +344,12 @@ class _HistoryPageState extends State<HistoryPage>
final history = item; final history = item;
final contact = history.contact; final contact = history.contact;
final isExpanded = _expandedIndex == index; final isExpanded = _expandedIndex == index;
// Generate the avatar color
Color avatarColor = generateColorFromName(contact.displayName); Color avatarColor = generateColorFromName(contact.displayName);
return Column( return Column(
children: [ children: [
ListTile( ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () { onTap: () {
// When the profile picture is tapped, show the ContactModal
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -266,8 +360,7 @@ class _HistoryPageState extends State<HistoryPage>
onEdit: () async { onEdit: () async {
if (await FlutterContacts.requestPermission()) { if (await FlutterContacts.requestPermission()) {
final updatedContact = final updatedContact =
await FlutterContacts.openExternalEdit( await FlutterContacts.openExternalEdit(contact.id);
contact.id);
if (updatedContact != null) { if (updatedContact != null) {
await _refreshContacts(); await _refreshContacts();
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -306,12 +399,14 @@ class _HistoryPageState extends State<HistoryPage>
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
subtitle: Text( subtitle: Text(
'${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}', DateFormat('MMM dd, hh:mm a').format(history.date),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_getCallIcon(history),
const SizedBox(width: 8),
Text( Text(
'${history.attempts}x', '${history.attempts}x',
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
@ -320,20 +415,18 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green), icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async { onPressed: () async {
if (contact.phones.isNotEmpty) { if (contact.phones.isNotEmpty) {
final Uri callUri = Uri( final Uri callUri =
scheme: 'tel', path: contact.phones.first.number); Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) { if (await canLaunchUrl(callUri)) {
await launchUrl(callUri); await launchUrl(callUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Could not launch call')),
content: Text('Could not launch call')),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Contact has no phone number')),
content: Text('Contact has no phone number')),
); );
} }
}, },
@ -351,9 +444,7 @@ class _HistoryPageState extends State<HistoryPage>
color: Colors.grey[850], color: Colors.grey[850],
child: FutureBuilder<bool>( child: FutureBuilder<bool>(
future: BlockService().isNumberBlocked( future: BlockService().isNumberBlocked(
contact.phones.isNotEmpty contact.phones.isNotEmpty ? contact.phones.first.number : ''),
? contact.phones.first.number
: ''),
builder: (context, snapshot) { builder: (context, snapshot) {
final isBlocked = snapshot.data ?? false; final isBlocked = snapshot.data ?? false;
return Row( return Row(
@ -369,37 +460,29 @@ class _HistoryPageState extends State<HistoryPage>
await launchUrl(smsUri); await launchUrl(smsUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Could not send message')),
content:
Text('Could not send message')),
); );
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Contact has no phone number')),
content:
Text('Contact has no phone number')),
); );
} }
}, },
icon: icon: const Icon(Icons.message, color: Colors.white),
const Icon(Icons.message, color: Colors.white), label: const Text('Message', style: TextStyle(color: Colors.white)),
label: const Text('Message',
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => builder: (_) => CallDetailsPage(history: history),
CallDetailsPage(history: history),
), ),
); );
}, },
icon: const Icon(Icons.info, color: Colors.white), icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', label: const Text('Details', style: TextStyle(color: Colors.white)),
style: TextStyle(color: Colors.white)),
), ),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
@ -408,30 +491,24 @@ class _HistoryPageState extends State<HistoryPage>
: null; : null;
if (phoneNumber == null) { if (phoneNumber == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('Contact has no phone number')),
content:
Text('Contact has no phone number')),
); );
return; return;
} }
if (isBlocked) { if (isBlocked) {
await BlockService().unblockNumber(phoneNumber); await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('$phoneNumber unblocked')),
content: Text('$phoneNumber unblocked')),
); );
} else { } else {
await BlockService().blockNumber(phoneNumber); await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('$phoneNumber blocked')),
content: Text('$phoneNumber blocked')),
); );
} }
setState(() {}); // Refresh the button state setState(() {});
}, },
icon: Icon( icon: Icon(isBlocked ? Icons.lock_open : Icons.block,
isBlocked ? Icons.lock_open : Icons.block,
color: Colors.white), color: Colors.white),
label: Text(isBlocked ? 'Unblock' : 'Block', label: Text(isBlocked ? 'Unblock' : 'Block',
style: const TextStyle(color: Colors.white)), style: const TextStyle(color: Colors.white)),
@ -444,7 +521,6 @@ class _HistoryPageState extends State<HistoryPage>
], ],
); );
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
); );
@ -473,27 +549,26 @@ class CallDetailsPage extends StatelessWidget {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
// Display Contact Name and Thumbnail // Display Contact Name and Thumbnail.
Row( Row(
children: [ children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty) (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? ObfuscatedAvatar( ? ObfuscatedAvatar(
imageBytes: contact.thumbnail, imageBytes: contact.thumbnail,
radius: 30, radius: 30,
backgroundColor: contactBg, backgroundColor: contactBg,
fallbackInitial: contact.displayName, fallbackInitial: contact.displayName,
) )
: CircleAvatar( : CircleAvatar(
backgroundColor: backgroundColor: generateColorFromName(contact.displayName),
generateColorFromName(contact.displayName), radius: 30,
radius: 30, child: Text(
child: Text( contact.displayName.isNotEmpty
contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase()
? contact.displayName[0].toUpperCase() : '?',
: '?', style: TextStyle(color: contactLetter),
style: TextStyle(color: contactLetter), ),
), ),
),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
@ -504,8 +579,7 @@ class CallDetailsPage extends StatelessWidget {
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Display call details.
// Display call type, status, date, attempts
DetailRow( DetailRow(
label: 'Call Type:', label: 'Call Type:',
value: history.callType, value: history.callType,
@ -522,15 +596,11 @@ class CallDetailsPage extends StatelessWidget {
label: 'Attempts:', label: 'Attempts:',
value: '${history.attempts}', value: '${history.attempts}',
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// If you have more details like duration, contact number, etc.
if (contact.phones.isNotEmpty) if (contact.phones.isNotEmpty)
DetailRow( DetailRow(
label: 'Number:', label: 'Number:',
value: _obfuscateService value: _obfuscateService.obfuscateData(contact.phones.first.number),
.obfuscateData(contact.phones.first.number),
), ),
], ],
), ),

View File

@ -56,6 +56,8 @@ dependencies:
uuid: ^4.5.1 uuid: ^4.5.1
provider: ^6.1.2 provider: ^6.1.2
intl: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter