Compare commits

..

No commits in common. "4f2d2d5d2b67885309e3926f64ec9d17eb587053" and "fb5f155430dadafdbdcb09f2e9ef0f0bf96d23ce" have entirely different histories.

13 changed files with 157 additions and 918 deletions

View File

@ -7,7 +7,6 @@ gradle-wrapper.jar
/gradle.properties
GeneratedPluginRegistrant.java
gradle.properties
.cxx
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore

View File

@ -1,6 +1,4 @@
<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" />
@ -9,10 +7,7 @@
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="Icing Dialer"
@ -40,48 +35,7 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Dialer intent filters (required for default dialer eligibility) -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="tel" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" />
</intent-filter>
</activity>
<service
android:name=".services.MyInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
</service>
<!-- Custom ConnextionService, will be needed at some point when we implement our own protocol -->
<!-- <service
android:name=".services.CallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service> -->
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@ -1,222 +1,94 @@
package com.icing.dialer.activities
import android.Manifest
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.CallLog
import android.telecom.TelecomManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.icing.dialer.KeystoreHelper
import com.icing.dialer.services.CallService
import com.icing.dialer.services.MyInCallService
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
class MainActivity: FlutterActivity() {
// Existing channel for keystore operations.
private val KEYSTORE_CHANNEL = "com.example.keystore"
// New channel for call log access.
private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service"
private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started")
Log.d(TAG, "Waiting for Flutter to signal permissions")
handleIncomingCallIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIncomingCallIntent(intent)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(TAG, "Configuring Flutter engine")
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result ->
// Call service channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
checkAndRequestDefaultDialer()
result.success(true)
}
"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)
}
CallService.makeGsmCall(this, phoneNumber)
result.success("Calling $phoneNumber")
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
}
"hangUpCall" -> {
val success = MyInCallService.currentCall?.let {
it.disconnect()
Log.d(TAG, "Call disconnected")
MyInCallService.channel?.invokeMethod("callEnded", mapOf(
"callId" to it.details.handle.toString()
))
true
} ?: false
if (success) {
result.success(mapOf("status" to "ended"))
} else {
Log.w(TAG, "No active call to hang up")
result.error("HANGUP_FAILED", "No active call to hang up", null)
}
}
"answerCall" -> {
val success = MyInCallService.currentCall?.let {
it.answer(0)
Log.d(TAG, "Answered call")
true
} ?: false
if (success) {
result.success(mapOf("status" to "answered"))
} else {
Log.w(TAG, "No active call to answer")
result.error("ANSWER_FAILED", "No active call to answer", null)
}
CallService.hangUpCall(this)
result.success("Call ended")
}
else -> result.notImplemented()
}
}
// Set up the keystore channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
.setMethodCallHandler { call, result ->
// Delegate method calls to KeystoreHelper.
KeystoreHelper(call, result).handleMethodCall()
}
// Set up the call log channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
val callLogs = getCallLogs()
result.success(callLogs)
} else {
requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION)
result.error("PERMISSION_DENIED", "Call log permission not granted", null)
}
} else {
result.notImplemented()
}
}
}
private fun checkAndRequestDefaultDialer() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
if (currentDefault != packageName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) && !roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
} else {
Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
}
} else {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
Log.d(TAG, "Launched TelecomManager intent for default dialer")
} catch (e: Exception) {
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
launchDefaultAppsSettings()
}
}
} else {
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?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$data")
if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "User accepted default dialer change")
} else {
Log.w(TAG, "Default dialer prompt canceled or failed (resultCode=$resultCode)")
launchDefaultAppsSettings()
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_CALL_LOG_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Call log permission granted")
MyInCallService.channel?.invokeMethod("callLogPermissionGranted", null)
} else {
Log.w(TAG, "Call log permission denied")
}
}
}
/**
* Queries the Android call log and returns a list of maps.
* Each map contains keys: "number", "type", "date", and "duration".
*/
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"
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))
val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
if (cursor != null) {
while (cursor.moveToNext()) {
val number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
val type = cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))
val duration = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION))
val map = mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,
"duration" to duration
)
val map = HashMap<String, Any?>()
map["number"] = number
map["type"] = type // Typically: 1 for incoming, 2 for outgoing, 3 for missed.
map["date"] = date
map["duration"] = duration
logsList.add(map)
}
cursor.close()
}
return logsList
}
private fun handleIncomingCallIntent(intent: Intent?) {
intent?.let {
if (it.getBooleanExtra("isIncomingCall", false)) {
val phoneNumber = it.getStringExtra("phoneNumber")
Log.d(TAG, "Received incoming call intent for $phoneNumber")
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
"phoneNumber" to phoneNumber
))
}
}
}
}

