diff --git a/dialer/android/app/src/main/AndroidManifest.xml b/dialer/android/app/src/main/AndroidManifest.xml index fcaf71f..c2a9779 100644 --- a/dialer/android/app/src/main/AndroidManifest.xml +++ b/dialer/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + when (call.method) { @@ -60,22 +68,31 @@ class MainActivity : FlutterActivity() { } } "hangUpCall" -> { - val success = CallService.hangUpCall(this) + val success = MyInCallService.currentCall?.let { + it.disconnect() + Log.d(TAG, "Call disconnected") + MyInCallService.channel?.invokeMethod("callEnded", mapOf( + "callId" to it.details.handle.toString() + )) + true + } ?: false if (success) { result.success(mapOf("status" to "ended")) } else { - result.error("HANGUP_FAILED", "Failed to end call", null) + Log.w(TAG, "No active call to hang up") + result.error("HANGUP_FAILED", "No active call to hang up", null) } } "answerCall" -> { val success = MyInCallService.currentCall?.let { - it.answer(0) // 0 for default video state (audio-only) + it.answer(0) Log.d(TAG, "Answered call") true } ?: false if (success) { result.success(mapOf("status" to "answered")) } else { + Log.w(TAG, "No active call to answer") result.error("ANSWER_FAILED", "No active call to answer", null) } } @@ -84,13 +101,20 @@ class MainActivity : FlutterActivity() { } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL) - .setMethodCallHandler { call, result -> KeystoreHelper(call, result).handleMethodCall() } + .setMethodCallHandler { call, result -> + KeystoreHelper(call, result).handleMethodCall() + } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL) .setMethodCallHandler { call, result -> if (call.method == "getCallLogs") { - val callLogs = getCallLogs() - result.success(callLogs) + 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() } @@ -148,6 +172,18 @@ class MainActivity : FlutterActivity() { } } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_CODE_CALL_LOG_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Call log permission granted") + MyInCallService.channel?.invokeMethod("callLogPermissionGranted", null) + } else { + Log.w(TAG, "Call log permission denied") + } + } + } + private fun getCallLogs(): List> { val logsList = mutableListOf>() val cursor: Cursor? = contentResolver.query( @@ -171,4 +207,16 @@ class MainActivity : FlutterActivity() { } return logsList } + + private fun handleIncomingCallIntent(intent: Intent?) { + intent?.let { + if (it.getBooleanExtra("isIncomingCall", false)) { + val phoneNumber = it.getStringExtra("phoneNumber") + Log.d(TAG, "Received incoming call intent for $phoneNumber") + MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf( + "phoneNumber" to phoneNumber + )) + } + } + } } \ No newline at end of file diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index 48b7edd..24c922d 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -1,8 +1,16 @@ package com.icing.dialer.services +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build import android.telecom.Call import android.telecom.InCallService import android.util.Log +import androidx.core.app.NotificationCompat +import com.icing.dialer.activities.MainActivity import io.flutter.plugin.common.MethodChannel class MyInCallService : InCallService() { @@ -10,6 +18,8 @@ class MyInCallService : InCallService() { var channel: MethodChannel? = null var currentCall: Call? = null private const val TAG = "MyInCallService" + private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel" + private const val NOTIFICATION_ID = 1 } private val callCallback = object : Call.Callback() { @@ -28,10 +38,13 @@ class MyInCallService : InCallService() { "callId" to call.details.handle.toString(), "state" to stateStr )) - if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) { + if (state == Call.STATE_RINGING) { + showIncomingCallNotification(call.details.handle.toString().replace("tel:", "")) + } else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) { Log.d(TAG, "Call ended: ${call.details.handle}") channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString())) currentCall = null + cancelNotification() } } } @@ -43,13 +56,16 @@ class MyInCallService : InCallService() { Call.STATE_DIALING -> "dialing" Call.STATE_ACTIVE -> "active" Call.STATE_RINGING -> "ringing" - else -> "dialing" // Default for outgoing + else -> "dialing" } Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr") channel?.invokeMethod("callAdded", mapOf( "callId" to call.details.handle.toString(), "state" to stateStr )) + if (stateStr == "ringing") { + showIncomingCallNotification(call.details.handle.toString().replace("tel:", "")) + } call.registerCallback(callCallback) } @@ -59,6 +75,7 @@ class MyInCallService : InCallService() { call.unregisterCallback(callCallback) channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString())) currentCall = null + cancelNotification() } override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) { @@ -66,4 +83,55 @@ class MyInCallService : InCallService() { Log.d(TAG, "Audio state changed: route=${state.route}") channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route)) } + + private fun showIncomingCallNotification(phoneNumber: String) { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Create notification channel (Android 8.0+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Incoming Calls", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Notifications for incoming calls" + enableVibration(true) + setShowBadge(true) + } + notificationManager.createNotificationChannel(channel) + } + + // Intent to open MainActivity with phone number + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra("phoneNumber", phoneNumber) + putExtra("isIncomingCall", true) + } + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) + ) + + // Build notification + val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_alert) // Replace with your app icon + .setContentTitle("Incoming Call") + .setContentText("Call from $phoneNumber") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setOngoing(true) // Keep visible until call ends + .build() + + notificationManager.notify(NOTIFICATION_ID, notification) + Log.d(TAG, "Notification shown for incoming call from $phoneNumber") + } + + private fun cancelNotification() { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_ID) + Log.d(TAG, "Notification canceled") + } } \ No newline at end of file diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 1416a98..368ffde 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -61,9 +61,16 @@ class _CallPageState extends State { void _hangUp() async { try { - await _callService.hangUpCall(context); + final result = await _callService.hangUpCall(context); + print('CallPage: Hang up result: $result'); + if (result["status"] == "ended" && mounted && Navigator.canPop(context)) { + Navigator.pop(context); + } } catch (e) { - print("Error hanging up: $e"); + print("CallPage: Error hanging up: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error hanging up: $e")), + ); } } @@ -86,9 +93,10 @@ class _CallPageState extends State { children: [ SizedBox(height: 35), ObfuscatedAvatar( - imageBytes: widget.thumbnail, // Uses thumbnail if provided + imageBytes: widget.thumbnail, radius: avatarRadius, - backgroundColor: generateColorFromName(widget.displayName), + backgroundColor: + generateColorFromName(widget.displayName), fallbackInitial: widget.displayName, ), const SizedBox(height: 4), @@ -122,11 +130,13 @@ class _CallPageState extends State { ), Text( widget.phoneNumber, - style: TextStyle(fontSize: statusFontSize, color: Colors.white70), + style: TextStyle( + fontSize: statusFontSize, color: Colors.white70), ), Text( 'Calling...', - style: TextStyle(fontSize: statusFontSize, color: Colors.white70), + style: TextStyle( + fontSize: statusFontSize, color: Colors.white70), ), ], ), @@ -157,7 +167,8 @@ class _CallPageState extends State { IconButton( padding: EdgeInsets.zero, onPressed: _toggleKeypad, - icon: const Icon(Icons.close, color: Colors.white), + icon: + const Icon(Icons.close, color: Colors.white), ), ], ), @@ -193,7 +204,8 @@ class _CallPageState extends State { child: Center( child: Text( label, - style: const TextStyle(fontSize: 32, color: Colors.white), + style: const TextStyle( + fontSize: 32, color: Colors.white), ), ), ), @@ -225,7 +237,8 @@ class _CallPageState extends State { ), Text( isMuted ? 'Unmute' : 'Mute', - style: const TextStyle(color: Colors.white, fontSize: 14), + style: const TextStyle( + color: Colors.white, fontSize: 14), ), ], ), @@ -234,11 +247,13 @@ class _CallPageState extends State { children: [ IconButton( onPressed: _toggleKeypad, - icon: const Icon(Icons.dialpad, color: Colors.white, size: 32), + icon: const Icon(Icons.dialpad, + color: Colors.white, size: 32), ), const Text( 'Keypad', - style: TextStyle(color: Colors.white, fontSize: 14), + style: TextStyle( + color: Colors.white, fontSize: 14), ), ], ), @@ -248,14 +263,19 @@ class _CallPageState extends State { IconButton( onPressed: _toggleSpeaker, icon: Icon( - isSpeakerOn ? Icons.volume_up : Icons.volume_off, - color: isSpeakerOn ? Colors.amber : Colors.white, + isSpeakerOn + ? Icons.volume_up + : Icons.volume_off, + color: isSpeakerOn + ? Colors.amber + : Colors.white, size: 32, ), ), const Text( 'Speaker', - style: TextStyle(color: Colors.white, fontSize: 14), + style: TextStyle( + color: Colors.white, fontSize: 14), ), ], ), @@ -270,10 +290,12 @@ class _CallPageState extends State { children: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.person_add, color: Colors.white, size: 32), + icon: const Icon(Icons.person_add, + color: Colors.white, size: 32), ), const Text('Add Contact', - style: TextStyle(color: Colors.white, fontSize: 14)), + style: TextStyle( + color: Colors.white, fontSize: 14)), ], ), Column( @@ -281,10 +303,12 @@ class _CallPageState extends State { children: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.sim_card, color: Colors.white, size: 32), + icon: const Icon(Icons.sim_card, + color: Colors.white, size: 32), ), const Text('Change SIM', - style: TextStyle(color: Colors.white, fontSize: 14)), + style: TextStyle( + color: Colors.white, fontSize: 14)), ], ), ], @@ -321,4 +345,4 @@ class _CallPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index d42326e..5975210 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; -import '../features/call/incoming_call_page.dart'; // Import the new page +import '../features/call/incoming_call_page.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); @@ -13,7 +13,6 @@ class CallService { CallService() { _channel.setMethodCallHandler((call) async { final context = navigatorKey.currentContext; - print('CallService: Received method ${call.method} with args ${call.arguments}'); if (context == null) { print('CallService: Navigator context is null, cannot navigate'); return; @@ -48,17 +47,19 @@ class CallService { _closeCallPage(context); currentPhoneNumber = null; break; + case "incomingCallFromNotification": + final phoneNumber = call.arguments["phoneNumber"] as String; + currentPhoneNumber = phoneNumber; + print('CallService: Incoming call from notification: $phoneNumber'); + _navigateToIncomingCallPage(context); + break; } }); } void _navigateToCallPage(BuildContext context) { - if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { - print('CallService: CallPage already visible, skipping navigation'); - return; - } print('CallService: Navigating to CallPage'); - Navigator.pushReplacement( + Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: '/call'), @@ -70,15 +71,12 @@ class CallService { ), ).then((_) { _isCallPageVisible = false; + print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { - if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { - print('CallService: IncomingCallPage already visible, skipping navigation'); - return; - } print('CallService: Navigating to IncomingCallPage'); Navigator.push( context, @@ -92,23 +90,23 @@ class CallService { ), ).then((_) { _isCallPageVisible = false; + print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _closeCallPage(BuildContext context) { - if (!_isCallPageVisible) { - print('CallService: CallPage not visible, skipping pop'); - return; - } + print('CallService: Attempting to close call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { - print('CallService: Popping CallPage'); + print('CallService: Popping call page'); Navigator.pop(context); _isCallPageVisible = false; + } else { + print('CallService: No page to pop'); } } - Future makeGsmCall( + Future> makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -119,11 +117,13 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - if (result["status"] != "calling") { + final resultMap = Map.from(result as Map); // Safe cast + if (resultMap["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); } + return resultMap; } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( @@ -133,16 +133,18 @@ class CallService { } } - Future hangUpCall(BuildContext context) async { + Future> hangUpCall(BuildContext context) async { try { print('CallService: Hanging up call'); final result = await _channel.invokeMethod('hangUpCall'); print('CallService: hangUpCall result: $result'); - if (result["status"] != "ended") { + final resultMap = Map.from(result as Map); // Safe cast + if (resultMap["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } + return resultMap; } catch (e) { print("CallService: Error hanging up call: $e"); ScaffoldMessenger.of(context).showSnackBar(