feat: request perm in flutter, wait for perm before trying to become main dialer
All checks were successful
/ build (push) Successful in 8m35s
/ build-stealth (push) Successful in 8m33s
/ mirror (push) Successful in 5s

This commit is contained in:
Florian Griffon 2025-03-05 16:04:05 +01:00
parent c886e29d75
commit 5529a6e038
3 changed files with 77 additions and 68 deletions

View File

@ -1,14 +1,18 @@
package com.icing.dialer.activities package com.icing.dialer.activities
import android.Manifest
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.CallLog import android.provider.CallLog
import android.telecom.PhoneAccount import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle 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 com.icing.dialer.KeystoreHelper import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallConnectionService import com.icing.dialer.services.CallConnectionService
import com.icing.dialer.services.CallService import com.icing.dialer.services.CallService
@ -20,69 +24,14 @@ 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" 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") Log.d(TAG, "onCreate started")
registerPhoneAccount() Log.d(TAG, "Waiting for Flutter to signal permissions")
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 = val phoneAccountHandle =
@ -90,6 +39,7 @@ class MainActivity : FlutterActivity() {
ComponentName(this, CallConnectionService::class.java), ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount" "IcingDialerAccount"
) )
Log.d(TAG, "PhoneAccountHandle component: ${phoneAccountHandle.componentName}")
val phoneAccount = val phoneAccount =
PhoneAccount.builder(phoneAccountHandle, "Icing Dialer") PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities( .setCapabilities(
@ -100,18 +50,46 @@ class MainActivity : FlutterActivity() {
telecomManager.registerPhoneAccount(phoneAccount) telecomManager.registerPhoneAccount(phoneAccount)
CallService.setPhoneAccountHandle(phoneAccountHandle) CallService.setPhoneAccountHandle(phoneAccountHandle)
Log.d(TAG, "PhoneAccount registered: ${phoneAccountHandle.id}") Log.d(TAG, "PhoneAccount registered: ${phoneAccountHandle.id}")
Log.d(
TAG, val registeredAccounts = telecomManager.callCapablePhoneAccounts
"Registered PhoneAccounts: ${telecomManager.callCapablePhoneAccounts.joinToString()}" 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")
CallConnectionService.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" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
registerPhoneAccount()
checkAndRequestDefaultDialer()
result.success(true)
}
"makeGsmCall" -> { "makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber") val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) { if (phoneNumber != null) {
@ -173,25 +151,34 @@ class MainActivity : FlutterActivity() {
TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
packageName packageName
) )
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try { try {
startActivityForResult(intent, 1001) // Use startActivityForResult to track response startActivityForResult(intent, 1001)
Log.d(TAG, "Default dialer prompt launched with requestCode 1001") Log.d(TAG, "Default dialer prompt launched with requestCode 1001")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e) 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")
} }
} }
private fun launchDefaultAppsSettings() {
val settingsIntent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
startActivity(settingsIntent)
Log.d(TAG, "Opened default apps settings as fallback")
}
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") Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$data")
if (requestCode == 1001) { if (requestCode == 1001) {
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, "User rejected or canceled default dialer change") Log.d(TAG, "Default dialer prompt canceled (resultCode=$resultCode)")
launchDefaultAppsSettings()
} }
} }
} }

View File

@ -5,24 +5,38 @@ 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.net.Uri
import android.os.Bundle
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"
}
init {
Log.d(TAG, "CallConnectionService initialized")
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created")
}
override fun onDestroy() {
super.onDestroy()
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")
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("CallConnectionService", "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"
@ -33,13 +47,14 @@ class CallConnectionService : ConnectionService() {
} }
override fun onDisconnect() { override fun onDisconnect() {
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() // Start in dialing state connection.setDialing()
return connection return connection
} }
@ -47,12 +62,15 @@ class CallConnectionService : ConnectionService() {
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")
val connection = object : Connection() { val connection = object : Connection() {
override fun onAnswer() { override fun onAnswer() {
Log.d(TAG, "Connection answered")
setActive() setActive()
} }
override fun onDisconnect() { override fun onDisconnect() {
Log.d(TAG, "Connection disconnected")
setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy() destroy()
} }

View File

@ -2,6 +2,7 @@ import 'package:dialer/features/home/home_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/services/call_service.dart'; import 'package:dialer/services/call_service.dart';
import 'package:flutter/services.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -39,6 +40,9 @@ Future<void> _requestPermissions() async {
].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");
// Signal MainActivity
const channel = MethodChannel('call_service');
await channel.invokeMethod('permissionsGranted');
} else { } else {
print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
} }