View File

@ -1,82 +0,0 @@
// package com.icing.dialer.services
// import android.telecom.Connection
// import android.telecom.ConnectionService
// import android.telecom.PhoneAccountHandle
// import android.telecom.TelecomManager
// import android.telecom.DisconnectCause
// import android.util.Log
// import io.flutter.plugin.common.MethodChannel
// class CallConnectionService : ConnectionService() {
// companion object {
// 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(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onStateChanged(state: Int) {
// super.onStateChanged(state)
// Log.d(TAG, "Connection state changed: $state")
// 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() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setInitialized()
// connection.setDialing()
// return connection
// }
// override fun onCreateIncomingConnection(
// connectionManagerPhoneAccount: PhoneAccountHandle?,
// request: android.telecom.ConnectionRequest
// ): Connection {
// Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
// val connection = object : Connection() {
// override fun onAnswer() {
// Log.d(TAG, "Connection answered")
// setActive()
// }
// override fun onDisconnect() {
// Log.d(TAG, "Connection disconnected")
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
// destroy()
// }
// }
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
// connection.setRinging()
// return connection
// }
// }

View File

@ -1,55 +1,30 @@
package com.icing.dialer.services
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.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
object CallService {
private val TAG = "CallService"
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
return try {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val uri = Uri.parse("tel:$phoneNumber")
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
telecomManager.placeCall(uri, Bundle())
Log.d(TAG, "Initiated call to $phoneNumber")
true
} else {
Log.e(TAG, "CALL_PHONE permission not granted")
false
}
fun makeGsmCall(context: Context, phoneNumber: String) {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:$phoneNumber")
context.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Error making GSM call: ${e.message}", e)
false
Log.e("CallService", "Error making GSM call: ${e.message}")
}
}
fun hangUpCall(context: Context): Boolean {
return try {
if (MyInCallService.currentCall != null) {
MyInCallService.currentCall?.disconnect()
Log.d(TAG, "Disconnected active call via MyInCallService")
true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
fun hangUpCall(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
Log.d(TAG, "Ended call via TelecomManager (no active call in MyInCallService)")
true
} else {
Log.e(TAG, "No active call and hangup not supported below Android P")
false
}
} catch (e: Exception) {
Log.e(TAG, "Error hanging up call: ${e.message}", e)
false
Log.e("CallService", "Hangup call is only supported on Android P or later.")
}
}
}

View File

