feat: add method to retrieve latest call log and update history, enhancing call tracking
This commit is contained in:
parent
355e040322
commit
34d5990a3b
@ -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<String, Any?>? {
|
||||
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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -40,9 +40,13 @@ class HistoryPage extends StatefulWidget {
|
||||
|
||||
class HistoryPageState extends State<HistoryPage>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
List<History> histories = [];
|
||||
// Static histories list shared across all instances
|
||||
static List<History> _globalHistories = [];
|
||||
|
||||
// Getter to access the global histories list
|
||||
List<History> 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<HistoryPage>
|
||||
|
||||
// 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<HistoryPage>
|
||||
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<HistoryPage>
|
||||
// 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 {
|
||||
final contactState = ContactState.of(context);
|
||||
try {
|
||||
@ -290,37 +309,35 @@ class HistoryPageState extends State<HistoryPage>
|
||||
|
||||
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<void> _reloadHistoriesInBackground() async {
|
||||
if (_isBackgroundLoading) return;
|
||||
|
||||
_isBackgroundLoading = true;
|
||||
|
||||
/// Add the latest call log entry to the history list
|
||||
Future<void> _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<dynamic> 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<String, dynamic> latestEntry = Map<String, dynamic>.from(
|
||||
(rawEntry as Map<Object?, Object?>).cast<String, dynamic>()
|
||||
);
|
||||
|
||||
// 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<HistoryPage>
|
||||
}
|
||||
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(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");
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user