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">
<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,60 +1,137 @@
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(
val phoneAccountHandle =
PhoneAccountHandle(
ComponentName(this, CallConnectionService::class.java),
"IcingDialerAccount"
)
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
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 ->
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))
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)
result.error(
"INVALID_PHONE_NUMBER",
"Phone number is required",
null
)
}
}
"hangUpCall" -> {
@ -87,19 +164,47 @@ class MainActivity : FlutterActivity() {
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()) {
@ -108,7 +213,8 @@ 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?>(
val map =
mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,

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
}

View File

@ -5,23 +5,31 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle
import android.telephony.TelephonyManager
import android.util.Log
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
object CallService {
private var phoneAccountHandle: PhoneAccountHandle? = null
fun setPhoneAccountHandle(handle: PhoneAccountHandle) {
phoneAccountHandle = handle
}
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
return try {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val uri = Uri.parse("tel:$phoneNumber")
// Check CALL_PHONE permission
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
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
} else {
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:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/services/call_service.dart';
import 'globals.dart' as globals;
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
void main() async {
@ -13,19 +15,35 @@ void main() async {
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
await cryptoService.initializeDefaultKeyPair();
// Request permissions before running the app
await _requestPermissions();
CallService(); // Initialize CallService
runApp(
MultiProvider(
providers: [
Provider<AsymmetricCryptoService>(
create: (_) => cryptoService,
),
// Add other providers here
],
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 {
const Dialer({super.key});
@ -33,11 +51,12 @@ class Dialer extends StatelessWidget {
Widget build(BuildContext context) {
return ContactState(
child: MaterialApp(
navigatorKey: CallService.navigatorKey,
theme: ThemeData(
brightness: Brightness.dark
brightness: Brightness.dark,
),
home: SafeArea(child: MyHomePage()),
)
),
);
}
}

View File

@ -4,26 +4,45 @@ import '../features/call/call_page.dart';
class CallService {
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(
BuildContext context, {
required String phoneNumber,
String? displayName,
Uint8List? thumbnail, // Added optional thumbnail
Uint8List? thumbnail,
}) async {
try {
currentPhoneNumber = phoneNumber;
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
if (result["status"] == "calling") {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: displayName ?? phoneNumber, // Fallback to phoneNumber if no name
phoneNumber: phoneNumber,
thumbnail: thumbnail, // Pass the thumbnail
),
),
);
// CallPage will be shown via CallConnectionService callback
} else if (result["status"] == "pending_default_dialer") {
print("Waiting for user to set app as default dialer");
ScaffoldMessenger.of(context).showSnackBar(