@ -1,137 +0,0 @@
package com.icing.dialer.services
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.telecom.Call
import android.telecom.InCallService
import android.util.Log
import androidx.core.app.NotificationCompat
import com.icing.dialer.activities.MainActivity
import io.flutter.plugin.common.MethodChannel
class MyInCallService : InCallService() {
companion object {
var channel: MethodChannel? = null
var currentCall: Call? = null
private const val TAG = "MyInCallService"
private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel"
private const val NOTIFICATION_ID = 1
}
private val callCallback = object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
super.onStateChanged(call, state)
val stateStr = when (state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
Call.STATE_DISCONNECTED -> "disconnected"
Call.STATE_DISCONNECTING -> "disconnecting"
Call.STATE_RINGING -> "ringing"
else -> "unknown"
}
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
channel?.invokeMethod("callStateChanged", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
if (state == Call.STATE_RINGING) {
showIncomingCallNotification(call.details.handle.toString().replace("tel:", ""))
} else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
Log.d(TAG, "Call ended: ${call.details.handle}")
channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString()))
currentCall = null
cancelNotification()
}
}
}
override fun onCallAdded(call: Call) {
super.onCallAdded(call)
currentCall = call
val stateStr = when (call.state) {
Call.STATE_DIALING -> "dialing"
Call.STATE_ACTIVE -> "active"
Call.STATE_RINGING -> "ringing"
else -> "dialing"
}
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
channel?.invokeMethod("callAdded", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
if (stateStr == "ringing") {
showIncomingCallNotification(call.details.handle.toString().replace("tel:", ""))
}
call.registerCallback(callCallback)
}
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
Log.d(TAG, "Call removed: ${call.details.handle}")
call.unregisterCallback(callCallback)
channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString()))
currentCall = null
cancelNotification()
}
override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) {
super.onCallAudioStateChanged(state)
Log.d(TAG, "Audio state changed: route=${state.route}")
channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route))
}
private fun showIncomingCallNotification(phoneNumber: String) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Create notification channel (Android 8.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Incoming Calls",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications for incoming calls"
enableVibration(true)
setShowBadge(true)
}
notificationManager.createNotificationChannel(channel)
}
// Intent to open MainActivity with phone number
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("phoneNumber", phoneNumber)
putExtra("isIncomingCall", true)
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
)
// Build notification
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_alert) // Replace with your app icon
.setContentTitle("Incoming Call")
.setContentText("Call from $phoneNumber")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setOngoing(true) // Keep visible until call ends
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
Log.d(TAG, "Notification shown for incoming call from $phoneNumber")
}
private fun cancelNotification() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NOTIFICATION_ID)
Log.d(TAG, "Notification canceled")
}
}

View File

