diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index e157dc1..ea618d6 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -230,16 +230,25 @@ class MainActivity : FlutterActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL) .setMethodCallHandler { call, result -> - if (call.method == "getCallLogs") { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) { - val callLogs = getCallLogs() - result.success(callLogs) - } else { - requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION) - result.error("PERMISSION_DENIED", "Call log permission not granted", null) + when (call.method) { + "getCallLogs" -> { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) { + val callLogs = getCallLogs() + result.success(callLogs) + } else { + requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION) + result.error("PERMISSION_DENIED", "Call log permission not granted", null) + } } - } else { - result.notImplemented() + "getLatestCallLog" -> { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) { + val latestCallLog = getLatestCallLog() + result.success(latestCallLog) + } else { + result.error("PERMISSION_DENIED", "Call log permission not granted", null) + } + } + else -> result.notImplemented() } } } @@ -354,6 +363,50 @@ class MainActivity : FlutterActivity() { return logsList } + private fun getLatestCallLog(): Map? { + val cursor: Cursor? = contentResolver.query( + CallLog.Calls.CONTENT_URI, + null, + null, + null, + CallLog.Calls.DATE + " DESC" + ) + cursor?.use { + if (it.moveToNext()) { + val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER)) + val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE)) + val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE)) + val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION)) + + // Extract subscription ID (SIM card info) if available + var subscriptionId: Int? = null + var simName: String? = null + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + val subIdColumnIndex = it.getColumnIndex("subscription_id") + if (subIdColumnIndex >= 0) { + subscriptionId = it.getInt(subIdColumnIndex) + // Get the actual SIM name + simName = getSimNameFromSubscriptionId(subscriptionId) + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to get subscription_id: ${e.message}") + } + + return mapOf( + "number" to number, + "type" to type, + "date" to date, + "duration" to duration, + "subscription_id" to subscriptionId, + "sim_name" to simName + ) + } + } + return null + } + private fun getSimNameFromSubscriptionId(subscriptionId: Int?): String? { if (subscriptionId == null) return null diff --git a/dialer/lib/domain/services/call_service.dart b/dialer/lib/domain/services/call_service.dart index 1113afd..f641dc6 100644 --- a/dialer/lib/domain/services/call_service.dart +++ b/dialer/lib/domain/services/call_service.dart @@ -5,6 +5,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../presentation/features/call/call_page.dart'; import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page import 'contact_service.dart'; +// Import for history update callback +import '../../presentation/features/history/history_page.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); @@ -114,6 +116,13 @@ class CallService { if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } + + // Notify history page to add the latest call + // Add a small delay to ensure call log is updated by the system + Timer(const Duration(milliseconds: 500), () { + HistoryPageState.addNewCallToHistory(); + }); + _activeCallNumber = null; // Handle pending SIM switch after call is disconnected _handlePendingSimSwitch(); @@ -179,6 +188,13 @@ class CallService { if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } + + // Notify history page to add the latest call + // Add a small delay to ensure call log is updated by the system + Timer(const Duration(milliseconds: 500), () { + HistoryPageState.addNewCallToHistory(); + }); + currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index 868ac17..6acd892 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -40,9 +40,13 @@ class HistoryPage extends StatefulWidget { class HistoryPageState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver { - List histories = []; + // Static histories list shared across all instances + static List _globalHistories = []; + + // Getter to access the global histories list + List get histories => _globalHistories; + bool _isInitialLoad = true; - bool _isBackgroundLoading = false; int? _expandedIndex; final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); @@ -50,6 +54,12 @@ class HistoryPageState extends State // Create a MethodChannel instance. static const MethodChannel _channel = MethodChannel('com.example.calllog'); + + // Static reference to the current instance for call-end notifications + static HistoryPageState? _currentInstance; + + // Global flag to track if history has been loaded once across all instances + static bool _hasLoadedInitialHistory = false; @override bool get wantKeepAlive => true; // Preserve state when switching pages @@ -58,31 +68,50 @@ class HistoryPageState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - // Load initial data - _buildHistories(); + _currentInstance = this; // Register this instance + + // Only load initial data if it hasn't been loaded before + if (!_hasLoadedInitialHistory) { + _buildHistories(); + } else { + // If history was already loaded, just mark this instance as not doing initial load + _isInitialLoad = false; + } } /// Public method to trigger reload when page becomes visible void triggerReload() { - if (!_isInitialLoad && !_isBackgroundLoading) { - _debouncedReload(); - } + // Disabled automatic reloading - only load once and add new entries via addNewCallToHistory + print("HistoryPage: triggerReload called but disabled to prevent full reload"); } @override void dispose() { _debounceTimer?.cancel(); WidgetsBinding.instance.removeObserver(this); + if (_currentInstance == this) { + _currentInstance = null; // Unregister this instance + } super.dispose(); } + /// Static method to add a new call to the history list + static void addNewCallToHistory() { + _currentInstance?._addLatestCallToHistory(); + } + + /// Notify all instances to refresh UI when history changes + static void _notifyHistoryChanged() { + _currentInstance?.setState(() { + // Trigger UI rebuild for the current instance + }); + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - // Reload data when app comes back to foreground - if (state == AppLifecycleState.resumed && !_isInitialLoad && !_isBackgroundLoading) { - _debouncedReload(); - } + // Disabled automatic reloading when app comes to foreground + print("HistoryPage: didChangeAppLifecycleState called but disabled to prevent full reload"); } @override @@ -92,16 +121,6 @@ class HistoryPageState extends State // We'll use a different approach with RouteAware or manual detection } - /// Debounced reload to prevent multiple rapid reloads - void _debouncedReload() { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 300), () { - if (mounted && !_isBackgroundLoading) { - _reloadHistoriesInBackground(); - } - }); - } - Future _refreshContacts() async { final contactState = ContactState.of(context); try { @@ -290,37 +309,35 @@ class HistoryPageState extends State if (mounted) { setState(() { - histories = callHistories; + _globalHistories = callHistories; _isInitialLoad = false; + _hasLoadedInitialHistory = true; // Mark that history has been loaded once }); + // Notify other instances about the initial load + _notifyHistoryChanged(); } } - /// Reload histories in the background without showing loading indicators - Future _reloadHistoriesInBackground() async { - if (_isBackgroundLoading) return; - - _isBackgroundLoading = true; - + /// Add the latest call log entry to the history list + Future _addLatestCallToHistory() async { try { - // Request permission. - bool hasPermission = await _requestCallLogPermission(); - if (!hasPermission) { - _isBackgroundLoading = false; + // Get the latest call log entry + final dynamic rawEntry = await _channel.invokeMethod('getLatestCallLog'); + + if (rawEntry == null) { + print("No latest call log entry found"); 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}"); - _isBackgroundLoading = false; - return; - } + // Convert to proper type - handle the method channel result properly + final Map latestEntry = Map.from( + (rawEntry as Map).cast() + ); - // Ensure contacts are loaded. + final String number = latestEntry['number'] ?? ''; + if (number.isEmpty) return; + + // Ensure contacts are loaded final contactState = ContactState.of(context); if (contactState.loading) { await Future.doWhile(() async { @@ -330,83 +347,80 @@ class HistoryPageState extends State } List contacts = contactState.contacts; - List callHistories = []; - // Process each log entry with intermittent yields to avoid freezing. - for (int i = 0; i < nativeLogs.length; i++) { - final entry = nativeLogs[i]; - final String number = entry['number'] ?? ''; - if (number.isEmpty) continue; + // Convert timestamp to DateTime + DateTime callDate = DateTime.fromMillisecondsSinceEpoch(latestEntry['date'] ?? 0); - // Convert timestamp to DateTime. - DateTime callDate = - DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0); + int typeInt = latestEntry['type'] ?? 0; + int duration = latestEntry['duration'] ?? 0; + String callType; + String callStatus; - 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)], - ); - } - - // Extract SIM information if available - String? simName; - if (entry.containsKey('sim_name') && entry['sim_name'] != null) { - simName = entry['sim_name'] as String; - print("DEBUG: Found sim_name: $simName for number: $number"); // Debug print - } else if (entry.containsKey('subscription_id')) { - final subId = entry['subscription_id']; - print("DEBUG: Found subscription_id: $subId for number: $number, but no sim_name"); // Debug print - simName = _getSimNameFromSubscriptionId(subId); - print("DEBUG: Mapped to SIM name: $simName"); // Debug print - } else { - print("DEBUG: No SIM info found for number: $number"); // Debug print - } - - callHistories - .add(History(matchedContact, callDate, callType, callStatus, 1, simName)); - // Yield every 10 iterations to avoid blocking the UI. - if (i % 10 == 0) await Future.delayed(Duration(milliseconds: 1)); + // Map integer values to call type/status + 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"; } - // Sort histories by most recent. - callHistories.sort((a, b) => b.date.compareTo(a.date)); + // 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)], + ); + } - if (mounted) { + // Extract SIM information if available + String? simName; + if (latestEntry.containsKey('sim_name') && latestEntry['sim_name'] != null) { + simName = latestEntry['sim_name'] as String; + print("DEBUG: Found sim_name: $simName for number: $number"); + } else if (latestEntry.containsKey('subscription_id')) { + final subId = latestEntry['subscription_id']; + print("DEBUG: Found subscription_id: $subId for number: $number, but no sim_name"); + simName = _getSimNameFromSubscriptionId(subId); + print("DEBUG: Mapped to SIM name: $simName"); + } else { + print("DEBUG: No SIM info found for number: $number"); + } + + // Create new history entry + History newHistory = History(matchedContact, callDate, callType, callStatus, 1, simName); + + // Check if this call is already in the list (avoid duplicates) + bool alreadyExists = _globalHistories.any((history) => + history.contact.phones.isNotEmpty && + sanitizeNumber(history.contact.phones.first.number) == sanitizeNumber(number) && + history.date.difference(callDate).abs().inSeconds < 5); // Within 5 seconds + + if (!alreadyExists && mounted) { setState(() { - histories = callHistories; + // Insert at the beginning since it's the most recent + _globalHistories.insert(0, newHistory); }); + // Notify other instances about the change + _notifyHistoryChanged(); + print("Added new call to history: $number at $callDate"); + } else { + print("Call already exists in history or widget unmounted"); } - } finally { - _isBackgroundLoading = false; + } catch (e) { + print("Error adding latest call to history: $e"); } }