feat: APP IS DEFAULT DIALER
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m46s
/ build (push) Successful in 8m39s

This commit is contained in:
Florian Griffon 2025-03-14 00:21:21 +02:00
parent e4ad9726ae
commit 5704fa1607
7 changed files with 314 additions and 286 deletions

View File

@ -1,8 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
@ -12,11 +12,6 @@
<uses-permission android:name="android.permission.MANAGE_OWN_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.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<application
android:label="Icing Dialer"
@ -39,35 +34,21 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Dialer intent filters -->
<!-- Dialer intent filters (required for default dialer eligibility) -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Dialer intent filters -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -75,27 +56,36 @@
</intent-filter>
</activity>
<service
android:name=".services.MyInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
</service>
<!-- Custom ConnextionService, will be needed at some point when we implement our own protocol -->
<!-- <service
android:name=".services.CallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
</service> -->
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text -->
<!-- Required to query activities that can process text -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View File

@ -1,21 +1,21 @@
package com.icing.dialer.activities
import android.Manifest
import android.content.ComponentName
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.CallLog
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallConnectionService
import com.icing.dialer.services.CallService
import com.icing.dialer.services.MyInCallService
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@ -25,6 +25,7 @@ class MainActivity : FlutterActivity() {
private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service"
private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -32,132 +33,84 @@ class MainActivity : FlutterActivity() {
Log.d(TAG, "Waiting for Flutter to signal permissions")
}
private fun registerPhoneAccount() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccountHandle =
PhoneAccountHandle(
ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount"
)
Log.d(TAG, "PhoneAccountHandle component: ${phoneAccountHandle.componentName}")
val phoneAccount =
PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(
PhoneAccount.CAPABILITY_CALL_PROVIDER or
PhoneAccount.CAPABILITY_CONNECTION_MANAGER
)
.build()
telecomManager.registerPhoneAccount(phoneAccount)
CallService.setPhoneAccountHandle(phoneAccountHandle)
Log.d(TAG, "PhoneAccount registered: ${phoneAccountHandle.id}")
val registeredAccounts = telecomManager.callCapablePhoneAccounts
Log.d(TAG, "Registered PhoneAccounts: ${registeredAccounts.joinToString()}")
if (!registeredAccounts.contains(phoneAccountHandle)) {
Log.w(TAG, "PhoneAccount not found in callCapablePhoneAccounts")
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) ==
PackageManager.PERMISSION_GRANTED
) {
val uri = Uri.parse("tel:1234567890")
val extras =
Bundle().apply {
putParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
phoneAccountHandle
)
}
telecomManager.placeCall(uri, extras)
Log.d(TAG, "Triggered dummy call to bind CallConnectionService")
} else {
Log.w(TAG, "CALL_PHONE permission not granted, cannot test binding")
}
} else {
Log.d(TAG, "PhoneAccount successfully found in callCapablePhoneAccounts")
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(TAG, "Configuring Flutter engine")
CallConnectionService.channel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
registerPhoneAccount()
checkAndRequestDefaultDialer()
result.success(true)
}
"makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) {
val success = CallService.makeGsmCall(this, phoneNumber)
if (success) {
result.success(
mapOf(
"status" to "calling",
"phoneNumber" to phoneNumber
)
)
} else {
result.error("CALL_FAILED", "Failed to initiate call", null)
}
} else {
result.error(
"INVALID_PHONE_NUMBER",
"Phone number is required",
null
)
}
}
"hangUpCall" -> {
val success = CallService.hangUpCall(this)
if (success) {
result.success(mapOf("status" to "ended"))
} else {
result.error("HANGUP_FAILED", "Failed to end call", null)
}
}
else -> result.notImplemented()
.setMethodCallHandler { call, result ->
when (call.method) {
"permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
checkAndRequestDefaultDialer()
result.success(true)
}
"makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) {
val success = CallService.makeGsmCall(this, phoneNumber)
if (success) {
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber))
} else {
result.error("CALL_FAILED", "Failed to initiate call", null)
}
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
}
"hangUpCall" -> {
val success = CallService.hangUpCall(this)
if (success) {
result.success(mapOf("status" to "ended"))
} else {
result.error("HANGUP_FAILED", "Failed to end call", null)
}
}
else -> result.notImplemented()
}
}
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)
} else {
result.notImplemented()
}
.setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") {
val callLogs = getCallLogs()
result.success(callLogs)
} else {
result.notImplemented()
}
}
}
private fun checkAndRequestDefaultDialer() {
val tm = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = tm.defaultDialerPackage
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
if (currentDefault != packageName) {
val intent =
Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
.putExtra(
TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
packageName
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivityForResult(intent, 1001)
Log.d(TAG, "Default dialer prompt launched with requestCode 1001")
} catch (e: Exception) {
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
launchDefaultAppsSettings()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) && !roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
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 {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched TelecomManager intent for default dialer")
} catch (e: Exception) {
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
launchDefaultAppsSettings()
}
}
} else {
Log.d(TAG, "Already the default dialer")
@ -173,11 +126,11 @@ class MainActivity : FlutterActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$data")
if (requestCode == 1001) {
if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "User accepted default dialer change")
} else {
Log.d(TAG, "Default dialer prompt canceled (resultCode=$resultCode)")
Log.w(TAG, "Default dialer prompt canceled or failed (resultCode=$resultCode)")
launchDefaultAppsSettings()
}
}
@ -185,14 +138,13 @@ class MainActivity : FlutterActivity() {
private fun getCallLogs(): List<Map<String, Any?>> {
val logsList = mutableListOf<Map<String, Any?>>()
val cursor: Cursor? =
contentResolver.query(
CallLog.Calls.CONTENT_URI,
null,
null,
null,
CallLog.Calls.DATE + " DESC"
)
val cursor: Cursor? = contentResolver.query(
CallLog.Calls.CONTENT_URI,
null,
null,
null,
CallLog.Calls.DATE + " DESC"
)
cursor?.use {
while (it.moveToNext()) {
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
@ -200,13 +152,12 @@ class MainActivity : FlutterActivity() {
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
val map =
mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,
"duration" to duration
)
val map = mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,
"duration" to duration
)
logsList.add(map)
}
}

View File

@ -1,82 +1,82 @@
package com.icing.dialer.services
// package com.icing.dialer.services
import android.telecom.Connection
import android.telecom.ConnectionService
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.telecom.DisconnectCause
import android.util.Log
import io.flutter.plugin.common.MethodChannel
// import android.telecom.Connection
// import android.telecom.ConnectionService
// import android.telecom.PhoneAccountHandle
// import android.telecom.TelecomManager
// import android.telecom.DisconnectCause
// import android.util.Log
// import io.flutter.plugin.common.MethodChannel
class CallConnectionService : ConnectionService() {
companion object {
var channel: MethodChannel? = null
private const val TAG = "CallConnectionService"
}
// class CallConnectionService : ConnectionService() {
// companion object {
// var channel: MethodChannel? = null
// private const val TAG = "CallConnectionService"
// }
init {
Log.d(TAG, "CallConnectionService initialized")
}
// init {
// Log.d(TAG, "CallConnectionService initialized")
// }
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created")
}
// override fun onCreate() {
// super.onCreate()
// Log.d(TAG, "Service created")
// }
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service destroyed")
}
// override fun onDestroy() {
// super.onDestroy()
// Log.d(TAG, "Service destroyed")
// }
override fun onCreateOutgoingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: android.telecom.ConnectionRequest
): Connection {
Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
val connection = object : Connection() {
override fun onStateChanged(state: Int) {
super.onStateChanged(state)
Log.d(TAG, "Connection state changed: $state")
val stateStr = when (state) {
STATE_DIALING -> "dialing"
STATE_ACTIVE -> "active"
STATE_DISCONNECTED -> "disconnected"
else -> "unknown"
}
channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
}
// override fun onCreateOutgoingConnection(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onStateChanged(state: Int) {
// super.onStateChanged(state)
// Log.d(TAG, "Connection state changed: $state")
// val stateStr = when (state) {
// STATE_DIALING -> "dialing"
// STATE_ACTIVE -> "active"
// STATE_DISCONNECTED -> "disconnected"
// else -> "unknown"
// }
// channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
// }
override fun onDisconnect() {
Log.d(TAG, "Connection disconnected")
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy()
}
}
connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
connection.setInitialized()
connection.setDialing()
return connection
}
// override fun onDisconnect() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setInitialized()
// connection.setDialing()
// return connection
// }
override fun onCreateIncomingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: android.telecom.ConnectionRequest
): Connection {
Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
val connection = object : Connection() {
override fun onAnswer() {
Log.d(TAG, "Connection answered")
setActive()
}
// override fun onCreateIncomingConnection(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onAnswer() {
// Log.d(TAG, "Connection answered")
// setActive()
// }
override fun onDisconnect() {
Log.d(TAG, "Connection disconnected")
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy()
}
}
connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
connection.setRinging()
return connection
}
}
// override fun onDisconnect() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setRinging()
// return connection
// }
// }

