From 6ada4f6e5c1fcd7079cf48b22daecd8b0afec7e7 Mon Sep 17 00:00:00 2001
From: Bartosz <bartosz.michalak@epitech.eu>
Date: Mon, 17 Feb 2025 09:49:50 +0000
Subject: [PATCH] Ready: Fix History (#35)

Co-authored-by: stcb <21@stcb.cc>
Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/35
Co-authored-by: Bartosz <bartosz.michalak@epitech.eu>
Co-committed-by: Bartosz <bartosz.michalak@epitech.eu>
---
 dialer/android/.gitignore                     |   1 +
 .../android/app/src/main/AndroidManifest.xml  |   4 +
 .../kotlin/com/icing/dialer/MainActivity.kt   |  59 +++-
 dialer/lib/features/history/history_page.dart | 288 +++++++++++-------
 dialer/pubspec.yaml                           |   2 +
 5 files changed, 241 insertions(+), 113 deletions(-)

diff --git a/dialer/android/.gitignore b/dialer/android/.gitignore
index e409267..e6d71b3 100644
--- a/dialer/android/.gitignore
+++ b/dialer/android/.gitignore
@@ -4,6 +4,7 @@ gradle-wrapper.jar
 /gradlew
 /gradlew.bat
 /local.properties
+/gradle.properties
 GeneratedPluginRegistrant.java
 gradle.properties
 
diff --git a/dialer/android/app/src/main/AndroidManifest.xml b/dialer/android/app/src/main/AndroidManifest.xml
index cb4976e..138e514 100644
--- a/dialer/android/app/src/main/AndroidManifest.xml
+++ b/dialer/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,10 @@
     <uses-permission android:name="android.permission.SEND_SMS" />
     <uses-permission android:name="android.permission.READ_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
         android:label="Icing Dialer"
         android:name="${applicationName}"
diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/MainActivity.kt
index 799d7b5..eac8625 100644
--- a/dialer/android/app/src/main/kotlin/com/icing/dialer/MainActivity.kt
+++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/MainActivity.kt
@@ -1,19 +1,70 @@
 package com.icing.dialer
 
+import android.database.Cursor
 import android.os.Bundle
+import android.provider.CallLog
 import io.flutter.embedding.android.FlutterActivity
 import io.flutter.embedding.engine.FlutterEngine
 import io.flutter.plugin.common.MethodChannel
 import com.icing.dialer.KeystoreHelper
 
 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) {
         super.configureFlutterEngine(flutterEngine)
-        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
-            // Delegate method calls to KeystoreHelper
-            KeystoreHelper(call, result).handleMethodCall()
+
+        // Set up the keystore channel.
+        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
     }
 }
diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart
index 6054173..1108a2e 100644
--- a/dialer/lib/features/history/history_page.dart
+++ b/dialer/lib/features/history/history_page.dart
@@ -1,8 +1,11 @@
+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';
@@ -17,12 +20,12 @@ class History {
   final int attempts;
 
   History(
-    this.contact,
-    this.date,
-    this.callType,
-    this.callStatus,
-    this.attempts,
-  );
+      this.contact,
+      this.date,
+      this.callType,
+      this.callStatus,
+      this.attempts,
+      );
 }
 
 class HistoryPage extends StatefulWidget {
@@ -37,9 +40,11 @@ class _HistoryPageState extends State<HistoryPage>
   List<History> histories = [];
   bool loading = true;
   int? _expandedIndex;
-
   final ObfuscateService _obfuscateService = ObfuscateService();
 
+  // Create a MethodChannel instance.
+  static const MethodChannel _channel = MethodChannel('com.example.calllog');
+
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
@@ -51,19 +56,16 @@ class _HistoryPageState extends State<HistoryPage>
   Future<void> _refreshContacts() async {
     final contactState = ContactState.of(context);
     try {
-      // Refresh contacts or fetch them again
       await contactState.fetchContacts();
     } catch (e) {
       print('Error refreshing contacts: $e');
-      ScaffoldMessenger.of(context).showSnackBar(
-        SnackBar(content: Text('Failed to refresh contacts')),
-      );
+      ScaffoldMessenger.of(context)
+          .showSnackBar(SnackBar(content: Text('Failed to refresh contacts')));
     }
   }
 
   void _toggleFavorite(Contact contact) async {
     try {
-      // Ensure you have the necessary permissions to fetch contact details
       if (await FlutterContacts.requestPermission()) {
         Contact? fullContact = await FlutterContacts.getContact(contact.id,
             withProperties: true,
@@ -75,22 +77,68 @@ class _HistoryPageState extends State<HistoryPage>
           fullContact.isStarred = !fullContact.isStarred;
           await FlutterContacts.updateContact(fullContact);
         }
-        await _refreshContacts(); // Refresh the contact list
+        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 contact favorite status')),
-      );
+      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<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 {
+    // 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);
     if (contactState.loading) {
-      // Wait for contacts to be loaded
       await Future.doWhile(() async {
         await Future.delayed(const Duration(milliseconds: 100));
         return contactState.loading;
@@ -98,30 +146,66 @@ class _HistoryPageState extends State<HistoryPage>
     }
     List<Contact> contacts = contactState.contacts;
 
-    if (contacts.isEmpty) {
-      setState(() {
-        loading = false;
-      });
-      return;
+    List<History> 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 = List.generate(
-        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,
-        ),
-      );
+      histories = callHistories;
       loading = false;
     });
   }
 
   List _buildGroupedList(List<History> historyList) {
-    // Sort histories by date (most recent first)
     historyList.sort((a, b) => b.date.compareTo(a.date));
 
     final now = DateTime.now();
@@ -134,7 +218,7 @@ class _HistoryPageState extends State<HistoryPage>
 
     for (var history in historyList) {
       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) {
         todayHistories.add(history);
       } else if (callDate == yesterday) {
@@ -144,7 +228,6 @@ class _HistoryPageState extends State<HistoryPage>
       }
     }
 
-    // Combine them with headers
     final items = <dynamic>[];
     if (todayHistories.isNotEmpty) {
       items.add('Today');
@@ -162,6 +245,28 @@ class _HistoryPageState extends State<HistoryPage>
     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);
@@ -169,9 +274,7 @@ class _HistoryPageState extends State<HistoryPage>
     if (loading || contactState.loading) {
       return Scaffold(
         backgroundColor: Colors.black,
-        body: const Center(
-          child: CircularProgressIndicator(),
-        ),
+        body: const Center(child: CircularProgressIndicator()),
       );
     }
 
@@ -187,9 +290,8 @@ class _HistoryPageState extends State<HistoryPage>
       );
     }
 
-    // Filter missed calls
     List<History> missedCalls =
-        histories.where((h) => h.callStatus == 'missed').toList();
+    histories.where((h) => h.callStatus == 'missed').toList();
 
     final allItems = _buildGroupedList(histories);
     final missedItems = _buildGroupedList(missedCalls);
@@ -213,9 +315,7 @@ class _HistoryPageState extends State<HistoryPage>
         ),
         body: TabBarView(
           children: [
-            // All Calls
             _buildListView(allItems),
-            // Missed Calls
             _buildListView(missedItems),
           ],
         ),
