From c9ebd7b92f228462f557006fcfb75482e55be235 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:08:00 +0200 Subject: [PATCH] feat: enhance history page with lifecycle management and background loading --- .../features/history/history_page.dart | 178 ++++++++++++++++-- .../presentation/features/home/home_page.dart | 15 +- 2 files changed, 172 insertions(+), 21 deletions(-) diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index efd9afd..968acfb 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -33,16 +33,18 @@ class HistoryPage extends StatefulWidget { const HistoryPage({Key? key}) : super(key: key); @override - _HistoryPageState createState() => _HistoryPageState(); + HistoryPageState createState() => HistoryPageState(); } -class _HistoryPageState extends State - with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { +class HistoryPageState extends State + with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver { List histories = []; - bool loading = true; + bool _isInitialLoad = true; + bool _isBackgroundLoading = false; int? _expandedIndex; final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); + Timer? _debounceTimer; // Create a MethodChannel instance. static const MethodChannel _channel = MethodChannel('com.example.calllog'); @@ -50,12 +52,52 @@ class _HistoryPageState extends State @override bool get wantKeepAlive => true; // Preserve state when switching pages + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + // Load initial data + _buildHistories(); + } + + /// Public method to trigger reload when page becomes visible + void triggerReload() { + if (!_isInitialLoad && !_isBackgroundLoading) { + _debouncedReload(); + } + } + + @override + void dispose() { + _debounceTimer?.cancel(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + // Reload data when app comes back to foreground + if (state == AppLifecycleState.resumed && !_isInitialLoad && !_isBackgroundLoading) { + _debouncedReload(); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); - if (loading && histories.isEmpty) { - _buildHistories(); - } + // didChangeDependencies is not reliable for TabBarView changes + // 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 { @@ -130,10 +172,12 @@ class _HistoryPageState extends State // Request permission. bool hasPermission = await _requestCallLogPermission(); if (!hasPermission) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Call log permission not granted'))); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Call log permission not granted'))); + } setState(() { - loading = false; + _isInitialLoad = false; }); return; } @@ -212,10 +256,112 @@ class _HistoryPageState extends State // Sort histories by most recent. callHistories.sort((a, b) => b.date.compareTo(a.date)); - setState(() { - histories = callHistories; - loading = false; - }); + if (mounted) { + setState(() { + histories = callHistories; + _isInitialLoad = false; + }); + } + } + + /// Reload histories in the background without showing loading indicators + Future _reloadHistoriesInBackground() async { + if (_isBackgroundLoading) return; + + _isBackgroundLoading = true; + + try { + // Request permission. + bool hasPermission = await _requestCallLogPermission(); + if (!hasPermission) { + _isBackgroundLoading = false; + return; + } + + // Retrieve call logs from native code. + List nativeLogs = []; + try { + nativeLogs = await _channel.invokeMethod('getCallLogs'); + } on PlatformException catch (e) { + print("Error fetching call logs: ${e.message}"); + _isBackgroundLoading = false; + return; + } + + // Ensure contacts are loaded. + final contactState = ContactState.of(context); + if (contactState.loading) { + await Future.doWhile(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return contactState.loading; + }); + } + List contacts = contactState.contacts; + + List callHistories = []; + // Process each log entry 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(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)); + // Yield every 10 iterations to avoid blocking the UI. + if (i % 10 == 0) await Future.delayed(Duration(milliseconds: 1)); + } + + // Sort histories by most recent. + callHistories.sort((a, b) => b.date.compareTo(a.date)); + + if (mounted) { + setState(() { + histories = callHistories; + }); + } + } finally { + _isBackgroundLoading = false; + } } List _buildGroupedList(List historyList) { @@ -283,9 +429,9 @@ class _HistoryPageState extends State @override Widget build(BuildContext context) { super.build(context); // required due to AutomaticKeepAliveClientMixin - final contactState = ContactState.of(context); - if (loading || contactState.loading) { + // Show loading only on initial load and if no data is available yet + if (_isInitialLoad && histories.isEmpty) { return Scaffold( backgroundColor: Colors.black, body: const Center(child: CircularProgressIndicator()), diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index 95ca9eb..fd136b6 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -19,6 +19,7 @@ class _MyHomePageState extends State with SingleTickerProviderStateM final TextEditingController _searchController = TextEditingController(); late SearchController _searchBarController; String _rawSearchInput = ''; + final GlobalKey _historyPageKey = GlobalKey(); @override void initState() { @@ -93,6 +94,10 @@ class _MyHomePageState extends State with SingleTickerProviderStateM void _handleTabIndex() { setState(() {}); + // Trigger history page reload when switching to history tab (index 1) + if (_tabController.index == 1) { + _historyPageKey.currentState?.triggerReload(); + } } void _toggleFavorite(Contact contact) async { @@ -270,11 +275,11 @@ class _MyHomePageState extends State with SingleTickerProviderStateM children: [ TabBarView( controller: _tabController, - children: const [ - FavoritesPage(), - HistoryPage(), - ContactPage(), - VoicemailPage(), + children: [ + const FavoritesPage(), + HistoryPage(key: _historyPageKey), + const ContactPage(), + const VoicemailPage(), ], ), Positioned(