Compare commits

..

2 Commits

Author SHA1 Message Date
c886e29d75 feat: perms & UI methodchannel
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 8m24s
/ build (push) Successful in 8m24s
2025-03-04 18:44:55 +01:00
24dc5a9bbe feat: update flutter UI via methodchannel, permissions via flutter at startup 2025-03-04 18:43:48 +01:00
6 changed files with 247 additions and 83 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,60 +1,137 @@
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 =
PhoneAccountHandle(
ComponentName(this, CallConnectionService::class.java), ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount" "IcingDialerAccount"
) )
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Icing Dialer") val phoneAccount =
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(
PhoneAccount.CAPABILITY_CALL_PROVIDER or
PhoneAccount.CAPABILITY_CONNECTION_MANAGER
)
.build() .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)
.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"makeGsmCall" -> { "makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber") val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) { if (phoneNumber != null) {
val success = CallService.makeGsmCall(this, phoneNumber) val success = CallService.makeGsmCall(this, phoneNumber)
if (success) { if (success) {
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber)) result.success(
mapOf(
"status" to "calling",
"phoneNumber" to phoneNumber
)
)
} else { } else {
result.error("CALL_FAILED", "Failed to initiate call", null) result.error("CALL_FAILED", "Failed to initiate call", null)
} }
} else { } else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null) result.error(
"INVALID_PHONE_NUMBER",
"Phone number is required",
null
)
} }
} }
"hangUpCall" -> { "hangUpCall" -> {
@ -87,19 +164,47 @@ class MainActivity : FlutterActivity() {
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()) {
@ -108,7 +213,8 @@ 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 =
mutableMapOf<String, Any?>(
"number" to number, "number" to number,
"type" to type, "type" to type,
"date" to date, "date" to date,

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
} }

View File

@ -5,23 +5,31 @@ import android.content.Context
import android.content.Intent 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.telecom.TelecomManager import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle
import android.telephony.TelephonyManager 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
object CallService { object CallService {
private var phoneAccountHandle: PhoneAccountHandle? = null
fun setPhoneAccountHandle(handle: PhoneAccountHandle) {
phoneAccountHandle = handle
}
fun makeGsmCall(context: Context, phoneNumber: String): Boolean { fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
return try { return try {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val uri = Uri.parse("tel:$phoneNumber") val uri = Uri.parse("tel:$phoneNumber")
// Check CALL_PHONE permission
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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
telecomManager.placeCall(uri, null) val extras = Bundle()
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
telecomManager.placeCall(uri, extras)
true true
} else { } else {
Log.e("CallService", "GSM call not supported below Android M") Log.e("CallService", "GSM call not supported below Android M")

View File

@ -1,8 +1,10 @@
import 'package:dialer/features/home/home_page.dart'; 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 '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:provider/provider.dart'; import 'package:provider/provider.dart';
void main() async { void main() async {
@ -13,19 +15,35 @@ void main() async {
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
await cryptoService.initializeDefaultKeyPair(); await cryptoService.initializeDefaultKeyPair();
// Request permissions before running the app
await _requestPermissions();
CallService(); // Initialize CallService
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
Provider<AsymmetricCryptoService>( Provider<AsymmetricCryptoService>(
create: (_) => cryptoService, create: (_) => cryptoService,
), ),
// Add other providers here
], ],
child: Dialer(), child: Dialer(),
), ),
); );
} }
Future<void> _requestPermissions() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.phone,
Permission.contacts,
].request();
if (statuses.values.every((status) => status.isGranted)) {
print("All required permissions granted");
} else {
print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
}
}
class Dialer extends StatelessWidget { class Dialer extends StatelessWidget {
const Dialer({super.key}); const Dialer({super.key});
@ -33,11 +51,12 @@ class Dialer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContactState( return ContactState(
child: MaterialApp( child: MaterialApp(
navigatorKey: CallService.navigatorKey,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark brightness: Brightness.dark,
), ),
home: SafeArea(child: MyHomePage()), home: SafeArea(child: MyHomePage()),
) ),
); );
} }
} }

View File

@ -4,26 +4,45 @@ 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;
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,
),
),
);
} else if (state == "disconnected") {
Navigator.pop(navigatorKey.currentContext!);
}
}
});
}
// 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,
String? displayName, String? displayName,
Uint8List? thumbnail, // Added optional thumbnail Uint8List? thumbnail,
}) async { }) async {
try { try {
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") {
Navigator.push( // CallPage will be shown via CallConnectionService callback
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: displayName ?? phoneNumber, // Fallback to phoneNumber if no name
phoneNumber: phoneNumber,
thumbnail: thumbnail, // Pass the thumbnail
),
),
);
} else if (result["status"] == "pending_default_dialer") { } else if (result["status"] == "pending_default_dialer") {
print("Waiting for user to set app as default dialer"); print("Waiting for user to set app as default dialer");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(