@ -1,20 +1,13 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:dialer/services/call_service.dart';
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart';
class CallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const CallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
const CallPage({super.key, required this.displayName, this.thumbnail});
@override
_CallPageState createState() => _CallPageState();
@ -22,12 +15,11 @@ class CallPage extends StatefulWidget {
class _CallPageState extends State<CallPage> {
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
bool isMuted = false;
bool isSpeakerOn = false;
bool isKeypadVisible = false;
bool icingProtocolOk = true;
String _typedDigits = "";
String _typedDigits = ""; // New state variable for pressed digits
void _addDigit(String digit) {
setState(() {
@ -59,26 +51,15 @@ class _CallPageState extends State<CallPage> {
});
}
void _hangUp() async {
try {
final result = await _callService.hangUpCall(context);
print('CallPage: Hang up result: $result');
if (result["status"] == "ended" && mounted && Navigator.canPop(context)) {
void _hangUp() {
Navigator.pop(context);
}
} catch (e) {
print("CallPage: Error hanging up: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error hanging up: $e")),
);
}
}
@override
Widget build(BuildContext context) {
final double avatarRadius = isKeypadVisible ? 45.0 : 45.0;
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0;
final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; // Smaller avatar
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; // Smaller font
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; // Smaller status
return Scaffold(
body: Container(
@ -86,6 +67,7 @@ class _CallPageState extends State<CallPage> {
child: SafeArea(
child: Column(
children: [
// Top section - make it more compact
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
@ -95,8 +77,7 @@ class _CallPageState extends State<CallPage> {
ObfuscatedAvatar(
imageBytes: widget.thumbnail,
radius: avatarRadius,
backgroundColor:
generateColorFromName(widget.displayName),
backgroundColor: generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName,
),
const SizedBox(height: 4),
@ -128,24 +109,23 @@ class _CallPageState extends State<CallPage> {
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: TextStyle(
fontSize: statusFontSize, color: Colors.white70),
),
Text(
'Calling...',
style: TextStyle(
fontSize: statusFontSize, color: Colors.white70),
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
],
),
),
// Middle section - make it flexible and scrollable if needed
Expanded(
child: Column(
children: [
if (isKeypadVisible) ...[
// Add spacer to push keypad down
const Spacer(flex: 2),
// Typed digits display
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
@ -167,12 +147,13 @@ class _CallPageState extends State<CallPage> {
IconButton(
padding: EdgeInsets.zero,
onPressed: _toggleKeypad,
icon:
const Icon(Icons.close, color: Colors.white),
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
),
// Keypad grid
Container(
height: MediaQuery.of(context).size.height * 0.35,
margin: const EdgeInsets.symmetric(horizontal: 20),
@ -204,8 +185,7 @@ class _CallPageState extends State<CallPage> {
child: Center(
child: Text(
label,
style: const TextStyle(
fontSize: 32, color: Colors.white),
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
@ -213,17 +193,22 @@ class _CallPageState extends State<CallPage> {
}),
),
),
// Add spacer after keypad
const Spacer(flex: 1),
] else ...[
const Spacer(),
// Control buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Main control buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Mute
Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -237,78 +222,75 @@ class _CallPageState extends State<CallPage> {
),
Text(
isMuted ? 'Unmute' : 'Mute',
style: const TextStyle(
color: Colors.white, fontSize: 14),
style: const TextStyle(color: Colors.white, fontSize: 14),
),
],
),
// Keypad
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleKeypad,
icon: const Icon(Icons.dialpad,
color: Colors.white, size: 32),
icon: const Icon(Icons.dialpad, color: Colors.white, size: 32),
),
const Text(
'Keypad',
style: TextStyle(
color: Colors.white, fontSize: 14),
style: TextStyle(color: Colors.white, fontSize: 14),
),
],
),
// Speaker
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleSpeaker,
icon: Icon(
isSpeakerOn
? Icons.volume_up
: Icons.volume_off,
color: isSpeakerOn
? Colors.amber
: Colors.white,
isSpeakerOn ? Icons.volume_up : Icons.volume_off,
color: isSpeakerOn ? Colors.amber : Colors.white,
size: 32,
),
),
const Text(
'Speaker',
style: TextStyle(
color: Colors.white, fontSize: 14),
style: TextStyle(color: Colors.white, fontSize: 14),
),
],
),
],
),
const SizedBox(height: 20),
// Additional buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Add Contact
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.person_add,
color: Colors.white, size: 32),
onPressed: () {
// ...existing code...
},
icon: const Icon(Icons.person_add, color: Colors.white, size: 32),
),
const Text('Add Contact',
style: TextStyle(
color: Colors.white, fontSize: 14)),
style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
// Change SIM
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.sim_card,
color: Colors.white, size: 32),
onPressed: () {
// ...existing code...
},
icon: const Icon(Icons.sim_card, color: Colors.white, size: 32),
),
const Text('Change SIM',
style: TextStyle(
color: Colors.white, fontSize: 14)),
style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
],
@ -321,6 +303,8 @@ class _CallPageState extends State<CallPage> {
],
),
),
// Bottom section - hang up button
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: GestureDetector(

View File

@ -1,181 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dialer/services/call_service.dart';
import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:dialer/features/call/call_page.dart';
class IncomingCallPage extends StatefulWidget {
final String displayName;
final String phoneNumber;
final Uint8List? thumbnail;
const IncomingCallPage({
super.key,
required this.displayName,
required this.phoneNumber,
this.thumbnail,
});
@override
_IncomingCallPageState createState() => _IncomingCallPageState();
}
class _IncomingCallPageState extends State<IncomingCallPage> {
static const MethodChannel _channel = MethodChannel('call_service');
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
bool icingProtocolOk = true;
void _toggleIcingProtocol() {
setState(() {
icingProtocolOk = !icingProtocolOk;
});
}
void _answerCall() async {
try {
final result = await _channel.invokeMethod('answerCall');
print('IncomingCallPage: Answer call result: $result');
if (result["status"] == "answered") {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CallPage(
displayName: widget.displayName,
phoneNumber: widget.phoneNumber,
thumbnail: widget.thumbnail,
),
),
);
}
} catch (e) {
print("IncomingCallPage: Error answering call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error answering call: $e")),
);
}
}
void _declineCall() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
print("IncomingCallPage: Error declining call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error declining call: $e")),
);
}
}
@override
Widget build(BuildContext context) {
const double avatarRadius = 45.0;
const double nameFontSize = 24.0;
const double statusFontSize = 16.0;
return Scaffold(
body: Container(
color: Colors.black,
child: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 35),
ObfuscatedAvatar(
imageBytes: widget.thumbnail,
radius: avatarRadius,
backgroundColor: generateColorFromName(widget.displayName),
fallbackInitial: widget.displayName,
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icingProtocolOk ? Icons.lock : Icons.lock_open,
color: icingProtocolOk ? Colors.green : Colors.red,
size: 16,
),
const SizedBox(width: 4),
Text(
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
style: TextStyle(
color: icingProtocolOk ? Colors.green : Colors.red,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Text(
_obfuscateService.obfuscateData(widget.displayName),
style: const TextStyle(
fontSize: nameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: const TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
const Text(
'Incoming Call...',
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
],
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: _declineCall,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call_end,
color: Colors.white,
size: 32,
),
),
),
GestureDetector(
onTap: _answerCall,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call,
color: Colors.white,
size: 32,
),
),
),
],
),
),
const SizedBox(height: 16),
],
),
),
),
);
}
}

