From 5c8373f575dc6aeb1f8291d8bf0c91de2d8de588 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 7 May 2025 23:35:47 +0300 Subject: [PATCH 1/4] feat: DTMF dialpad --- .../icing/dialer/activities/MainActivity.kt | 9 + .../icing/dialer/services/MyInCallService.kt | 18 +- dialer/lib/features/call/call_page.dart | 608 ++++++++++-------- 3 files changed, 350 insertions(+), 285 deletions(-) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index fd1b904..4d51245 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -199,6 +199,15 @@ class MainActivity : FlutterActivity() { checkAndRequestDefaultDialer() result.success(true) } + "sendDtmfTone" -> { + val digit = call.argument("digit") + if (digit != null) { + val success = MyInCallService.sendDtmfTone(digit) + result.success(success) + } else { + result.error("INVALID_ARGUMENT", "Digit is null", null) + } + } else -> result.notImplemented() } } diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index 5469c6d..2acb1f3 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -52,6 +52,22 @@ class MyInCallService : InCallService() { } } ?: 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() { @@ -111,7 +127,7 @@ class MyInCallService : InCallService() { } call.registerCallback(callCallback) if (callAudioState != null) { - val audioState = callAudioState + val audioState = callAudioState channel?.invokeMethod("audioStateChanged", mapOf( "route" to audioState.route, "muted" to audioState.isMuted, diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 0d7cf48..95f48a5 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.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:flutter/services.dart'; class CallPage extends StatefulWidget { final String displayName; @@ -124,10 +124,32 @@ class _CallPageState extends State { }); } - void _addDigit(String digit) { + void _addDigit(String digit) async { + print('CallPage: Tapped digit: $digit'); setState(() { _typedDigits += digit; }); + // Send DTMF tone + const channel = MethodChannel('call_service'); + try { + final success = + await channel.invokeMethod('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 { @@ -195,7 +217,7 @@ class _CallPageState extends State { print('CallPage: Error hanging up: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error hanging up: $e")), + SnackBar(content: Text('Error hanging up: $e')), ); } } @@ -227,298 +249,316 @@ class _CallPageState extends State { print( 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); return PopScope( - canPop: - _callStatus == "Call Ended", // Allow navigation only if call ended - child: 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, + canPop: _callStatus == "Call Ended", + onPopInvoked: (didPop) { + if (!didPop) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Cannot leave during an active call')), + ); + } + }, + child: 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, - 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( - _callStatus, - 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), - ), - ], + fontSize: 12, + fontWeight: FontWeight.bold, ), ), - 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: isMuted - ? Colors.amber - : 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( - isSpeaker - ? Icons.volume_up - : Icons.volume_off, - color: isSpeaker - ? 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: [ - if (isNumberUnknown) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _addContact, - 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, + ), + const SizedBox(height: 4), + Text( + _obfuscateService.obfuscateData(widget.displayName), + style: TextStyle( + fontSize: nameFontSize, color: Colors.white, - size: 32, + fontWeight: FontWeight.bold, ), ), + Text( + widget.phoneNumber, + style: TextStyle( + fontSize: statusFontSize, + color: Colors.white70, + ), + ), + Text( + _callStatus, + 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.4, + margin: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.all(8), + child: GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + childAspectRatio: 1.5, + 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: isMuted + ? Colors.amber + : 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( + isSpeaker + ? Icons.volume_up + : Icons.volume_off, + color: isSpeaker + ? 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: [ + if (isNumberUnknown) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _addContact, + 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, + ), ), ), - ], - ), + ), + ], ), ), - )); + ), + ), + ); } } -- 2.45.2 From 6b3bac08e5951c8de370175b3ae6d9c5569eb318 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 14 May 2025 09:45:09 +0000 Subject: [PATCH 2/4] feat: default dialer prompt screen (#56) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/56 Co-authored-by: Florian Griffon Co-committed-by: Florian Griffon --- .../kotlin/com/icing/dialer/activities/MainActivity.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index 4d51245..f58cdab 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -208,6 +208,15 @@ class MainActivity : FlutterActivity() { result.error("INVALID_ARGUMENT", "Digit is null", null) } } + "isDefaultDialer" -> { + val isDefault = isDefaultDialer() + Log.d(TAG, "isDefaultDialer called, returning: $isDefault") + result.success(isDefault) + } + "requestDefaultDialer" -> { + checkAndRequestDefaultDialer() + result.success(true) + } else -> result.notImplemented() } } -- 2.45.2 From bec5f5b5d2564df4c00c227dece824b2c4e44695 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 7 May 2025 23:35:47 +0300 Subject: [PATCH 3/4] feat: DTMF dialpad --- .../kotlin/com/icing/dialer/activities/MainActivity.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index f58cdab..e96696c 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -217,6 +217,15 @@ class MainActivity : FlutterActivity() { checkAndRequestDefaultDialer() result.success(true) } + "sendDtmfTone" -> { + val digit = call.argument("digit") + if (digit != null) { + val success = MyInCallService.sendDtmfTone(digit) + result.success(success) + } else { + result.error("INVALID_ARGUMENT", "Digit is null", null) + } + } else -> result.notImplemented() } } -- 2.45.2 From 8f361578089b6c5e6a0b5aa382f36dd827631c35 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 14 May 2025 12:54:06 +0300 Subject: [PATCH 4/4] fix: dublicate dtmf --- .../kotlin/com/icing/dialer/activities/MainActivity.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index e96696c..f58cdab 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -217,15 +217,6 @@ class MainActivity : FlutterActivity() { checkAndRequestDefaultDialer() result.success(true) } - "sendDtmfTone" -> { - val digit = call.argument("digit") - if (digit != null) { - val success = MyInCallService.sendDtmfTone(digit) - result.success(success) - } else { - result.error("INVALID_ARGUMENT", "Digit is null", null) - } - } else -> result.notImplemented() } } -- 2.45.2