Compare commits

...

13 Commits

Author SHA1 Message Date
0be14978a0 feat: merged
Some checks failed
/ mirror (push) Successful in 5s
/ build-stealth (push) Failing after 4m40s
/ build (push) Failing after 4m40s
2025-04-17 16:47:11 +03:00
6144c71dda feat: merge call_service with callNotifications changes
Some checks failed
/ mirror (push) Successful in 4s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-04-17 16:45:16 +03:00
e6efe0161c fix: no screen stacking when making calls 2025-04-17 16:45:11 +03:00
2af6f43480 feat: give parameters to callPage to avoid fetching when possible 2025-04-17 16:45:01 +03:00
ba2c1b049f feat: fetch contact in makeGsmCall to show it in call 2025-04-17 16:42:35 +03:00
e6b19c63e1 callNotifications and various fix related to calls (#49)
Reviewed-on: #49
Co-authored-by: florian <florian.griffon@epitech.eu>
Co-committed-by: florian <florian.griffon@epitech.eu>
2025-04-17 16:41:37 +03:00
6628dd6172 feat: merge call_service with callNotifications changes
Some checks failed
/ mirror (push) Successful in 6s
/ build-stealth (push) Failing after 4m29s
/ build (push) Failing after 4m32s
2025-04-17 16:07:08 +03:00
25c943c50d fix: no screen stacking when making calls
Some checks failed
/ mirror (push) Successful in 4s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-04-17 16:04:18 +03:00
69e0bbab13 feat: give parameters to callPage to avoid fetching when possible 2025-04-17 16:04:18 +03:00
24f3e6903a callNotifications and various fix related to calls (#49)
Reviewed-on: #49
Co-authored-by: florian <florian.griffon@epitech.eu>
Co-committed-by: florian <florian.griffon@epitech.eu>
2025-04-17 16:03:57 +03:00
f491fb61e0 fix: no screen stacking when making calls
All checks were successful
/ mirror (push) Successful in 4s
2025-04-17 15:55:49 +03:00
acbccaac74 feat: give parameters to callPage to avoid fetching when possible 2025-04-17 15:55:49 +03:00
20d8c9643f feat: fetch contact in makeGsmCall to show it in call 2025-04-17 15:55:49 +03:00
7 changed files with 416 additions and 72 deletions

View File

@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" /> <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" /> <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application <application
android:label="Icing Dialer" android:label="Icing Dialer"

View File

@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.CallLog import android.provider.CallLog
@ -26,23 +25,76 @@ class MainActivity : FlutterActivity() {
private val CALL_CHANNEL = "call_service" private val CALL_CHANNEL = "call_service"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001 private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002
private var pendingIncomingCall: Pair<String?, Boolean>? = null
private var wasPhoneLocked: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started") Log.d(TAG, "onCreate started")
Log.d(TAG, "Waiting for Flutter to signal permissions") wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false)
Log.d(TAG, "Was phone locked at start: $wasPhoneLocked")
updateLockScreenFlags(intent)
handleIncomingCallIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false)
Log.d(TAG, "onNewIntent, wasPhoneLocked: $wasPhoneLocked")
updateLockScreenFlags(intent)
handleIncomingCallIntent(intent)
}
private fun updateLockScreenFlags(intent: Intent?) {
val isIncomingCall = intent?.getBooleanExtra("isIncomingCall", false) ?: false
if (isIncomingCall && wasPhoneLocked) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
} else {
@Suppress("DEPRECATION")
window.addFlags(
android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
)
}
Log.d(TAG, "Enabled showWhenLocked and turnScreenOn for incoming call")
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)
} else {
@Suppress("DEPRECATION")
window.clearFlags(
android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
)
}
Log.d(TAG, "Disabled showWhenLocked and turnScreenOn for normal usage")
}
} }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
Log.d(TAG, "Configuring Flutter engine") Log.d(TAG, "Configuring Flutter engine")
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL) MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"permissionsGranted" -> { "permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter") Log.d(TAG, "Received permissionsGranted from Flutter")
pendingIncomingCall?.let { (phoneNumber, showScreen) ->
if (showScreen) {
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
"phoneNumber" to phoneNumber,
"wasPhoneLocked" to wasPhoneLocked
))
pendingIncomingCall = null
}
}
checkAndRequestDefaultDialer() checkAndRequestDefaultDialer()
result.success(true) result.success(true)
} }
@ -60,37 +112,66 @@ class MainActivity : FlutterActivity() {
} }
} }
"hangUpCall" -> { "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(),
"wasPhoneLocked" to wasPhoneLocked
))
true
} ?: false
if (success) { if (success) {
result.success(mapOf("status" to "ended")) result.success(mapOf("status" to "ended"))
if (wasPhoneLocked) {
Log.d(TAG, "Finishing and removing task after hangup, phone was locked")
finishAndRemoveTask()
}
} else { } 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" -> { "answerCall" -> {
val success = MyInCallService.currentCall?.let { val success = MyInCallService.currentCall?.let {
it.answer(0) // 0 for default video state (audio-only) it.answer(0)
Log.d(TAG, "Answered call") Log.d(TAG, "Answered call")
true true
} ?: false } ?: false
if (success) { if (success) {
result.success(mapOf("status" to "answered")) result.success(mapOf("status" to "answered"))
} else { } else {
Log.w(TAG, "No active call to answer")
result.error("ANSWER_FAILED", "No active call to answer", null) result.error("ANSWER_FAILED", "No active call to answer", null)
} }
} }
"callEndedFromFlutter" -> {
Log.d(TAG, "Call ended from Flutter, wasPhoneLocked: $wasPhoneLocked")
if (wasPhoneLocked) {
finishAndRemoveTask()
Log.d(TAG, "Finishing and removing task after call ended, phone was locked")
}
result.success(true)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL) 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) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") { if (call.method == "getCallLogs") {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
val callLogs = getCallLogs() val callLogs = getCallLogs()
result.success(callLogs) 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 { } else {
result.notImplemented() result.notImplemented()
} }
@ -109,8 +190,6 @@ class MainActivity : FlutterActivity() {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER) val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER) startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+") Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
} else {
Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
} }
} else { } else {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER) val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
@ -148,6 +227,18 @@ class MainActivity : FlutterActivity() {
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 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<Map<String, Any?>> { private fun getCallLogs(): List<Map<String, Any?>> {
val logsList = mutableListOf<Map<String, Any?>>() val logsList = mutableListOf<Map<String, Any?>>()
val cursor: Cursor? = contentResolver.query( val cursor: Cursor? = contentResolver.query(
@ -171,4 +262,25 @@ class MainActivity : FlutterActivity() {
} }
return logsList return logsList
} }
private fun handleIncomingCallIntent(intent: Intent?) {
intent?.let {
if (it.getBooleanExtra("isIncomingCall", false)) {
val phoneNumber = it.getStringExtra("phoneNumber")
val showScreen = it.getBooleanExtra("showIncomingCallScreen", false)
Log.d(TAG, "Received incoming call intent for $phoneNumber, showScreen=$showScreen, wasPhoneLocked=$wasPhoneLocked")
if (showScreen) {
if (MyInCallService.channel != null) {
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
"phoneNumber" to phoneNumber,
"wasPhoneLocked" to wasPhoneLocked
))
} else {
pendingIncomingCall = Pair(phoneNumber, true)
Log.d(TAG, "Flutter channel not ready, storing pending call: $phoneNumber")
}
}
}
}
}
} }