View File

@ -1,24 +1,17 @@
package com.icing.dialer.services
import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle
import android.telephony.TelephonyManager
import android.util.Log
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
object CallService {
private var phoneAccountHandle: PhoneAccountHandle? = null
fun setPhoneAccountHandle(handle: PhoneAccountHandle) {
phoneAccountHandle = handle
}
private val TAG = "CallService"
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
return try {
@ -26,37 +19,36 @@ object CallService {
val uri = Uri.parse("tel:$phoneNumber")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val extras = Bundle()
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
telecomManager.placeCall(uri, extras)
true
} else {
Log.e("CallService", "GSM call not supported below Android M")
false
}
telecomManager.placeCall(uri, Bundle())
Log.d(TAG, "Initiated call to $phoneNumber")
true
} else {
Log.e("CallService", "CALL_PHONE permission not granted")
Log.e(TAG, "CALL_PHONE permission not granted")
false
}
} catch (e: Exception) {
Log.e("CallService", "Error making GSM call: ${e.message}")
Log.e(TAG, "Error making GSM call: ${e.message}", e)
false
}
}
fun hangUpCall(context: Context): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (MyInCallService.currentCall != null) {
MyInCallService.currentCall?.disconnect()
Log.d(TAG, "Disconnected active call via MyInCallService")
true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
Log.d(TAG, "Ended call via TelecomManager (no active call in MyInCallService)")
true
} else {
Log.e("CallService", "Hangup call only supported on Android P or later")
Log.e(TAG, "No active call and hangup not supported below Android P")
false
}
} catch (e: Exception) {
Log.e("CallService", "Error hanging up call: ${e.message}")
Log.e(TAG, "Error hanging up call: ${e.message}", e)
false
}
}

