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"> <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.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" />
@ -48,7 +50,6 @@
<data android:scheme="tel" /> <data android:scheme="tel" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Moved service outside of activity -->
<service <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"

View File

@ -1,106 +1,211 @@
package com.icing.dialer.activities package com.icing.dialer.activities
import android.content.ComponentName
import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.os.Bundle import android.os.Bundle
import android.provider.CallLog import android.provider.CallLog
import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle
import android.telecom.PhoneAccount import android.telecom.PhoneAccount
import android.content.Intent import android.telecom.PhoneAccountHandle
import android.content.ComponentName 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.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel 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() { class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore" private val KEYSTORE_CHANNEL = "com.example.keystore"
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 REQUEST_CALL_PERMISSIONS = 1
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started")
registerPhoneAccount() registerPhoneAccount()
checkAndRequestDefaultDialer() 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() { private fun registerPhoneAccount() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccountHandle = PhoneAccountHandle( val phoneAccountHandle =
ComponentName(this, CallConnectionService::class.java), PhoneAccountHandle(
"IcingDialerAccount" ComponentName(this, CallConnectionService::class.java),
) "IcingDialerAccount"
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Icing Dialer") )
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) val phoneAccount =
.build() PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(
PhoneAccount.CAPABILITY_CALL_PROVIDER or
PhoneAccount.CAPABILITY_CONNECTION_MANAGER
)
.build()
telecomManager.registerPhoneAccount(phoneAccount) 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) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
when (call.method) { .setMethodCallHandler { call, result ->
"makeGsmCall" -> { when (call.method) {
val phoneNumber = call.argument<String>("phoneNumber") "makeGsmCall" -> {
if (phoneNumber != null) { val phoneNumber = call.argument<String>("phoneNumber")
val success = CallService.makeGsmCall(this, phoneNumber) if (phoneNumber != null) {
if (success) { val success = CallService.makeGsmCall(this, phoneNumber)
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber)) if (success) {
} else { result.success(
result.error("CALL_FAILED", "Failed to initiate call", null) 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 { "hangUpCall" -> {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null) 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) 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 tm = getSystemService(TELECOM_SERVICE) as TelecomManager
tm.defaultDialerPackage?.let { val currentDefault = tm.defaultDialerPackage
if (it != packageName) { Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER) if (currentDefault != packageName) {
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName) val intent =
startActivity(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?>> { 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? =
CallLog.Calls.CONTENT_URI, null, null, null, CallLog.Calls.DATE + " DESC" contentResolver.query(
) CallLog.Calls.CONTENT_URI,
null,
null,
null,
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))
@ -108,15 +213,16 @@ 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 = mutableMapOf<String, Any?>( val map =
"number" to number, mutableMapOf<String, Any?>(
"type" to type, "number" to number,
"date" to date, "type" to type,
"duration" to duration "date" to date,
) "duration" to duration
)
logsList.add(map) logsList.add(map)
} }
} }
return logsList return logsList
} }
} }

View File

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