Compare commits

...

5 Commits

Author SHA1 Message Date
f491fb61e0 fix: no screen stacking when making calls
All checks were successful
/ mirror (push) Successful in 4s
2025-04-17 15:55:49 +03:00
acbccaac74 feat: give parameters to callPage to avoid fetching when possible 2025-04-17 15:55:49 +03:00
20d8c9643f feat: fetch contact in makeGsmCall to show it in call 2025-04-17 15:55:49 +03:00
22941f78d0 Argiliser exemples (#53)
All checks were successful
/ mirror (push) Successful in 4s
Reviewed-on: #53
2025-04-15 12:54:41 +00:00
b9dd156eca fix: search bar is non case sensitive and don't have delay (contact page) (#50)
All checks were successful
/ mirror (push) Successful in 8s
/ build (push) Successful in 14m22s
/ build-stealth (push) Successful in 6m28s
Co-authored-by: stcb <21@stcb.cc>
Reviewed-on: #50
Co-authored-by: Florian Griffon <florian.griffon@epitech.eu>
Co-committed-by: Florian Griffon <florian.griffon@epitech.eu>
2025-04-09 10:43:31 +00:00
5 changed files with 193 additions and 104 deletions

View File

@ -267,8 +267,12 @@ class _ContactModalState extends State<ContactModal> {
),
onTap: () async {
if (widget.contact.phones.isNotEmpty) {
await _callService.makeGsmCall(context,
phoneNumber: phoneNumber);
await _callService.makeGsmCall(
context,
phoneNumber: phoneNumber,
displayName: widget.contact.displayName,
thumbnail: widget.contact.thumbnail,
);
}
},
),

View File

