feat: update flutter UI via methodchannel, permissions via flutter at startup

This commit is contained in:
Florian Griffon 2025-03-04 18:43:48 +01:00
parent b042a68a8e
commit 24dc5a9bbe
3 changed files with 184 additions and 66 deletions

View File

@ -1,4 +1,6 @@
<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.CALL_PHONE" />
@ -48,7 +50,6 @@
<data android:scheme="tel" />
</intent-filter>
</activity>
<!-- Moved service outside of activity -->
<service
android:name=".services.CallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"

View File

@ -1,106 +1,211 @@
package com.icing.dialer.activities
import android.content.ComponentName
import android.content.Intent
import android.database.Cursor
import android.os.Bundle
import android.provider.CallLog
import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle
import android.telecom.PhoneAccount
import android.content.Intent
import android.content.ComponentName
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallConnectionService
import com.icing.dialer.services.CallService
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallService
import com.icing.dialer.services.CallConnectionService
class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore"
private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service"
private val REQUEST_CALL_PERMISSIONS = 1
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started")
registerPhoneAccount()
checkAndRequestDefaultDialer()
Log.d(TAG, "onCreate completed")
}
// private fun checkPermissions(): Boolean {
// val permissions =
// arrayOf(
// Manifest.permission.CALL_PHONE,
// Manifest.permission.READ_PHONE_STATE,
// Manifest.permission.MANAGE_OWN_CALLS,
// Manifest.permission.READ_CONTACTS // Add this
// )
// return permissions
// .all {
// ContextCompat.checkSelfPermission(this, it) ==
// PackageManager.PERMISSION_GRANTED
// }
// .also { Log.d(TAG, "Permissions check result: $it") }
// }
// private fun requestPermissions() {
// ActivityCompat.requestPermissions(
// this,
// arrayOf(
// Manifest.permission.CALL_PHONE,
// Manifest.permission.READ_PHONE_STATE,
// Manifest.permission.MANAGE_OWN_CALLS,
// Manifest.permission.READ_CONTACTS // Add this
// ),
// REQUEST_CALL_PERMISSIONS
// )
// Log.d(TAG, "Permission request dispatched")
// }
// override fun onRequestPermissionsResult(
// requestCode: Int,
// permissions: Array<out String>,
// grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// Log.d(TAG, "onRequestPermissionsResult: $requestCode, ${grantResults.joinToString()}")
// if (requestCode == REQUEST_CALL_PERMISSIONS &&
// grantResults.all { it == PackageManager.PERMISSION_GRANTED }
// ) {
// Log.d(TAG, "All permissions granted")
// registerPhoneAccount()
// checkAndRequestDefaultDialer()
// } else {
// Log.e(
// TAG,
// "Required permissions not granted: ${permissions.joinToString()},
// ${grantResults.joinToString()}"
// )
// }
// }
private fun registerPhoneAccount() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccountHandle = PhoneAccountHandle(
ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount"
)
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build()
val phoneAccountHandle =
PhoneAccountHandle(
ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount"
)
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}")
Log.d(
TAG,
"Registered PhoneAccounts: ${telecomManager.callCapablePhoneAccounts.joinToString()}"
)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"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)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"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
)
}
}
} 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()
}
}
"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
tm.defaultDialerPackage?.let {
if (it != packageName) {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
startActivity(intent)
val currentDefault = tm.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
)
try {
startActivityForResult(intent, 1001) // Use startActivityForResult to track response
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)
}
} else {
Log.d(TAG, "Already the default dialer")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode")
if (requestCode == 1001) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "User accepted default dialer change")
} else {
Log.d(TAG, "User rejected or canceled default dialer change")
}
}
}
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))
@ -108,15 +213,16 @@ 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)
}
}
return logsList
}
}
}

View File

@ -8,8 +8,13 @@ import android.telecom.DisconnectCause
import android.net.Uri
import android.os.Bundle
import android.util.Log
import io.flutter.plugin.common.MethodChannel
class CallConnectionService : ConnectionService() {
companion object {
var channel: MethodChannel? = null
}
override fun onCreateOutgoingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: android.telecom.ConnectionRequest
@ -18,7 +23,13 @@ class CallConnectionService : ConnectionService() {
override fun onStateChanged(state: Int) {
super.onStateChanged(state)
Log.d("CallConnectionService", "Connection state changed: $state")
// Update Flutter UI via MethodChannel if needed
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() {
@ -28,7 +39,7 @@ class CallConnectionService : ConnectionService() {
}
connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
connection.setInitialized()
connection.setActive()
connection.setDialing() // Start in dialing state
return connection
}