feat: APP IS DEFAULT DIALER
This commit is contained in:
parent
e4ad9726ae
commit
5704fa1607
@ -1,8 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
|
<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-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<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.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.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
|
<application
|
||||||
android:label="Icing Dialer"
|
android:label="Icing Dialer"
|
||||||
@ -39,35 +34,21 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Dialer intent filters -->
|
<!-- Dialer intent filters (required for default dialer eligibility) -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<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" />
|
<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" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
@ -75,27 +56,36 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<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:name=".services.CallConnectionService"
|
||||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.telecom.ConnectionService" />
|
<action android:name="android.telecom.ConnectionService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service> -->
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text -->
|
|
||||||
<!-- Required to query activities that can process text -->
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
@ -1,21 +1,21 @@
|
|||||||
package com.icing.dialer.activities
|
package com.icing.dialer.activities
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
import android.app.role.RoleManager
|
||||||
|
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.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.CallLog
|
import android.provider.CallLog
|
||||||
import android.telecom.PhoneAccount
|
|
||||||
import android.telecom.PhoneAccountHandle
|
|
||||||
import android.telecom.TelecomManager
|
import android.telecom.TelecomManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.icing.dialer.KeystoreHelper
|
import com.icing.dialer.KeystoreHelper
|
||||||
import com.icing.dialer.services.CallConnectionService
|
|
||||||
import com.icing.dialer.services.CallService
|
import com.icing.dialer.services.CallService
|
||||||
|
import com.icing.dialer.services.MyInCallService
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@ -25,6 +25,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val CALLLOG_CHANNEL = "com.example.calllog"
|
private val CALLLOG_CHANNEL = "com.example.calllog"
|
||||||
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
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -32,132 +33,84 @@ class MainActivity : FlutterActivity() {
|
|||||||
Log.d(TAG, "Waiting for Flutter to signal permissions")
|
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) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
Log.d(TAG, "Configuring Flutter engine")
|
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)
|
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")
|
||||||
registerPhoneAccount()
|
checkAndRequestDefaultDialer()
|
||||||
checkAndRequestDefaultDialer()
|
result.success(true)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
"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)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result -> KeystoreHelper(call, result).handleMethodCall() }
|
||||||
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") {
|
||||||
val callLogs = getCallLogs()
|
val callLogs = getCallLogs()
|
||||||
result.success(callLogs)
|
result.success(callLogs)
|
||||||
} else {
|
} else {
|
||||||
result.notImplemented()
|
result.notImplemented()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAndRequestDefaultDialer() {
|
private fun checkAndRequestDefaultDialer() {
|
||||||
val tm = getSystemService(TELECOM_SERVICE) as TelecomManager
|
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||||
val currentDefault = tm.defaultDialerPackage
|
val currentDefault = telecomManager.defaultDialerPackage
|
||||||
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
|
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
|
||||||
|
|
||||||
if (currentDefault != packageName) {
|
if (currentDefault != packageName) {
|
||||||
val intent =
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
|
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
|
||||||
.putExtra(
|
if (roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) && !roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
|
||||||
TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
|
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
|
||||||
packageName
|
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
|
||||||
)
|
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
} else {
|
||||||
try {
|
Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
|
||||||
startActivityForResult(intent, 1001)
|
}
|
||||||
Log.d(TAG, "Default dialer prompt launched with requestCode 1001")
|
} else {
|
||||||
} catch (e: Exception) {
|
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
|
||||||
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
|
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
|
||||||
launchDefaultAppsSettings()
|
.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 {
|
} else {
|
||||||
Log.d(TAG, "Already the default dialer")
|
Log.d(TAG, "Already the default dialer")
|
||||||
@ -173,11 +126,11 @@ class MainActivity : FlutterActivity() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$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) {
|
if (resultCode == RESULT_OK) {
|
||||||
Log.d(TAG, "User accepted default dialer change")
|
Log.d(TAG, "User accepted default dialer change")
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Default dialer prompt canceled (resultCode=$resultCode)")
|
Log.w(TAG, "Default dialer prompt canceled or failed (resultCode=$resultCode)")
|
||||||
launchDefaultAppsSettings()
|
launchDefaultAppsSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,14 +138,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
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? =
|
val cursor: Cursor? = contentResolver.query(
|
||||||
contentResolver.query(
|
CallLog.Calls.CONTENT_URI,
|
||||||
CallLog.Calls.CONTENT_URI,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
CallLog.Calls.DATE + " DESC"
|
||||||
CallLog.Calls.DATE + " DESC"
|
)
|
||||||
)
|
|
||||||
cursor?.use {
|
cursor?.use {
|
||||||
while (it.moveToNext()) {
|
while (it.moveToNext()) {
|
||||||
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
|
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 date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
|
||||||
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
||||||
|
|
||||||
val map =
|
val map = mutableMapOf<String, Any?>(
|
||||||
mutableMapOf<String, Any?>(
|
"number" to number,
|
||||||
"number" to number,
|
"type" to type,
|
||||||
"type" to type,
|
"date" to date,
|
||||||
"date" to date,
|
"duration" to duration
|
||||||
"duration" to duration
|
)
|
||||||
)
|
|
||||||
logsList.add(map)
|
logsList.add(map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,82 +1,82 @@
|
|||||||
package com.icing.dialer.services
|
// package com.icing.dialer.services
|
||||||
|
|
||||||
import android.telecom.Connection
|
// import android.telecom.Connection
|
||||||
import android.telecom.ConnectionService
|
// import android.telecom.ConnectionService
|
||||||
import android.telecom.PhoneAccountHandle
|
// import android.telecom.PhoneAccountHandle
|
||||||
import android.telecom.TelecomManager
|
// import android.telecom.TelecomManager
|
||||||
import android.telecom.DisconnectCause
|
// import android.telecom.DisconnectCause
|
||||||
import android.util.Log
|
// import android.util.Log
|
||||||
import io.flutter.plugin.common.MethodChannel
|
// import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class CallConnectionService : ConnectionService() {
|
// class CallConnectionService : ConnectionService() {
|
||||||
companion object {
|
// companion object {
|
||||||
var channel: MethodChannel? = null
|
// var channel: MethodChannel? = null
|
||||||
private const val TAG = "CallConnectionService"
|
// private const val TAG = "CallConnectionService"
|
||||||
}
|
// }
|
||||||
|
|
||||||
init {
|
// init {
|
||||||
Log.d(TAG, "CallConnectionService initialized")
|
// Log.d(TAG, "CallConnectionService initialized")
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onCreate() {
|
// override fun onCreate() {
|
||||||
super.onCreate()
|
// super.onCreate()
|
||||||
Log.d(TAG, "Service created")
|
// Log.d(TAG, "Service created")
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onDestroy() {
|
// override fun onDestroy() {
|
||||||
super.onDestroy()
|
// super.onDestroy()
|
||||||
Log.d(TAG, "Service destroyed")
|
// Log.d(TAG, "Service destroyed")
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onCreateOutgoingConnection(
|
// override fun onCreateOutgoingConnection(
|
||||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
// connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
request: android.telecom.ConnectionRequest
|
// request: android.telecom.ConnectionRequest
|
||||||
): Connection {
|
// ): Connection {
|
||||||
Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
// Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
||||||
val connection = object : Connection() {
|
// val connection = object : Connection() {
|
||||||
override fun onStateChanged(state: Int) {
|
// override fun onStateChanged(state: Int) {
|
||||||
super.onStateChanged(state)
|
// super.onStateChanged(state)
|
||||||
Log.d(TAG, "Connection state changed: $state")
|
// Log.d(TAG, "Connection state changed: $state")
|
||||||
val stateStr = when (state) {
|
// val stateStr = when (state) {
|
||||||
STATE_DIALING -> "dialing"
|
// STATE_DIALING -> "dialing"
|
||||||
STATE_ACTIVE -> "active"
|
// STATE_ACTIVE -> "active"
|
||||||
STATE_DISCONNECTED -> "disconnected"
|
// STATE_DISCONNECTED -> "disconnected"
|
||||||
else -> "unknown"
|
// else -> "unknown"
|
||||||
}
|
// }
|
||||||
channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
|
// channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onDisconnect() {
|
// override fun onDisconnect() {
|
||||||
Log.d(TAG, "Connection disconnected")
|
// Log.d(TAG, "Connection disconnected")
|
||||||
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||||
destroy()
|
// destroy()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
||||||
connection.setInitialized()
|
// connection.setInitialized()
|
||||||
connection.setDialing()
|
// connection.setDialing()
|
||||||
return connection
|
// return connection
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onCreateIncomingConnection(
|
// override fun onCreateIncomingConnection(
|
||||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
// connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
request: android.telecom.ConnectionRequest
|
// request: android.telecom.ConnectionRequest
|
||||||
): Connection {
|
// ): Connection {
|
||||||
Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
// Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
||||||
val connection = object : Connection() {
|
// val connection = object : Connection() {
|
||||||
override fun onAnswer() {
|
// override fun onAnswer() {
|
||||||
Log.d(TAG, "Connection answered")
|
// Log.d(TAG, "Connection answered")
|
||||||
setActive()
|
// setActive()
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onDisconnect() {
|
// override fun onDisconnect() {
|
||||||
Log.d(TAG, "Connection disconnected")
|
// Log.d(TAG, "Connection disconnected")
|
||||||
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||||
destroy()
|
// destroy()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
||||||
connection.setRinging()
|
// connection.setRinging()
|
||||||
return connection
|
// return connection
|
||||||
}
|
// }
|
||||||
}
|
// }
|
@ -1,24 +1,17 @@
|
|||||||
package com.icing.dialer.services
|
package com.icing.dialer.services
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.telecom.TelecomManager
|
import android.telecom.TelecomManager
|
||||||
import android.telecom.PhoneAccountHandle
|
|
||||||
import android.telephony.TelephonyManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.Manifest
|
||||||
|
|
||||||
object CallService {
|
object CallService {
|
||||||
private var phoneAccountHandle: PhoneAccountHandle? = null
|
private val TAG = "CallService"
|
||||||
|
|
||||||
fun setPhoneAccountHandle(handle: PhoneAccountHandle) {
|
|
||||||
phoneAccountHandle = handle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
|
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
@ -26,37 +19,36 @@ object CallService {
|
|||||||
val uri = Uri.parse("tel:$phoneNumber")
|
val uri = Uri.parse("tel:$phoneNumber")
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
telecomManager.placeCall(uri, Bundle())
|
||||||
val extras = Bundle()
|
Log.d(TAG, "Initiated call to $phoneNumber")
|
||||||
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
true
|
||||||
telecomManager.placeCall(uri, extras)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
Log.e("CallService", "GSM call not supported below Android M")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("CallService", "CALL_PHONE permission not granted")
|
Log.e(TAG, "CALL_PHONE permission not granted")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("CallService", "Error making GSM call: ${e.message}")
|
Log.e(TAG, "Error making GSM call: ${e.message}", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hangUpCall(context: Context): Boolean {
|
fun hangUpCall(context: Context): Boolean {
|
||||||
return try {
|
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
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||||
telecomManager.endCall()
|
telecomManager.endCall()
|
||||||
|
Log.d(TAG, "Ended call via TelecomManager (no active call in MyInCallService)")
|
||||||
true
|
true
|
||||||
} else {
|
} 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
|
false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("CallService", "Error hanging up call: ${e.message}")
|
Log.e(TAG, "Error hanging up call: ${e.message}", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ Future<void> _requestPermissions() async {
|
|||||||
Map<Permission, PermissionStatus> statuses = await [
|
Map<Permission, PermissionStatus> statuses = await [
|
||||||
Permission.phone,
|
Permission.phone,
|
||||||
Permission.contacts,
|
Permission.contacts,
|
||||||
|
Permission.microphone,
|
||||||
].request();
|
].request();
|
||||||
if (statuses.values.every((status) => status.isGranted)) {
|
if (statuses.values.every((status) => status.isGranted)) {
|
||||||
print("All required permissions granted");
|
print("All required permissions granted");
|
||||||
|
@ -1,37 +1,66 @@
|
|||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import '../features/call/call_page.dart';
|
import '../features/call/call_page.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;
|
||||||
|
|
||||||
|
// Add a GlobalKey for Navigator
|
||||||
|
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
CallService() {
|
CallService() {
|
||||||
_channel.setMethodCallHandler((call) async {
|
_channel.setMethodCallHandler((call) async {
|
||||||
if (call.method == "callStateChanged") {
|
final context = navigatorKey.currentContext;
|
||||||
final state = call.arguments["state"] as String;
|
if (context == null) return;
|
||||||
final phoneNumber = call.arguments["phoneNumber"] as String;
|
|
||||||
if (state == "dialing" || state == "active") {
|
switch (call.method) {
|
||||||
Navigator.push(
|
case "callAdded":
|
||||||
navigatorKey.currentContext!,
|
final phoneNumber = call.arguments["callId"] as String; // tel:1234567890
|
||||||
MaterialPageRoute(
|
final state = call.arguments["state"] as String;
|
||||||
builder: (context) => CallPage(
|
currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); // Extract number
|
||||||
displayName: phoneNumber, // Replace with contact lookup if available
|
if (state == "dialing" || state == "active") {
|
||||||
phoneNumber: phoneNumber,
|
Navigator.push(
|
||||||
thumbnail: null,
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CallPage(
|
||||||
|
displayName: currentPhoneNumber!, // Replace with contact lookup if available
|
||||||
|
phoneNumber: currentPhoneNumber!,
|
||||||
|
thumbnail: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
} else if (state == "disconnected") {
|
break;
|
||||||
Navigator.pop(navigatorKey.currentContext!);
|
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(
|
Future<void> makeGsmCall(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
@ -42,15 +71,17 @@ class CallService {
|
|||||||
currentPhoneNumber = phoneNumber;
|
currentPhoneNumber = phoneNumber;
|
||||||
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||||
if (result["status"] == "calling") {
|
if (result["status"] == "calling") {
|
||||||
// CallPage will be shown via CallConnectionService callback
|
// CallPage will be shown via MyInCallService callback
|
||||||
} else if (result["status"] == "pending_default_dialer") {
|
} else {
|
||||||
print("Waiting for user to set app as default dialer");
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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) {
|
} catch (e) {
|
||||||
print("Error making call: $e");
|
print("Error making call: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Error making call: $e")),
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,11 +90,14 @@ class CallService {
|
|||||||
try {
|
try {
|
||||||
final result = await _channel.invokeMethod('hangUpCall');
|
final result = await _channel.invokeMethod('hangUpCall');
|
||||||
if (result["status"] == "ended") {
|
if (result["status"] == "ended") {
|
||||||
Navigator.pop(context);
|
// Navigator.pop will be handled by MyInCallService callback
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error hanging up call: $e");
|
print("Error hanging up call: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Error hanging up call: $e")),
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user