Compare commits

..

1 Commits

Author SHA1 Message Date
AlexisDanlos
7ff9418e06 bug fix: enhance contact fetching with permission handling
Some checks failed
/ mirror (push) Successful in 4s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled
2025-03-06 15:44:03 +01:00
16 changed files with 314 additions and 1312 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,9 +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" />
<application
android:label="Icing Dialer"
@ -39,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,91 +1,54 @@
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() {
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate started")
Log.d(TAG, "Waiting for Flutter to signal permissions")
}
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 ->
when (call.method) {
"permissionsGranted" -> {
Log.d(TAG, "Received permissionsGranted from Flutter")
checkAndRequestDefaultDialer()
result.success(true)
// Call service channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
if (phoneNumber != null) {
CallService.makeGsmCall(this, phoneNumber)
result.success("Calling $phoneNumber")
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
"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)
}
} else {
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
}
}
"hangUpCall" -> {
val success = CallService.hangUpCall(this)
if (success) {
result.success(mapOf("status" to "ended"))
} else {
result.error("HANGUP_FAILED", "Failed to end call", null)
}
}
"answerCall" -> {
val success = MyInCallService.currentCall?.let {
it.answer(0) // 0 for default video state (audio-only)
Log.d(TAG, "Answered call")
true
} ?: false
if (success) {
result.success(mapOf("status" to "answered"))
} else {
result.error("ANSWER_FAILED", "No active call to answer", null)
}
}
else -> result.notImplemented()
}
"hangUpCall" -> {
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()
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
.setMethodCallHandler { call, result -> KeystoreHelper(call, result).handleMethodCall() }
// Set up the call log channel.
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getCallLogs") {
@ -97,78 +60,35 @@ class MainActivity : FlutterActivity() {
}
}
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()
}
}
}
/**
* 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
}
}
}

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) {
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
fun hangUpCall(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
} else {
Log.e("CallService", "Hangup call is only supported on Android P or later.")
}
}
}

View File

@ -1,69 +0,0 @@
package com.icing.dialer.services
import android.telecom.Call
import android.telecom.InCallService
import android.util.Log
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 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_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
}
}
}
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" // Default for outgoing
}
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
channel?.invokeMethod("callAdded", mapOf(
"callId" to call.details.handle.toString(),
"state" to stateStr
))
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
}
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))
}
}

View File

@ -1,324 +0,0 @@
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,
});
@override
_CallPageState createState() => _CallPageState();
}
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 = "";
void _addDigit(String digit) {
setState(() {
_typedDigits += digit;
});
}
void _toggleMute() {
setState(() {
isMuted = !isMuted;
});
}
void _toggleSpeaker() {
setState(() {
isSpeakerOn = !isSpeakerOn;
});
}
void _toggleKeypad() {
setState(() {
isKeypadVisible = !isKeypadVisible;
});
}
void _toggleIcingProtocol() {
setState(() {
icingProtocolOk = !icingProtocolOk;
});
}
void _hangUp() async {
try {
await _callService.hangUpCall(context);
} catch (e) {
print("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;
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: [
SizedBox(height: 35),
ObfuscatedAvatar(
imageBytes: widget.thumbnail, // Uses thumbnail if provided
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: TextStyle(
fontSize: nameFontSize,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
widget.phoneNumber,
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
Text(
'Calling...',
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
),
],
),
),
Expanded(
child: Column(
children: [
if (isKeypadVisible) ...[
const Spacer(flex: 2),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
_typedDigits,
maxLines: 1,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
padding: EdgeInsets.zero,
onPressed: _toggleKeypad,
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
),
Container(
height: MediaQuery.of(context).size.height * 0.35,
margin: const EdgeInsets.symmetric(horizontal: 20),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: List.generate(12, (index) {
String label;
if (index < 9) {
label = '${index + 1}';
} else if (index == 9) {
label = '*';
} else if (index == 10) {
label = '0';
} else {
label = '#';
}
return GestureDetector(
onTap: () => _addDigit(label),
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
),
child: Center(
child: Text(
label,
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
);
}),
),
),
const Spacer(flex: 1),
] else ...[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleMute,
icon: Icon(
isMuted ? Icons.mic_off : Icons.mic,
color: Colors.white,
size: 32,
),
),
Text(
isMuted ? 'Unmute' : 'Mute',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleKeypad,
icon: const Icon(Icons.dialpad, color: Colors.white, size: 32),
),
const Text(
'Keypad',
style: TextStyle(color: Colors.white, fontSize: 14),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _toggleSpeaker,
icon: Icon(
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),
),
],
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.person_add, color: Colors.white, size: 32),
),
const Text('Add Contact',
style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.sim_card, color: Colors.white, size: 32),
),
const Text('Change SIM',
style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
],
),
],
),
),
const Spacer(flex: 3),
],
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: GestureDetector(
onTap: _hangUp,
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,
),
),
),
),
],
),
),
),
);
}
}

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,9 @@ 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 {
const CompositionPage({super.key});
@ -17,7 +18,11 @@ class _CompositionPageState extends State<CompositionPage> {
List<Contact> _allContacts = [];
List<Contact> _filteredContacts = [];
final ContactService _contactService = ContactService();
// Instantiate the ObfuscateService
final ObfuscateService _obfuscateService = ObfuscateService();
// Instantiate the CallService
final CallService _callService = CallService();
@override
@ -35,13 +40,8 @@ class _CompositionPageState extends State<CompositionPage> {
void _filterContacts() {
setState(() {
_filteredContacts = _allContacts.where((contact) {
bool phoneMatch = contact.phones.any((phone) {
final rawPhoneNumber = phone.number;
final strippedPhoneNumber = rawPhoneNumber.replaceAll(RegExp(r'\D'), '');
final strippedDialedNumber = dialedNumber.replaceAll(RegExp(r'\D'), '');
return rawPhoneNumber.contains(dialedNumber) ||
strippedPhoneNumber.contains(strippedDialedNumber);
});
final phoneMatch = contact.phones.any((phone) =>
phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber));
final nameMatch = contact.displayName
.toLowerCase()
.contains(dialedNumber.toLowerCase());
@ -57,13 +57,6 @@ class _CompositionPageState extends State<CompositionPage> {
});
}
void _onPlusPress() {
setState(() {
dialedNumber += '+';
_filterContacts();
});
}
void _onDeletePress() {
setState(() {
if (dialedNumber.isNotEmpty) {
@ -80,20 +73,19 @@ 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;
});
} catch (e) {
debugPrint("Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to make call: $e')),
);
}
}
// Function to send an SMS to a contact's number
void _launchSms(String phoneNumber) async {
final uri = Uri(scheme: 'sms', path: phoneNumber);
if (await canLaunchUrl(uri)) {
@ -103,20 +95,6 @@ class _CompositionPageState extends State<CompositionPage> {
}
}
void _addContact() async {
if (await FlutterContacts.requestPermission()) {
final newContact = Contact()
..phones = [Phone(dialedNumber.isNotEmpty ? dialedNumber : '')];
final updatedContact = await FlutterContacts.openExternalInsert(newContact);
if (updatedContact != null) {
_fetchContacts();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Contact added successfully!')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -125,6 +103,7 @@ class _CompositionPageState extends State<CompositionPage> {
children: [
Column(
children: [
// Top half: Display contacts matching dialed number
Expanded(
flex: 2,
child: Container(
@ -136,51 +115,57 @@ class _CompositionPageState extends State<CompositionPage> {
children: [
Expanded(
child: ListView(
children: [
..._filteredContacts.map((contact) {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
return ListTile(
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.phone, color: Colors.green[300], size: 20),
onPressed: () => _makeCall(phoneNumber),
),
IconButton(
icon: Icon(Icons.message, color: Colors.blue[300], size: 20),
onPressed: () => _launchSms(phoneNumber),
),
],
),
onTap: () {},
);
}).toList(),
ListTile(
title: const Text(
'Add a contact',
style: TextStyle(color: Colors.white),
children: _filteredContacts.isNotEmpty
? _filteredContacts.map((contact) {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
return ListTile(
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
trailing: Icon(Icons.add, color: Colors.grey[600]),
onTap: _addContact,
),
],
subtitle: Text(
_obfuscateService.obfuscateData(phoneNumber),
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Call button (Now using CallService)
IconButton(
icon: Icon(Icons.phone,
color: Colors.green[300],
size: 20),
onPressed: () {
_makeCall(phoneNumber); // Make a call using CallService
},
),
// Message button
IconButton(
icon: Icon(Icons.message,
color: Colors.blue[300],
size: 20),
onPressed: () {
_launchSms(phoneNumber);
},
),
],
),
onTap: () {
// Handle contact selection if needed
},
);
}).toList()
: [],
),
),
],
),
),
),
// Bottom half: Dialpad and Dialed number display with erase button
Expanded(
flex: 2,
child: Container(
@ -188,6 +173,7 @@ class _CompositionPageState extends State<CompositionPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Display dialed number with erase button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -196,57 +182,61 @@ class _CompositionPageState extends State<CompositionPage> {
alignment: Alignment.center,
child: Text(
dialedNumber,
style: const TextStyle(fontSize: 24, color: Colors.white),
style: const TextStyle(
fontSize: 24, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
),
GestureDetector(
onTap: _onDeletePress,
onLongPress: _onClearPress,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.backspace, color: Colors.white),
),
IconButton(
onPressed: _onClearPress,
icon: const Icon(Icons.backspace,
color: Colors.white),
),
],
),
const SizedBox(height: 10),
// Dialpad
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('1', Colors.white),
_buildDialButton('2', Colors.white),
_buildDialButton('3', Colors.white),
_buildDialButton('1'),
_buildDialButton('2'),
_buildDialButton('3'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('4', Colors.white),
_buildDialButton('5', Colors.white),
_buildDialButton('6', Colors.white),
_buildDialButton('4'),
_buildDialButton('5'),
_buildDialButton('6'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('7', Colors.white),
_buildDialButton('8', Colors.white),
_buildDialButton('9', Colors.white),
_buildDialButton('7'),
_buildDialButton('8'),
_buildDialButton('9'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildDialButton('*', const Color.fromRGBO(117, 117, 117, 1)),
_buildDialButtonWithPlus('0'),
_buildDialButton('#', const Color.fromRGBO(117, 117, 117, 1)),
_buildDialButton('*'),
_buildDialButton('0'),
_buildDialButton('#'),
],
),
],
@ -259,28 +249,26 @@ class _CompositionPageState extends State<CompositionPage> {
),
],
),
// Add Contact Button
Positioned(
bottom: 20.0,
left: 0,
right: 0,
child: Center(
child: ElevatedButton(
onPressed: dialedNumber.isNotEmpty ? () => _makeCall(dialedNumber) : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[700],
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: const Icon(Icons.phone, color: Colors.white, size: 30),
),
child: AddContactButton(),
),
),
// Top Row with Back Arrow
Positioned(
top: 40.0,
left: 16.0,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
onPressed: () {
Navigator.pop(context);
},
),
),
],
@ -288,7 +276,7 @@ class _CompositionPageState extends State<CompositionPage> {
);
}
Widget _buildDialButton(String number, Color textColor) {
Widget _buildDialButton(String number) {
return ElevatedButton(
onPressed: () => _onNumberPress(number),
style: ElevatedButton.styleFrom(
@ -298,38 +286,11 @@ class _CompositionPageState extends State<CompositionPage> {
),
child: Text(
number,
style: TextStyle(fontSize: 24, color: textColor),
style: const TextStyle(
fontSize: 24,
color: Colors.white,
),
),
);
}
Widget _buildDialButtonWithPlus(String number) {
return Stack(
alignment: Alignment.center,
children: [
GestureDetector(
onLongPress: _onPlusPress,
child: ElevatedButton(
onPressed: () => _onNumberPress(number),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: Text(
number,
style: const TextStyle(fontSize: 24, color: Colors.white),
),
),
),
Positioned(
bottom: 8,
child: Text(
'+',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
);
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../services/contact_service.dart';
class ContactState extends StatefulWidget {
@ -24,6 +25,7 @@ class _ContactStateState extends State<ContactState> {
bool _loading = true;
double _scrollOffset = 0.0;
Contact? _selfContact = Contact();
bool _permissionRequestInProgress = false;
// Getters for all contacts and favorites
List<Contact> get contacts => _allContacts;
@ -35,8 +37,26 @@ class _ContactStateState extends State<ContactState> {
@override
void initState() {
super.initState();
fetchContacts(); // Fetch all contacts by default
FlutterContacts.addListener(_onContactChange);
_initializeContacts();
}
Future<void> _initializeContacts() async {
try {
final status = await Permission.contacts.status;
if (status.isGranted) {
await fetchContacts();
} else {
final result = await Permission.contacts.request();
if (result.isGranted) {
await fetchContacts();
} else {
setState(() => _loading = false);
}
}
} catch (e) {
debugPrint('Error initializing contacts: $e');
setState(() => _loading = false);
}
}
void _onContactChange() => fetchContacts();

View File

@ -5,7 +5,7 @@ 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 '../../../services/call_service.dart'; // Import CallService
class ContactModal extends StatefulWidget {
final Contact contact;
@ -29,7 +29,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 +126,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 +134,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')),
);
}
}
@ -166,7 +162,7 @@ class _ContactModalState extends State<ContactModal> {
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius:
const BorderRadius.vertical(top: Radius.circular(20)),
const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
@ -220,7 +216,7 @@ class _ContactModalState extends State<ContactModal> {
const PopupMenuItem<String>(
value: 'create_shortcut',
child:
Text('Create shortcut (to home screen)'),
Text('Create shortcut (to home screen)'),
),
const PopupMenuItem<String>(
value: 'set_ringtone',
@ -242,13 +238,12 @@ class _ContactModalState extends State<ContactModal> {
imageBytes: widget.contact.thumbnail,
radius: 50,
backgroundColor:
generateColorFromName(widget.contact.displayName),
generateColorFromName(widget.contact.displayName),
fallbackInitial: widget.contact.displayName,
),
const SizedBox(height: 10),
Text(
_obfuscateService
.obfuscateData(widget.contact.displayName),
_obfuscateService.obfuscateData(widget.contact.displayName),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -267,8 +262,7 @@ class _ContactModalState extends State<ContactModal> {
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context,
phoneNumber: phoneNumber);
await _callService.makeGsmCall(phoneNumber);
}
},
),
@ -313,8 +307,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

@ -37,7 +37,7 @@ class HistoryPage extends StatefulWidget {
}
class _HistoryPageState extends State<HistoryPage>
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
List<History> histories = [];
bool loading = true;
int? _expandedIndex;
@ -47,13 +47,10 @@ class _HistoryPageState extends State<HistoryPage>
// Create a MethodChannel instance.
static const MethodChannel _channel = MethodChannel('com.example.calllog');
@override
bool get wantKeepAlive => true; // Preserve state when switching pages
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (loading && histories.isEmpty) {
if (loading) {
_buildHistories();
}
}
@ -71,25 +68,24 @@ class _HistoryPageState extends State<HistoryPage>
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
}
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
await _refreshContacts();
} else {
print("Could not fetch contact details");
}
} catch (e) {
print("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status')));
debugPrint("Error updating favorite status: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to update contact favorite status')),
);
}
}
}
@ -152,9 +148,9 @@ class _HistoryPageState extends State<HistoryPage>
List<Contact> contacts = contactState.contacts;
List<History> callHistories = [];
// Process each log entry with intermittent yields to avoid freezing.
for (int i = 0; i < nativeLogs.length; i++) {
final entry = nativeLogs[i];
// Process each log entry.
for (var entry in nativeLogs) {
// Each entry is a Map with keys: number, type, date, duration.
final String number = entry['number'] ?? '';
if (number.isEmpty) continue;
@ -200,8 +196,6 @@ class _HistoryPageState extends State<HistoryPage>
callHistories
.add(History(matchedContact, callDate, callType, callStatus, 1));
// Yield every 10 iterations to avoid blocking the UI.
if (i % 10 == 0) await Future.delayed(Duration(milliseconds: 1));
}
// Sort histories by most recent.
@ -277,7 +271,6 @@ class _HistoryPageState extends State<HistoryPage>
@override
Widget build(BuildContext context) {
super.build(context); // required due to AutomaticKeepAliveClientMixin
final contactState = ContactState.of(context);
if (loading || contactState.loading) {
@ -425,7 +418,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

@ -10,82 +10,53 @@ import '../../services/contact_service.dart';
import 'package:dialer/features/voicemail/voicemail_page.dart';
import '../contacts/widgets/contact_modal.dart';
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService();
final TextEditingController _searchController = TextEditingController();
late SearchController _searchBarController;
String _rawSearchInput = '';
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
// Set the TabController length to 4
_tabController = TabController(length: 4, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabIndex);
_searchBarController = SearchController();
_searchBarController.addListener(() {
if (_searchController.text != _searchBarController.text) {
_rawSearchInput = _searchBarController.text;
_searchController.text = _rawSearchInput;
_onSearchChanged(_searchBarController.text);
}
});
_fetchContacts();
}
void _fetchContacts() async {
_allContacts = await _contactService.fetchContacts();
_contactSuggestions = List.from(_allContacts);
if (mounted) setState(() {});
setState(() {});
}
void _clearSearch() {
_searchController.clear();
_searchBarController.clear();
_rawSearchInput = '';
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
_contactSuggestions = List.from(_allContacts); // Reset suggestions
} else {
final normalizedQuery = _normalizeString(query.toLowerCase());
_contactSuggestions = _allContacts.where((contact) {
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
return normalizedName.contains(normalizedQuery);
return contact.displayName
.toLowerCase()
.contains(query.toLowerCase());
}).toList();
}
});
}
String _normalizeString(String input) {
const accentMap = {
'àáâãäå': 'a',
'èéêë': 'e',
'ìíîï': 'i',
'òóôõö': 'o',
'ùúûü': 'u',
'ç': 'c',
'ñ': 'n',
};
String normalized = input;
accentMap.forEach((accents, base) {
for (var accent in accents.split('')) {
normalized = normalized.replaceAll(accent, base);
}
});
return normalized;
}
@override
void dispose() {
_searchController.dispose();
_searchBarController.dispose();
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
@ -98,18 +69,19 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(
contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true,
);
Contact? fullContact = await FlutterContacts.getContact(contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
_fetchContacts();
setState(() {
// Updating the contact list after toggling the favorite
_fetchContacts();
});
}
} else {
print("Could not fetch contact details");
@ -128,6 +100,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
backgroundColor: Colors.black,
body: Column(
children: [
// Persistent Search Bar
Padding(
padding: const EdgeInsets.only(
top: 24.0,
@ -145,33 +118,35 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
border: Border.all(color: Colors.grey.shade800, width: 1),
),
child: SearchAnchor(
searchController: _searchBarController,
builder: (BuildContext context, SearchController controller) {
builder:
(BuildContext context, SearchController controller) {
return GestureDetector(
onTap: () {
controller.openView();
controller.openView(); // Open the search view
},
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: Colors.grey.shade800, width: 1),
border: Border.all(
color: Colors.grey.shade800, width: 1),
),
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.search, color: Colors.grey, size: 24.0),
const Icon(Icons.search,
color: Colors.grey, size: 24.0),
const SizedBox(width: 8.0),
Expanded(
child: Text(
_rawSearchInput.isEmpty
? 'Search contacts'
: _rawSearchInput,
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
overflow: TextOverflow.ellipsis,
),
Text(
_searchController.text.isEmpty
? 'Search contacts'
: _searchController.text,
style: const TextStyle(
color: Colors.grey, fontSize: 16.0),
),
if (_rawSearchInput.isNotEmpty)
const Spacer(),
if (_searchController.text.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
@ -186,24 +161,23 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
);
},
viewOnChanged: (query) {
if (_searchBarController.text != query) {
_rawSearchInput = query;
_searchBarController.text = query;
_searchController.text = query;
}
_onSearchChanged(query);
_onSearchChanged(query); // Update immediately
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
suggestionsBuilder:
(BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white),
),
title: Text(_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white)),
onTap: () {
// Clear the search text input
controller.text = '';
// Close the search view
controller.closeView(contact.displayName);
// Show the ContactModal when a contact is tapped
showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -212,28 +186,34 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
return ContactModal(
contact: contact,
onEdit: () async {
if (await FlutterContacts.requestPermission()) {
final updatedContact = await FlutterContacts
.openExternalEdit(contact.id);
if (await FlutterContacts
.requestPermission()) {
final updatedContact =
await FlutterContacts
.openExternalEdit(contact.id);
if (updatedContact != null) {
_fetchContacts();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'${contact.displayName} updated successfully!'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text('Edit canceled or failed.'),
content: Text(
'Edit canceled or failed.'),
),
);
}
}
},
onToggleFavorite: () => _toggleFavorite(contact),
onToggleFavorite: () =>
_toggleFavorite(contact),
isFavorite: contact.isStarred,
);
},
@ -245,6 +225,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
),
),
),
// 3-dot menu
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
@ -257,7 +238,8 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
MaterialPageRoute(
builder: (context) => const SettingsPage()),
);
}
},
@ -265,6 +247,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
],
),
),
// Main content with TabBarView
Expanded(
child: Stack(
children: [
@ -339,4 +322,4 @@ class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
}

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,154 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../features/call/call_page.dart';
import '../features/call/incoming_call_page.dart'; // Import the new page
// 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;
print('CallService: Received method ${call.method} with args ${call.arguments}');
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;
}
});
}
void _navigateToCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
print('CallService: CallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to CallPage');
Navigator.pushReplacement(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _navigateToIncomingCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
print('CallService: IncomingCallPage already visible, skipping navigation');
return;
}
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;
});
_isCallPageVisible = true;
}
void _closeCallPage(BuildContext context) {
if (!_isCallPageVisible) {
print('CallService: CallPage not visible, skipping pop');
return;
}
if (Navigator.canPop(context)) {
print('CallService: Popping CallPage');
Navigator.pop(context);
_isCallPageVisible = false;
}
}
Future<void> 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');
if (result["status"] != "calling") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")),
);
}
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<void> 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');
if (result["status"] != "ended") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")),
);
}
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;
}
}
}
}

View File

@ -1,19 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
// Service to manage contact-related operations
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
final hasPermission = await Permission.contacts.status;
if (!hasPermission.isGranted) {
return [];
}
try {
return await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
withAccounts: true,
withGroups: true,
withPhoto: true);
} catch (e) {
debugPrint('Error fetching contacts: $e');
return [];
}
return [];
}
Future<List<Contact>> fetchFavoriteContacts() async {