Compare commits

...

4 Commits

Author SHA1 Message Date
9ba714d25c fix: dublicate dtmf
Some checks failed
/ mirror (push) Waiting to run
/ build-stealth (push) Has been cancelled
/ build (push) Has been cancelled
2025-05-14 12:54:06 +03:00
7ac8b3ca8f feat: DTMF dialpad
Some checks failed
/ build-stealth (push) Waiting to run
/ mirror (push) Waiting to run
/ build (push) Has been cancelled
2025-05-14 12:47:19 +03:00
fa3e13ae33 feat: default dialer prompt screen (#56)
Reviewed-on: #56
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-05-14 12:47:19 +03:00
bd3ca98883 feat: DTMF dialpad
All checks were successful
/ mirror (push) Successful in 4s
/ build-stealth (push) Successful in 10m2s
/ build (push) Successful in 10m5s
2025-05-07 23:35:47 +03:00
5 changed files with 498 additions and 287 deletions

View File

@ -95,7 +95,6 @@ class MainActivity : FlutterActivity() {
pendingIncomingCall = null pendingIncomingCall = null
} }
} }
checkAndRequestDefaultDialer()
result.success(true) result.success(true)
} }
"makeGsmCall" -> { "makeGsmCall" -> {
@ -191,6 +190,24 @@ class MainActivity : FlutterActivity() {
result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null) result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null)
} }
} }
"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)
}
}
"isDefaultDialer" -> {
val isDefault = isDefaultDialer()
Log.d(TAG, "isDefaultDialer called, returning: $isDefault")
result.success(isDefault)
}
"requestDefaultDialer" -> {
checkAndRequestDefaultDialer()
result.success(true)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -216,6 +233,13 @@ class MainActivity : FlutterActivity() {
} }
} }
private fun isDefaultDialer(): Boolean {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage
Log.d(TAG, "Checking default dialer: current=$currentDefault, myPackage=$packageName")
return currentDefault == packageName
}
private fun checkAndRequestDefaultDialer() { private fun checkAndRequestDefaultDialer() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val currentDefault = telecomManager.defaultDialerPackage val currentDefault = telecomManager.defaultDialerPackage

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> {
), ),
), ),
), ),
)); ),
);
} }
} }

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DefaultDialerPromptScreen extends StatelessWidget {
const DefaultDialerPromptScreen({super.key});
Future<void> _requestDefaultDialer(BuildContext context) async {
const channel = MethodChannel('call_service');
try {
await channel.invokeMethod('requestDefaultDialer');
// Navigate to home page after requesting default dialer
Navigator.of(context).pushReplacementNamed('/home');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error requesting default dialer: $e')),
);
}
}
void _exploreApp(BuildContext context) {
// Navigate to home page without requesting default dialer
Navigator.of(context).pushReplacementNamed('/home');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Set as Default Dialer',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'To handle calls effectively, Icing needs to be your default dialer app. This allows Icing to manage incoming and outgoing calls seamlessly.\n\nWithout the permission, Icing will not be able to encrypt calls.',
style: TextStyle(
fontSize: 16,
color: Colors.white70,
),
textAlign: TextAlign.center,
),
],
),
),
Row(
children: [
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ElevatedButton(
onPressed: () => _exploreApp(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[800],
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
child: Text('Explore App first'),
),
),
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ElevatedButton(
onPressed: () => _requestDefaultDialer(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
child: Text('Set as Default Dialer'),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:dialer/features/home/home_page.dart'; import 'package:dialer/features/home/home_page.dart';
import 'package:dialer/features/home/default_dialer_prompt.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/services/call_service.dart'; import 'package:dialer/services/call_service.dart';
@ -51,6 +52,17 @@ Future<void> _requestPermissions() async {
class Dialer extends StatelessWidget { class Dialer extends StatelessWidget {
const Dialer({super.key}); const Dialer({super.key});
Future<bool> _isDefaultDialer() async {
const channel = MethodChannel('call_service');
try {
final isDefault = await channel.invokeMethod<bool>('isDefaultDialer');
return isDefault ?? false;
} catch (e) {
print('Error checking default dialer: $e');
return false;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContactState( return ContactState(
@ -59,7 +71,24 @@ class Dialer extends StatelessWidget {
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
), ),
home: SafeArea(child: MyHomePage()), initialRoute: '/',
routes: {
'/': (context) => FutureBuilder<bool>(
future: _isDefaultDialer(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data == false) {
return DefaultDialerPromptScreen();
}
return SafeArea(child: MyHomePage());
},
),
'/home': (context) => SafeArea(child: MyHomePage()),
},
), ),
); );
} }