View File

@ -0,0 +1,60 @@
package com.icing.dialer.services
import android.telecom.Call
import android.telecom.InCallService
import io.flutter.plugin.common.MethodChannel
class MyInCallService : InCallService() {
companion object {
var channel: MethodChannel? = null
var currentCall: Call? = null
}
private val callCallback = object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
super.onStateChanged(call, state)
val stateStr = when (state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
Call.STATE_DISCONNECTED -> "disconnected"
Call.STATE_DISCONNECTING -> "disconnecting"
else -> "unknown"
}
channel?.invokeMethod("callStateChanged", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString()))
currentCall = null
}
}
}
override fun onCallAdded(call: Call) {
super.onCallAdded(call)
currentCall = call
val stateStr = when (call.state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
else -> "unknown"
}
channel?.invokeMethod("callAdded", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
call.registerCallback(callCallback)
}
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
call.unregisterCallback(callCallback)
channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString()))
currentCall = null
}
override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) {
super.onCallAudioStateChanged(state)
channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route))
}
}

View File

@ -37,6 +37,7 @@ Future<void> _requestPermissions() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.phone,
Permission.contacts,
Permission.microphone,
].request();
if (statuses.values.every((status) => status.isGranted)) {
print("All required permissions granted");

View File

@ -1,37 +1,66 @@
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../features/call/call_page.dart';
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber;
// Add a GlobalKey for Navigator
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
CallService() {
_channel.setMethodCallHandler((call) async {
if (call.method == "callStateChanged") {
final state = call.arguments["state"] as String;
final phoneNumber = call.arguments["phoneNumber"] as String;
if (state == "dialing" || state == "active") {
Navigator.push(
navigatorKey.currentContext!,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: phoneNumber, // Replace with contact lookup if available
phoneNumber: phoneNumber,
thumbnail: null,
final context = navigatorKey.currentContext;
if (context == null) return;
switch (call.method) {
case "callAdded":
final phoneNumber = call.arguments["callId"] as String; // tel:1234567890
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); // Extract number
if (state == "dialing" || state == "active") {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: currentPhoneNumber!, // Replace with contact lookup if available
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
),
);
} else if (state == "disconnected") {
Navigator.pop(navigatorKey.currentContext!);
}
);
}
break;
case "callStateChanged":
final state = call.arguments["state"] as String;
if (state == "disconnected" || state == "disconnecting") {
Navigator.pop(context);
} else if (state == "active") {
// Ensure CallPage is shown if not already
if (Navigator.canPop(context) && ModalRoute.of(context)?.settings.name != '/call') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
);
}
}
break;
case "callEnded":
case "callRemoved":
Navigator.pop(context);
currentPhoneNumber = null;
break;
}
});
}
// Add a GlobalKey for Navigator
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<void> makeGsmCall(
BuildContext context, {
required String phoneNumber,
@ -42,15 +71,17 @@ class CallService {
currentPhoneNumber = phoneNumber;
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
if (result["status"] == "calling") {
// CallPage will be shown via CallConnectionService callback
} else if (result["status"] == "pending_default_dialer") {
print("Waiting for user to set app as default dialer");
// CallPage will be shown via MyInCallService callback
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Please set this app as your default dialer to proceed")),
SnackBar(content: Text("Failed to initiate call")),
);
}
} catch (e) {
print("Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error making call: $e")),
);
rethrow;
}
}
@ -59,11 +90,14 @@ class CallService {
try {
final result = await _channel.invokeMethod('hangUpCall');
if (result["status"] == "ended") {
Navigator.pop(context);
// Navigator.pop will be handled by MyInCallService callback
}
} catch (e) {
print("Error hanging up call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error hanging up call: $e")),
);
rethrow;
}
}
}
}