feat: DTMF dialpad #55

Merged
stcb merged 4 commits from DTMF into dev 2025-05-14 09:58:50 +00:00
3 changed files with 350 additions and 285 deletions
Showing only changes of commit 5c8373f575 - Show all commits

View File

@ -199,6 +199,15 @@ class MainActivity : FlutterActivity() {
checkAndRequestDefaultDialer() checkAndRequestDefaultDialer()
result.success(true) result.success(true)
} }
"sendDtmfTone" -> {
val digit = call.argument<String>("digit")
if (digit != null) {
val success = MyInCallService.sendDtmfTone(digit)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "Digit is null", null)
}
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }

View File

@ -52,6 +52,22 @@ class MyInCallService : InCallService() {
} }
} ?: false } ?: false
} }
fun sendDtmfTone(digit: String): Boolean {
return instance?.let { service ->
try {
currentCall?.let { call ->
call.playDtmfTone(digit[0])
call.stopDtmfTone()
Log.d(TAG, "Sent DTMF tone: $digit")
true
} ?: false
} catch (e: Exception) {
Log.e(TAG, "Failed to send DTMF tone: $e")
false
}
} ?: false
}
} }
private val callCallback = object : Call.Callback() { private val callCallback = object : Call.Callback() {

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/services/call_service.dart'; import 'package:dialer/services/call_service.dart';
import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/services/obfuscate_service.dart';
import 'package:dialer/widgets/username_color_generator.dart'; import 'package:dialer/widgets/username_color_generator.dart';
import 'package:flutter/services.dart';
class CallPage extends StatefulWidget { class CallPage extends StatefulWidget {
final String displayName; final String displayName;
@ -124,10 +124,32 @@ class _CallPageState extends State<CallPage> {
}); });
} }
void _addDigit(String digit) { void _addDigit(String digit) async {
print('CallPage: Tapped digit: $digit');
setState(() { setState(() {
_typedDigits += digit; _typedDigits += digit;
}); });
// Send DTMF tone
const channel = MethodChannel('call_service');
try {
final success =
await channel.invokeMethod<bool>('sendDtmfTone', {'digit': digit});
if (success != true) {
print('CallPage: Failed to send DTMF tone for $digit');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send DTMF tone')),
);
}
}
} catch (e) {
print('CallPage: Error sending DTMF tone: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error sending DTMF tone: $e')),
);
}
}
} }
void _toggleMute() async { void _toggleMute() async {
@ -195,7 +217,7 @@ class _CallPageState extends State<CallPage> {
print('CallPage: Error hanging up: $e'); print('CallPage: Error hanging up: $e');
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error hanging up: $e")), SnackBar(content: Text('Error hanging up: $e')),
); );
} }
} }
@ -227,8 +249,14 @@ class _CallPageState extends State<CallPage> {
print( print(
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
return PopScope( return PopScope(
canPop: canPop: _callStatus == "Call Ended",
_callStatus == "Call Ended", // Allow navigation only if call ended onPopInvoked: (didPop) {
if (!didPop) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cannot leave during an active call')),
);
}
},
child: Scaffold( child: Scaffold(
body: Container( body: Container(
color: Colors.black, color: Colors.black,
@ -254,8 +282,7 @@ class _CallPageState extends State<CallPage> {
children: [ children: [
Icon( Icon(
icingProtocolOk ? Icons.lock : Icons.lock_open, icingProtocolOk ? Icons.lock : Icons.lock_open,
color: color: icingProtocolOk ? Colors.green : Colors.red,
icingProtocolOk ? Colors.green : Colors.red,
size: 16, size: 16,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@ -282,12 +309,16 @@ class _CallPageState extends State<CallPage> {
Text( Text(
widget.phoneNumber, widget.phoneNumber,
style: TextStyle( style: TextStyle(
fontSize: statusFontSize, color: Colors.white70), fontSize: statusFontSize,
color: Colors.white70,
),
), ),
Text( Text(
_callStatus, _callStatus,
style: TextStyle( style: TextStyle(
fontSize: statusFontSize, color: Colors.white70), fontSize: statusFontSize,
color: Colors.white70,
),
), ),
], ],
), ),
@ -298,8 +329,7 @@ class _CallPageState extends State<CallPage> {
if (isKeypadVisible) ...[ if (isKeypadVisible) ...[
const Spacer(flex: 2), const Spacer(flex: 2),
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 20.0),
const EdgeInsets.symmetric(horizontal: 20.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -319,20 +349,23 @@ class _CallPageState extends State<CallPage> {
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: _toggleKeypad, onPressed: _toggleKeypad,
icon: const Icon(Icons.close, icon: const Icon(
color: Colors.white), Icons.close,
color: Colors.white,
),
), ),
], ],
), ),
), ),
Container( Container(
height: MediaQuery.of(context).size.height * 0.35, height: MediaQuery.of(context).size.height * 0.4,
margin: const EdgeInsets.symmetric(horizontal: 20), margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(8),
child: GridView.count( child: GridView.count(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3, crossAxisCount: 3,
childAspectRatio: 1.3, childAspectRatio: 1.5,
mainAxisSpacing: 8, mainAxisSpacing: 8,
crossAxisSpacing: 8, crossAxisSpacing: 8,
children: List.generate(12, (index) { children: List.generate(12, (index) {
@ -357,7 +390,9 @@ class _CallPageState extends State<CallPage> {
child: Text( child: Text(
label, label,
style: const TextStyle( style: const TextStyle(
fontSize: 32, color: Colors.white), fontSize: 32,
color: Colors.white,
),
), ),
), ),
), ),
@ -369,8 +404,7 @@ class _CallPageState extends State<CallPage> {
] else ...[ ] else ...[
const Spacer(), const Spacer(),
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 32.0),
const EdgeInsets.symmetric(horizontal: 32.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -395,7 +429,8 @@ class _CallPageState extends State<CallPage> {
isMuted ? 'Unmute' : 'Mute', isMuted ? 'Unmute' : 'Mute',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14,
),
), ),
], ],
), ),
@ -414,7 +449,8 @@ class _CallPageState extends State<CallPage> {
'Keypad', 'Keypad',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14,
),
), ),
], ],
), ),
@ -437,7 +473,8 @@ class _CallPageState extends State<CallPage> {
'Speaker', 'Speaker',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14,
),
), ),
], ],
), ),
@ -464,7 +501,8 @@ class _CallPageState extends State<CallPage> {
'Add Contact', 'Add Contact',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14,
),
), ),
], ],
), ),
@ -483,7 +521,8 @@ class _CallPageState extends State<CallPage> {
'Change SIM', 'Change SIM',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14,
),
), ),
], ],
), ),
@ -519,6 +558,7 @@ class _CallPageState extends State<CallPage> {
), ),
), ),
), ),
)); ),
);
} }
} }