feat: enhance history page with lifecycle management and background loading
This commit is contained in:
parent
dd12b523cc
commit
c9ebd7b92f
@ -33,16 +33,18 @@ class HistoryPage extends StatefulWidget {
|
|||||||
const HistoryPage({Key? key}) : super(key: key);
|
const HistoryPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_HistoryPageState createState() => _HistoryPageState();
|
HistoryPageState createState() => HistoryPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryPageState extends State<HistoryPage>
|
class HistoryPageState extends State<HistoryPage>
|
||||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
List<History> histories = [];
|
List<History> histories = [];
|
||||||
bool loading = true;
|
bool _isInitialLoad = true;
|
||||||
|
bool _isBackgroundLoading = false;
|
||||||
int? _expandedIndex;
|
int? _expandedIndex;
|
||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
final CallService _callService = CallService();
|
final CallService _callService = CallService();
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
|
||||||
// Create a MethodChannel instance.
|
// Create a MethodChannel instance.
|
||||||
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
||||||
@ -50,12 +52,52 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true; // Preserve state when switching pages
|
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
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
if (loading && histories.isEmpty) {
|
// didChangeDependencies is not reliable for TabBarView changes
|
||||||
_buildHistories();
|
// 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<void> _refreshContacts() async {
|
Future<void> _refreshContacts() async {
|
||||||
@ -130,10 +172,12 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
// Request permission.
|
// Request permission.
|
||||||
bool hasPermission = await _requestCallLogPermission();
|
bool hasPermission = await _requestCallLogPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(content: Text('Call log permission not granted')));
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Call log permission not granted')));
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
loading = false;
|
_isInitialLoad = false;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -212,10 +256,112 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
// Sort histories by most recent.
|
// Sort histories by most recent.
|
||||||
callHistories.sort((a, b) => b.date.compareTo(a.date));
|
callHistories.sort((a, b) => b.date.compareTo(a.date));
|
||||||
|
|
||||||
setState(() {
|
if (mounted) {
|
||||||
histories = callHistories;
|
setState(() {
|
||||||
loading = false;
|
histories = callHistories;
|
||||||
});
|
_isInitialLoad = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload histories in the background without showing loading indicators
|
||||||
|
Future<void> _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<dynamic> 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<Contact> contacts = contactState.contacts;
|
||||||
|
|
||||||
|
List<History> 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<History> historyList) {
|
List _buildGroupedList(List<History> historyList) {
|
||||||
@ -283,9 +429,9 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context); // required due to AutomaticKeepAliveClientMixin
|
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(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
|
@ -19,6 +19,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
|||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
late SearchController _searchBarController;
|
late SearchController _searchBarController;
|
||||||
String _rawSearchInput = '';
|
String _rawSearchInput = '';
|
||||||
|
final GlobalKey<HistoryPageState> _historyPageKey = GlobalKey<HistoryPageState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -93,6 +94,10 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
void _handleTabIndex() {
|
void _handleTabIndex() {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
// Trigger history page reload when switching to history tab (index 1)
|
||||||
|
if (_tabController.index == 1) {
|
||||||
|
_historyPageKey.currentState?.triggerReload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleFavorite(Contact contact) async {
|
void _toggleFavorite(Contact contact) async {
|
||||||
@ -270,11 +275,11 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
TabBarView(
|
TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: const [
|
children: [
|
||||||
FavoritesPage(),
|
const FavoritesPage(),
|
||||||
HistoryPage(),
|
HistoryPage(key: _historyPageKey),
|
||||||
ContactPage(),
|
const ContactPage(),
|
||||||
VoicemailPage(),
|
const VoicemailPage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
Loading…
Reference in New Issue
Block a user