View File

@ -1,8 +1,17 @@
package com.icing.dialer.services package com.icing.dialer.services
import android.app.KeyguardManager
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.Call
import android.telecom.InCallService import android.telecom.InCallService
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import com.icing.dialer.activities.MainActivity
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class MyInCallService : InCallService() { class MyInCallService : InCallService() {
@ -10,6 +19,9 @@ class MyInCallService : InCallService() {
var channel: MethodChannel? = null var channel: MethodChannel? = null
var currentCall: Call? = null var currentCall: Call? = null
private const val TAG = "MyInCallService" private const val TAG = "MyInCallService"
private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel"
private const val NOTIFICATION_ID = 1
var wasPhoneLocked: Boolean = false
} }
private val callCallback = object : Call.Callback() { private val callCallback = object : Call.Callback() {
@ -26,12 +38,22 @@ class MyInCallService : InCallService() {
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}") Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
channel?.invokeMethod("callStateChanged", mapOf( channel?.invokeMethod("callStateChanged", mapOf(
"callId" to call.details.handle.toString(), "callId" to call.details.handle.toString(),
"state" to stateStr "state" to stateStr,
"wasPhoneLocked" to wasPhoneLocked
))
if (state == Call.STATE_RINGING) {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
wasPhoneLocked = keyguardManager.isKeyguardLocked
Log.d(TAG, "Phone locked at ringing: $wasPhoneLocked")
showIncomingCallScreen(call.details.handle.toString().replace("tel:", ""))
} else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
Log.d(TAG, "Call ended: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked")
channel?.invokeMethod("callEnded", mapOf(
"callId" to call.details.handle.toString(),
"wasPhoneLocked" to wasPhoneLocked
)) ))
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 currentCall = null
cancelNotification()
} }
} }
} }
@ -43,22 +65,32 @@ class MyInCallService : InCallService() {
Call.STATE_DIALING -> "dialing" Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active" Call.STATE_ACTIVE -> "active"
Call.STATE_RINGING -> "ringing" Call.STATE_RINGING -> "ringing"
else -> "dialing" // Default for outgoing else -> "dialing"
} }
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr") Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
channel?.invokeMethod("callAdded", mapOf( channel?.invokeMethod("callAdded", mapOf(
"callId" to call.details.handle.toString(), "callId" to call.details.handle.toString(),
"state" to stateStr "state" to stateStr
)) ))
if (stateStr == "ringing") {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
wasPhoneLocked = keyguardManager.isKeyguardLocked
Log.d(TAG, "Phone locked at call added: $wasPhoneLocked")
showIncomingCallScreen(call.details.handle.toString().replace("tel:", ""))
}
call.registerCallback(callCallback) call.registerCallback(callCallback)
} }
override fun onCallRemoved(call: Call) { override fun onCallRemoved(call: Call) {
super.onCallRemoved(call) super.onCallRemoved(call)
Log.d(TAG, "Call removed: ${call.details.handle}") Log.d(TAG, "Call removed: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked")
call.unregisterCallback(callCallback) call.unregisterCallback(callCallback)
channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString())) channel?.invokeMethod("callRemoved", mapOf(
"callId" to call.details.handle.toString(),
"wasPhoneLocked" to wasPhoneLocked
))
currentCall = null currentCall = null
cancelNotification()
} }
override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) { override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) {
@ -66,4 +98,59 @@ class MyInCallService : InCallService() {
Log.d(TAG, "Audio state changed: route=${state.route}") Log.d(TAG, "Audio state changed: route=${state.route}")
channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route)) channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route))
} }
private fun showIncomingCallScreen(phoneNumber: String) {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("phoneNumber", phoneNumber)
putExtra("isIncomingCall", true)
putExtra("showIncomingCallScreen", true)
putExtra("wasPhoneLocked", wasPhoneLocked)
}
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (keyguardManager.isKeyguardLocked) {
startActivity(intent)
Log.d(TAG, "Launched MainActivity directly for locked screen, phoneNumber: $phoneNumber")
} else {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
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)
}
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)
)
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentTitle("Incoming Call")
.setContentText("Call from $phoneNumber")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setFullScreenIntent(pendingIntent, true)
.setAutoCancel(true)
.setOngoing(true)
.build()
startActivity(intent)
notificationManager.notify(NOTIFICATION_ID, notification)
Log.d(TAG, "Launched MainActivity with notification for unlocked screen, phoneNumber: $phoneNumber")
}
}
private fun cancelNotification() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NOTIFICATION_ID)
Log.d(TAG, "Notification canceled")
}
} }