View File

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../services/contact_service.dart';
import '../../services/obfuscate_service.dart';
import '../../services/call_service.dart';
import '../../services/obfuscate_service.dart'; // Import ObfuscateService
import '../../services/call_service.dart'; // Import the CallService
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget {
@ -76,7 +76,7 @@ class _CompositionPageState extends State<CompositionPage> {
// Function to call a contact's number using the CallService
void _makeCall(String phoneNumber) async {
try {
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
await _callService.makeGsmCall(phoneNumber);
setState(() {
dialedNumber = phoneNumber;
});

View File

@ -5,7 +5,8 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import '../../../services/block_service.dart';
import '../../../services/contact_service.dart';
import '../../../services/call_service.dart';
import '../../../features/call/call_page.dart';
import '../../../services/call_service.dart'; // Import CallService
class ContactModal extends StatefulWidget {
final Contact contact;
@ -29,7 +30,7 @@ class _ContactModalState extends State<ContactModal> {
late String phoneNumber;
bool isBlocked = false;
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
final CallService _callService = CallService(); // Instantiate CallService
@override
void initState() {
@ -126,9 +127,7 @@ class _ContactModalState extends State<ContactModal> {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
);
// Close the modal
@ -136,9 +135,7 @@ class _ContactModalState extends State<ContactModal> {
} catch (e) {
// Handle errors and show a failure message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Failed to delete ${widget.contact.displayName}: $e')),
SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')),
);
}
}
@ -247,8 +244,7 @@ class _ContactModalState extends State<ContactModal> {
),
const SizedBox(height: 10),
Text(
_obfuscateService
.obfuscateData(widget.contact.displayName),
_obfuscateService.obfuscateData(widget.contact.displayName),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -267,10 +263,21 @@ class _ContactModalState extends State<ContactModal> {
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context,
phoneNumber: phoneNumber);
await _callService.makeGsmCall(phoneNumber);
}
},
onLongPress: () {
// Navigate to the beautiful calling page demo
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CallPage(
displayName: widget.contact.displayName,
thumbnail: widget.contact.thumbnail,
),
),
);
},
),
ListTile(
leading: const Icon(Icons.message, color: Colors.blue),
@ -313,8 +320,9 @@ class _ContactModalState extends State<ContactModal> {
icon: Icon(widget.isFavorite
? Icons.star
: Icons.star_border),
label: Text(
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
label: Text(widget.isFavorite
? 'Unfavorite'
: 'Favorite'),
),
),
const SizedBox(height: 10),

View File

@ -419,7 +419,7 @@ class _HistoryPageState extends State<HistoryPage>
icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async {
if (contact.phones.isNotEmpty) {
_callService.makeGsmCall(context, phoneNumber: contact.phones.first.number);
_callService.makeGsmCall(contact.phones.first.number);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(

View File

@ -1,11 +1,8 @@
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 'package:flutter/services.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 {
@ -16,38 +13,19 @@ void main() async {
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
await cryptoService.initializeDefaultKeyPair();
// Request permissions before running the app
await _requestPermissions();
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,
Permission.microphone,
].request();
if (statuses.values.every((status) => status.isGranted)) {
print("All required permissions granted");
const channel = MethodChannel('call_service');
await channel.invokeMethod('permissionsGranted');
} else {
print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
}
}
class Dialer extends StatelessWidget {
const Dialer({super.key});
@ -55,12 +33,11 @@ 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

@ -1,155 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../features/call/call_page.dart';
import '../features/call/incoming_call_page.dart';
// Service to manage call-related operations
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber;
static bool _isCallPageVisible = false;
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
CallService() {
_channel.setMethodCallHandler((call) async {
final context = navigatorKey.currentContext;
if (context == null) {
print('CallService: Navigator context is null, cannot navigate');
return;
}
switch (call.method) {
case "callAdded":
final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
break;
case "callStateChanged":
final state = call.arguments["state"] as String;
print('CallService: State changed to $state');
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
break;
case "callEnded":
case "callRemoved":
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
break;
case "incomingCallFromNotification":
final phoneNumber = call.arguments["phoneNumber"] as String;
currentPhoneNumber = phoneNumber;
print('CallService: Incoming call from notification: $phoneNumber');
_navigateToIncomingCallPage(context);
break;
}
});
}
void _navigateToCallPage(BuildContext context) {
print('CallService: Navigating to CallPage');
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
print('CallService: CallPage popped, _isCallPageVisible set to false');
});
_isCallPageVisible = true;
}
void _navigateToIncomingCallPage(BuildContext context) {
print('CallService: Navigating to IncomingCallPage');
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
print('CallService: IncomingCallPage popped, _isCallPageVisible set to false');
});
_isCallPageVisible = true;
}
void _closeCallPage(BuildContext context) {
print('CallService: Attempting to close call page, _isCallPageVisible: $_isCallPageVisible');
if (Navigator.canPop(context)) {
print('CallService: Popping call page');
Navigator.pop(context);
_isCallPageVisible = false;
} else {
print('CallService: No page to pop');
}
}
Future<Map<String, dynamic>> makeGsmCall(
BuildContext context, {
required String phoneNumber,
String? displayName,
Uint8List? thumbnail,
}) async {
// Function to make a GSM call
Future<void> makeGsmCall(String phoneNumber) async {
try {
currentPhoneNumber = phoneNumber;
print('CallService: Making GSM call to $phoneNumber');
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
print('CallService: makeGsmCall result: $result');
final resultMap = Map<String, dynamic>.from(result as Map); // Safe cast
if (resultMap["status"] != "calling") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")),
);
}
return resultMap;
await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
} catch (e) {
print("CallService: Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error making call: $e")),
);
print("Error making call: $e");
rethrow;
}
}
Future<Map<String, dynamic>> hangUpCall(BuildContext context) async {
// Function to hang up the current call
Future<void> hangUpCall() async {
try {
print('CallService: Hanging up call');
final result = await _channel.invokeMethod('hangUpCall');
print('CallService: hangUpCall result: $result');
final resultMap = Map<String, dynamic>.from(result as Map); // Safe cast
if (resultMap["status"] != "ended") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")),
);
}
return resultMap;
await _channel.invokeMethod('hangUpCall');
} catch (e) {
print("CallService: Error hanging up call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error hanging up call: $e")),
);
print("Error hanging up call: $e");
rethrow;
}
}