Ready: Fix History (#35)
Co-authored-by: stcb <21@stcb.cc> Reviewed-on: #35 Co-authored-by: Bartosz <bartosz.michalak@epitech.eu> Co-committed-by: Bartosz <bartosz.michalak@epitech.eu>
This commit is contained in:
parent
23c1839e7b
commit
6ada4f6e5c
1
dialer/android/.gitignore
vendored
1
dialer/android/.gitignore
vendored
@ -4,6 +4,7 @@ gradle-wrapper.jar
|
|||||||
/gradlew
|
/gradlew
|
||||||
/gradlew.bat
|
/gradlew.bat
|
||||||
/local.properties
|
/local.properties
|
||||||
|
/gradle.properties
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
gradle.properties
|
gradle.properties
|
||||||
|
|
||||||
|
@ -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}"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user