View File

@ -61,9 +61,16 @@ class _CallPageState extends State<CallPage> {
void _hangUp() async { void _hangUp() async {
try { 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) { } 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<CallPage> {
children: [ children: [
SizedBox(height: 35), SizedBox(height: 35),
ObfuscatedAvatar( ObfuscatedAvatar(
imageBytes: widget.thumbnail, // Uses thumbnail if provided imageBytes: widget.thumbnail,
radius: avatarRadius, radius: avatarRadius,
backgroundColor: generateColorFromName(widget.displayName), backgroundColor:
generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName, fallbackInitial: widget.displayName,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -122,11 +130,13 @@ class _CallPageState extends State<CallPage> {
), ),
Text( Text(
widget.phoneNumber, widget.phoneNumber,
style: TextStyle(fontSize: statusFontSize, color: Colors.white70), style: TextStyle(
fontSize: statusFontSize, color: Colors.white70),
), ),
Text( Text(
'Calling...', 'Calling...',
style: TextStyle(fontSize: statusFontSize, color: Colors.white70), style: TextStyle(
fontSize: statusFontSize, color: Colors.white70),
), ),
], ],
), ),
@ -157,7 +167,8 @@ class _CallPageState extends State<CallPage> {
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: _toggleKeypad, 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<CallPage> {
child: Center( child: Center(
child: Text( child: Text(
label, 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<CallPage> {
), ),
Text( Text(
isMuted ? 'Unmute' : 'Mute', 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<CallPage> {
children: [ children: [
IconButton( IconButton(
onPressed: _toggleKeypad, onPressed: _toggleKeypad,
icon: const Icon(Icons.dialpad, color: Colors.white, size: 32), icon: const Icon(Icons.dialpad,
color: Colors.white, size: 32),
), ),
const Text( const Text(
'Keypad', 'Keypad',
style: TextStyle(color: Colors.white, fontSize: 14), style: TextStyle(
color: Colors.white, fontSize: 14),
), ),
], ],
), ),
@ -248,14 +263,19 @@ class _CallPageState extends State<CallPage> {
IconButton( IconButton(
onPressed: _toggleSpeaker, onPressed: _toggleSpeaker,
icon: Icon( icon: Icon(
isSpeakerOn ? Icons.volume_up : Icons.volume_off, isSpeakerOn
color: isSpeakerOn ? Colors.amber : Colors.white, ? Icons.volume_up
: Icons.volume_off,
color: isSpeakerOn
? Colors.amber
: Colors.white,
size: 32, size: 32,
), ),
), ),
const Text( const Text(
'Speaker', 'Speaker',
style: TextStyle(color: Colors.white, fontSize: 14), style: TextStyle(
color: Colors.white, fontSize: 14),
), ),
], ],
), ),
@ -270,10 +290,12 @@ class _CallPageState extends State<CallPage> {
children: [ children: [
IconButton( IconButton(
onPressed: () {}, 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', const Text('Add Contact',
style: TextStyle(color: Colors.white, fontSize: 14)), style: TextStyle(
color: Colors.white, fontSize: 14)),
], ],
), ),
Column( Column(
@ -281,10 +303,12 @@ class _CallPageState extends State<CallPage> {
children: [ children: [
IconButton( IconButton(
onPressed: () {}, 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', const Text('Change SIM',
style: TextStyle(color: Colors.white, fontSize: 14)), style: TextStyle(
color: Colors.white, fontSize: 14)),
], ],
), ),
], ],

View File

@ -267,8 +267,12 @@ class _ContactModalState extends State<ContactModal> {
), ),
onTap: () async { onTap: () async {
if (widget.contact.phones.isNotEmpty) { if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context, await _callService.makeGsmCall(
phoneNumber: phoneNumber); context,
phoneNumber: phoneNumber,
displayName: widget.contact.displayName,
thumbnail: widget.contact.thumbnail,
);
} }
}, },
), ),