@@ -228,9 +328,7 @@ class _HistoryPageState extends State<HistoryPage>
       itemCount: items.length,
       itemBuilder: (context, index) {
         final item = items[index];
-
         if (item is String) {
-          // This is a header item
           return Container(
             padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
             color: Colors.grey[900],
@@ -246,16 +344,12 @@ class _HistoryPageState extends State<HistoryPage>
           final history = item;
           final contact = history.contact;
           final isExpanded = _expandedIndex == index;
-
-          // Generate the avatar color
           Color avatarColor = generateColorFromName(contact.displayName);
-
           return Column(
             children: [
               ListTile(
                 leading: GestureDetector(
                   onTap: () {
-                    // When the profile picture is tapped, show the ContactModal
                     showModalBottomSheet(
                       context: context,
                       isScrollControlled: true,
@@ -266,8 +360,7 @@ class _HistoryPageState extends State<HistoryPage>
                           onEdit: () async {
                             if (await FlutterContacts.requestPermission()) {
                               final updatedContact =
-                                  await FlutterContacts.openExternalEdit(
-                                      contact.id);
+                              await FlutterContacts.openExternalEdit(contact.id);
                               if (updatedContact != null) {
                                 await _refreshContacts();
                                 Navigator.of(context).pop();
@@ -306,12 +399,14 @@ class _HistoryPageState extends State<HistoryPage>
                   style: const TextStyle(color: Colors.white),
                 ),
                 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),
                 ),
                 trailing: Row(
                   mainAxisSize: MainAxisSize.min,
                   children: [
+                    _getCallIcon(history),
+                    const SizedBox(width: 8),
                     Text(
                       '${history.attempts}x',
                       style: const TextStyle(color: Colors.white),
@@ -320,20 +415,18 @@ class _HistoryPageState extends State<HistoryPage>
                       icon: const Icon(Icons.phone, color: Colors.green),
                       onPressed: () async {
                         if (contact.phones.isNotEmpty) {
-                          final Uri callUri = Uri(
-                              scheme: 'tel', path: contact.phones.first.number);
+                          final Uri callUri =
+                          Uri(scheme: 'tel', path: contact.phones.first.number);
                           if (await canLaunchUrl(callUri)) {
                             await launchUrl(callUri);
                           } else {
                             ScaffoldMessenger.of(context).showSnackBar(
-                              const SnackBar(
-                                  content: Text('Could not launch call')),
+                              const SnackBar(content: Text('Could not launch call')),
                             );
                           }
                         } else {
                           ScaffoldMessenger.of(context).showSnackBar(
-                            const SnackBar(
-                                content: Text('Contact has no phone number')),
+                            const SnackBar(content: Text('Contact has no phone number')),
                           );
                         }
                       },
@@ -351,9 +444,7 @@ class _HistoryPageState extends State<HistoryPage>
                   color: Colors.grey[850],
                   child: FutureBuilder<bool>(
                     future: BlockService().isNumberBlocked(
-                        contact.phones.isNotEmpty
-                            ? contact.phones.first.number
-                            : ''),
+                        contact.phones.isNotEmpty ? contact.phones.first.number : ''),
                     builder: (context, snapshot) {
                       final isBlocked = snapshot.data ?? false;
                       return Row(
@@ -369,37 +460,29 @@ class _HistoryPageState extends State<HistoryPage>
                                   await launchUrl(smsUri);
                                 } else {
                                   ScaffoldMessenger.of(context).showSnackBar(
-                                    const SnackBar(
-                                        content:
-                                            Text('Could not send message')),
+                                    const SnackBar(content: Text('Could not send message')),
                                   );
                                 }
                               } else {
                                 ScaffoldMessenger.of(context).showSnackBar(
-                                  const SnackBar(
-                                      content:
-                                          Text('Contact has no phone number')),
+                                  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)),
+                            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),
+                                  builder: (_) => CallDetailsPage(history: history),
                                 ),
                               );
                             },
                             icon: const Icon(Icons.info, color: Colors.white),
-                            label: const Text('Details',
-                                style: TextStyle(color: Colors.white)),
+                            label: const Text('Details', style: TextStyle(color: Colors.white)),
                           ),
                           TextButton.icon(
                             onPressed: () async {
@@ -408,30 +491,24 @@ class _HistoryPageState extends State<HistoryPage>
                                   : null;
                               if (phoneNumber == null) {
                                 ScaffoldMessenger.of(context).showSnackBar(
-                                  const SnackBar(
-                                      content:
-                                          Text('Contact has no phone number')),
+                                  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')),
+                                  SnackBar(content: Text('$phoneNumber unblocked')),
                                 );
                               } else {
                                 await BlockService().blockNumber(phoneNumber);
                                 ScaffoldMessenger.of(context).showSnackBar(
-                                  SnackBar(
-                                      content: Text('$phoneNumber blocked')),
+                                  SnackBar(content: Text('$phoneNumber blocked')),
                                 );
                               }
-                              setState(() {}); // Refresh the button state
+                              setState(() {});
                             },
-                            icon: Icon(
-                                isBlocked ? Icons.lock_open : Icons.block,
+                            icon: Icon(isBlocked ? Icons.lock_open : Icons.block,
                                 color: Colors.white),
                             label: Text(isBlocked ? 'Unblock' : 'Block',
                                 style: const TextStyle(color: Colors.white)),
@@ -444,7 +521,6 @@ class _HistoryPageState extends State<HistoryPage>
             ],
           );
         }
-
         return const SizedBox.shrink();
       },
     );
@@ -473,27 +549,26 @@ class CallDetailsPage extends StatelessWidget {
         padding: const EdgeInsets.all(16.0),
         child: Column(
           children: [
-            // Display Contact Name and Thumbnail
+            // Display Contact Name and Thumbnail.
             Row(
               children: [
                 (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
                     ? ObfuscatedAvatar(
-                        imageBytes: contact.thumbnail,
-                        radius: 30,
-                        backgroundColor: contactBg,
-                        fallbackInitial: contact.displayName,
-                      )
+                  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),
-                        ),
-                      ),
+                  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(
@@ -504,8 +579,7 @@ class CallDetailsPage extends StatelessWidget {
               ],
             ),
             const SizedBox(height: 24),
-
-            // Display call type, status, date, attempts
+            // Display call details.
             DetailRow(
               label: 'Call Type:',
               value: history.callType,
@@ -522,15 +596,11 @@ class CallDetailsPage extends StatelessWidget {
               label: 'Attempts:',
               value: '${history.attempts}',
             ),
-
             const SizedBox(height: 24),
-
-            // If you have more details like duration, contact number, etc.
             if (contact.phones.isNotEmpty)
               DetailRow(
                 label: 'Number:',
-                value: _obfuscateService
-                    .obfuscateData(contact.phones.first.number),
+                value: _obfuscateService.obfuscateData(contact.phones.first.number),
               ),
           ],
         ),
diff --git a/dialer/pubspec.yaml b/dialer/pubspec.yaml
index b3bbda7..244050a 100644
--- a/dialer/pubspec.yaml
+++ b/dialer/pubspec.yaml
@@ -56,6 +56,8 @@ dependencies:
   uuid: ^4.5.1
   provider: ^6.1.2
 
+  intl: any
+
 dev_dependencies:
   flutter_test:
     sdk: flutter