@ -425,7 +425,12 @@ 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);
await _callService.makeGsmCall(
context,
phoneNumber: contact.phones.first.number,
displayName: contact.displayName,
thumbnail: contact.thumbnail,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(

View File

@ -10,53 +10,82 @@ 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();
// Set the TabController length to 4
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
_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();
setState(() {});
_contactSuggestions = List.from(_allContacts);
if (mounted) setState(() {});
}
void _clearSearch() {
_searchController.clear();
_searchBarController.clear();
_rawSearchInput = '';
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts); // Reset suggestions
_contactSuggestions = List.from(_allContacts);
} else {
final normalizedQuery = _normalizeString(query.toLowerCase());
_contactSuggestions = _allContacts.where((contact) {
return contact.displayName
.toLowerCase()
.contains(query.toLowerCase());
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
return normalizedName.contains(normalizedQuery);
}).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();
@ -69,19 +98,18 @@ class _MyHomePageState extends State<MyHomePage>
void _toggleFavorite(Contact contact) async {
try {
if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id,
Contact? fullContact = await FlutterContacts.getContact(
contact.id,
withProperties: true,
withAccounts: true,
withPhoto: true,
withThumbnail: true);
withThumbnail: true,
);
if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact);
setState(() {
// Updating the contact list after toggling the favorite
_fetchContacts();
});
}
} else {
print("Could not fetch contact details");
@ -100,7 +128,6 @@ class _MyHomePageState extends State<MyHomePage>
backgroundColor: Colors.black,
body: Column(
children: [
// Persistent Search Bar
Padding(
padding: const EdgeInsets.only(
top: 24.0,
@ -118,35 +145,33 @@ class _MyHomePageState extends State<MyHomePage>
border: Border.all(color: Colors.grey.shade800, width: 1),
),
child: SearchAnchor(
builder:
(BuildContext context, SearchController controller) {
searchController: _searchBarController,
builder: (BuildContext context, SearchController controller) {
return GestureDetector(
onTap: () {
controller.openView(); // Open the search view
controller.openView();
},
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),
Text(
_searchController.text.isEmpty
Expanded(
child: Text(
_rawSearchInput.isEmpty
? 'Search contacts'
: _searchController.text,
style: const TextStyle(
color: Colors.grey, fontSize: 16.0),
: _rawSearchInput,
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
overflow: TextOverflow.ellipsis,
),
const Spacer(),
if (_searchController.text.isNotEmpty)
),
if (_rawSearchInput.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
@ -161,23 +186,24 @@ class _MyHomePageState extends State<MyHomePage>
);
},
viewOnChanged: (query) {
_onSearchChanged(query); // Update immediately
if (_searchBarController.text != query) {
_rawSearchInput = query;
_searchBarController.text = query;
_searchController.text = query;
}
_onSearchChanged(query);
},
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,
@ -186,34 +212,28 @@ class _MyHomePageState extends State<MyHomePage>
return ContactModal(
contact: contact,
onEdit: () async {
if (await FlutterContacts
.requestPermission()) {
final updatedContact =
await FlutterContacts
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,
);
},
@ -225,7 +245,6 @@ class _MyHomePageState extends State<MyHomePage>
),
),
),
// 3-dot menu
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
@ -238,8 +257,7 @@ class _MyHomePageState extends State<MyHomePage>
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()),
MaterialPageRoute(builder: (context) => const SettingsPage()),
);
}
},
@ -247,7 +265,6 @@ class _MyHomePageState extends State<MyHomePage>
],
),
),
// Main content with TabBarView
Expanded(
child: Stack(
children: [

View File

@ -1,12 +1,17 @@
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
import '../features/call/incoming_call_page.dart';
import '../services/contact_service.dart';
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber;
static String? currentDisplayName;
static Uint8List? currentThumbnail;
static bool _isCallPageVisible = false;
static String? _currentCallState;
final ContactService _contactService = ContactService();
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -24,16 +29,61 @@ class CallService {
final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
await _fetchContactInfo(currentPhoneNumber!);
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
_handleCallState(context, state);
break;
case "callStateChanged":
final state = call.arguments["state"] as String;
print('CallService: State changed to $state');
_handleCallState(context, state);
break;
case "callEnded":
case "callRemoved":
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
currentDisplayName = null;
currentThumbnail = null;
_currentCallState = null;
break;
}
});
}
Future<void> _fetchContactInfo(String phoneNumber) async {
try {
final contacts = await _contactService.fetchContacts();
final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber);
for (var contact in contacts) {
for (var phone in contact.phones) {
if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) {
currentDisplayName = contact.displayName;
currentThumbnail = contact.thumbnail;
return;
}
}
}
currentDisplayName = phoneNumber;
currentThumbnail = null;
} catch (e) {
print('CallService: Error fetching contact info: $e');
currentDisplayName = phoneNumber;
currentThumbnail = null;
}
}
String _normalizePhoneNumber(String number) {
return number.replaceAll(RegExp(r'[\s\-\(\)]'), '');
}
void _handleCallState(BuildContext context, String state) {
if (_currentCallState == state) {
print('CallService: State $state already handled, skipping');
return;
}
_currentCallState = state;
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
@ -41,70 +91,76 @@ class CallService {
} 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') {
final currentRoute = ModalRoute.of(context)?.settings.name;
print('CallService: Navigating to CallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute');
if (_isCallPageVisible && currentRoute == '/call') {
print('CallService: CallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to CallPage');
if (_isCallPageVisible && currentRoute == '/incoming_call') {
print('CallService: Replacing IncomingCallPage with CallPage');
Navigator.pop(context);
}
Navigator.pushReplacement(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
displayName: currentDisplayName ?? currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
thumbnail: currentThumbnail,
),
),
).then((_) {
print('CallService: CallPage popped');
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _navigateToIncomingCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
final currentRoute = ModalRoute.of(context)?.settings.name;
print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute');
if (_isCallPageVisible && currentRoute == '/incoming_call') {
print('CallService: IncomingCallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to IncomingCallPage');
if (_isCallPageVisible && currentRoute == '/call') {
print('CallService: CallPage visible, not showing IncomingCallPage');
return;
}
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!,
displayName: currentDisplayName ?? currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
thumbnail: currentThumbnail,
),
),
).then((_) {
print('CallService: IncomingCallPage popped');
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _closeCallPage(BuildContext context) {
print('CallService: Attempting to close call page. Visible: $_isCallPageVisible');
if (!_isCallPageVisible) {
print('CallService: CallPage not visible, skipping pop');
return;
}
if (Navigator.canPop(context)) {
print('CallService: Popping CallPage');
print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}');
Navigator.pop(context);
_isCallPageVisible = false;
} else {
print('CallService: Cannot pop, no routes to pop');
}
}
@ -116,6 +172,11 @@ class CallService {
}) async {
try {
currentPhoneNumber = phoneNumber;
currentDisplayName = displayName ?? phoneNumber;
currentThumbnail = thumbnail;
if (displayName == null || thumbnail == null) {
await _fetchContactInfo(phoneNumber);
}
print('CallService: Making GSM call to $phoneNumber');
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
print('CallService: makeGsmCall result: $result');
@ -123,7 +184,9 @@ class CallService {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")),
);
return;
}
_handleCallState(context, "dialing");
} catch (e) {
print("CallService: Error making call: $e");
ScaffoldMessenger.of(context).showSnackBar(
@ -142,6 +205,8 @@ class CallService {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")),
);
} else {
_closeCallPage(context);
}
} catch (e) {
print("CallService: Error hanging up call: $e");

View File

@ -27,14 +27,13 @@ The protocol definition will include as completed:
- Handshakes
- Real-time data-stream encryption (and decryption)
- Encrypted stream compression
- Transmission over audio stream
- Minimal error correction in audio-based transmission
- Error handling and user prevention
- Transmission over audio stream (at least one modulation type)
- First steps in FEC (Forward Error Correction): detecting half of transmission errors
And should include prototype or scratches functionalities, among which:
- Embedded silent data transmission (silently transmit light data during an encrypted phone call)
- Embedded silent data transmission (such as DTMF)
- On-the-fly key exchange (does not require prior key exchange, sacrifying some security)
- Strong error correction
- Stronger FEC: detecting >80%, correcting 20% of transmission errors
#### The Icing dialer (based on Icing kotlin library, an Icing protocol implementation)
@ -128,16 +127,15 @@ The remote bank advisor asks him to authenticate, making him type his password o
By using the Icing protocol, not only would Jeff and the bank be assured that the informations are transmitted safely,
but also that the call is coming from Jeff's phone and not an impersonator.
Elise is a 42 years-old extreme reporter.
After interviewing Russians opposition's leader, the FSB is looking to interview her.
She tries to stay discreet and hidden, but those measures constrains her to barely receive cellular network.
She suspects her phone line to be monitored, so the best she can do to call safely, is to use her Icing dialer.
Elise, 42 years-old, is a journalist covering sensitive topics.
Her work draws attention from people who want to know what she's saying - and to whom.
Forced to stay discreet, with unreliable signal and a likely monitored phone line,
she uses Icing dialer to make secure calls without exposing herself.
Paul, a 22 years-old developer working for a big company, decides to go to China for vacations.
Paul, a 22 years-old developer, is enjoying its vacations abroad.
But everything goes wrong! The company's product he works on, is failling in the middle of the day and no one is
qualified to fix it. Paul doesn't have WiFi and his phone plan only covers voice calls in China.
With Icing dialer, he can call his collegues and help fix the
problem, safe from potential Chinese spies.
qualified to fix it. Paul doesn't have WiFi and his phone plan only covers voice calls in his country.
With Icing dialer, he can call his collegues and help fix the problem, completely safe.
## Evaluation Criteria
### Protocol and lib