View File

@ -425,7 +425,12 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green), icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async { onPressed: () async {
if (contact.phones.isNotEmpty) { if (contact.phones.isNotEmpty) {
_callService.makeGsmCall(context, phoneNumber: contact.phones.first.number); await _callService.makeGsmCall(
context,
phoneNumber: contact.phones.first.number,
displayName: contact.displayName,
thumbnail: contact.thumbnail,
);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(

View File

@ -1,110 +1,211 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../features/call/call_page.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';
import '../services/contact_service.dart';
class CallService { class CallService {
static const MethodChannel _channel = MethodChannel('call_service'); static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber; static String? currentPhoneNumber;
static String? currentDisplayName;
static Uint8List? currentThumbnail;
static bool _isCallPageVisible = false; static bool _isCallPageVisible = false;
static String? _currentCallState;
static Map<String, dynamic>? _pendingCall;
static bool wasPhoneLocked = false;
final ContactService _contactService = ContactService();
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
CallService() { CallService() {
_channel.setMethodCallHandler((call) async { _channel.setMethodCallHandler((call) async {
final context = navigatorKey.currentContext;
print('CallService: Received method ${call.method} with args ${call.arguments}'); print('CallService: Received method ${call.method} with args ${call.arguments}');
if (context == null) {
print('CallService: Navigator context is null, cannot navigate');
return;
}
switch (call.method) { switch (call.method) {
case "callAdded": case "callAdded":
final phoneNumber = call.arguments["callId"] as String; final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String; final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
await _fetchContactInfo(currentPhoneNumber!);
print('CallService: Call added, number: $currentPhoneNumber, state: $state'); print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") { _handleCallState(state);
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
break; break;
case "callStateChanged": case "callStateChanged":
final state = call.arguments["state"] as String; final state = call.arguments["state"] as String;
print('CallService: State changed to $state'); wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
if (state == "disconnected" || state == "disconnecting") { print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked');
_closeCallPage(context); _handleCallState(state);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
break; break;
case "callEnded": case "callEnded":
case "callRemoved": case "callRemoved":
print('CallService: Call ended/removed'); wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
_closeCallPage(context); print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked');
_closeCallPage();
if (wasPhoneLocked) {
_channel.invokeMethod("callEndedFromFlutter");
}
currentPhoneNumber = null; currentPhoneNumber = null;
currentDisplayName = null;
currentThumbnail = null;
_currentCallState = null;
break;
case "incomingCallFromNotification":
final phoneNumber = call.arguments["phoneNumber"] as String;
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
currentPhoneNumber = phoneNumber;
await _fetchContactInfo(currentPhoneNumber!);
print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked');
_handleIncomingCall(phoneNumber);
break; break;
} }
}); });
} }
Future<void> _fetchContactInfo(String phoneNumber) async {
try {
final contacts = await _contactService.fetchContacts();
final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber);
for (var contact in contacts) {
for (var phone in contact.phones) {
if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) {
currentDisplayName = contact.displayName;
currentThumbnail = contact.thumbnail;
return;
}
}
}
currentDisplayName = phoneNumber;
currentThumbnail = null;
} catch (e) {
print('CallService: Error fetching contact info: $e');
currentDisplayName = phoneNumber;
currentThumbnail = null;
}
}
String _normalizePhoneNumber(String number) {
return number.replaceAll(RegExp(r'[\s\-\(\)]'), '');
}
void _handleIncomingCall(String phoneNumber) {
final context = navigatorKey.currentContext;
if (context == null) {
print('CallService: Context is null, queuing incoming call: $phoneNumber');
_pendingCall = {"phoneNumber": phoneNumber};
Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall());
} else {
_navigateToIncomingCallPage(context);
}
}
void _checkPendingCall() {
if (_pendingCall != null) {
final context = navigatorKey.currentContext;
if (context != null) {
print('CallService: Processing queued call: ${_pendingCall!["phoneNumber"]}');
currentPhoneNumber = _pendingCall!["phoneNumber"];
_navigateToIncomingCallPage(context);
_pendingCall = null;
} else {
print('CallService: Context still null, retrying...');
Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall());
}
}
}
void _handleCallState(String state) {
final context = navigatorKey.currentContext;
if (context == null) {
print('CallService: Navigator context is null, cannot navigate');
return;
}
if (_currentCallState == state) {
print('CallService: State $state already handled, skipping');
return;
}
_currentCallState = state;
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage();
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
}
void _navigateToCallPage(BuildContext context) { void _navigateToCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { final currentRoute = ModalRoute.of(context)?.settings.name;
print('CallService: Navigating to CallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute');
if (_isCallPageVisible && currentRoute == '/call') {
print('CallService: CallPage already visible, skipping navigation'); print('CallService: CallPage already visible, skipping navigation');
return; return;
} }
print('CallService: Navigating to CallPage'); if (_isCallPageVisible && currentRoute == '/incoming_call') {
print('CallService: Replacing IncomingCallPage with CallPage');
Navigator.pop(context);
}
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: '/call'), settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage( builder: (context) => CallPage(
displayName: currentPhoneNumber!, displayName: currentDisplayName ?? currentPhoneNumber!,
phoneNumber: currentPhoneNumber!, phoneNumber: currentPhoneNumber!,
thumbnail: null, thumbnail: currentThumbnail,
), ),
), ),
).then((_) { ).then((_) {
print('CallService: CallPage popped');
_isCallPageVisible = false; _isCallPageVisible = false;
}); });
_isCallPageVisible = true; _isCallPageVisible = true;
} }
void _navigateToIncomingCallPage(BuildContext context) { void _navigateToIncomingCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { final currentRoute = ModalRoute.of(context)?.settings.name;
print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute');
if (_isCallPageVisible && currentRoute == '/incoming_call') {
print('CallService: IncomingCallPage already visible, skipping navigation'); print('CallService: IncomingCallPage already visible, skipping navigation');
return; return;
} }
print('CallService: Navigating to IncomingCallPage'); if (_isCallPageVisible && currentRoute == '/call') {
print('CallService: CallPage visible, not showing IncomingCallPage');
return;
}
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'), settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage( builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!, displayName: currentDisplayName ?? currentPhoneNumber!,
phoneNumber: currentPhoneNumber!, phoneNumber: currentPhoneNumber!,
thumbnail: null, thumbnail: currentThumbnail,
), ),
), ),
).then((_) { ).then((_) {
print('CallService: IncomingCallPage popped');
_isCallPageVisible = false; _isCallPageVisible = false;
}); });
_isCallPageVisible = true; _isCallPageVisible = true;
} }
void _closeCallPage(BuildContext context) { void _closeCallPage() {
final context = navigatorKey.currentContext;
if (context == null) {
print('CallService: Cannot close page, context is null');
return;
}
print('CallService: Attempting to close call page. Visible: $_isCallPageVisible');
if (!_isCallPageVisible) { if (!_isCallPageVisible) {
print('CallService: CallPage not visible, skipping pop'); print('CallService: CallPage not visible, skipping pop');
return; return;
} }
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
print('CallService: Popping CallPage'); print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}');
Navigator.pop(context); Navigator.pop(context);
_isCallPageVisible = false; _isCallPageVisible = false;
} else {
print('CallService: Cannot pop, no routes to pop');
} }
} }
@ -116,6 +217,11 @@ class CallService {
}) async { }) async {
try { try {
currentPhoneNumber = phoneNumber; currentPhoneNumber = phoneNumber;
currentDisplayName = displayName ?? phoneNumber;
currentThumbnail = thumbnail;
if (displayName == null || thumbnail == null) {
await _fetchContactInfo(phoneNumber);
}
print('CallService: Making GSM call to $phoneNumber'); print('CallService: Making GSM call to $phoneNumber');
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
print('CallService: makeGsmCall result: $result'); print('CallService: makeGsmCall result: $result');
@ -123,7 +229,9 @@ class CallService {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")), SnackBar(content: Text("Failed to initiate call")),
); );
return;
} }
_handleCallState("dialing");
} catch (e) { } catch (e) {
print("CallService: Error making call: $e"); print("CallService: Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -142,6 +250,8 @@ class CallService {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")), SnackBar(content: Text("Failed to end call")),
); );
} else {
_closeCallPage();
} }
} catch (e) { } catch (e) {
print("CallService: Error hanging up call: $e"); print("CallService: Error hanging up call: $e");