Compare commits
2 Commits
6190991780
...
e6e05f2686
Author | SHA1 | Date | |
---|---|---|---|
e6e05f2686 | |||
ac004fd332 |
@ -10,53 +10,82 @@ import '../../services/contact_service.dart';
|
|||||||
import 'package:dialer/features/voicemail/voicemail_page.dart';
|
import 'package:dialer/features/voicemail/voicemail_page.dart';
|
||||||
import '../contacts/widgets/contact_modal.dart';
|
import '../contacts/widgets/contact_modal.dart';
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
|
||||||
class _MyHomePageState extends State<MyHomePage>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
List<Contact> _contactSuggestions = [];
|
List<Contact> _contactSuggestions = [];
|
||||||
final ContactService _contactService = ContactService();
|
final ContactService _contactService = ContactService();
|
||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
late SearchController _searchBarController;
|
||||||
|
String _rawSearchInput = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Set the TabController length to 4
|
|
||||||
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
|
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
|
||||||
_tabController.addListener(_handleTabIndex);
|
_tabController.addListener(_handleTabIndex);
|
||||||
|
_searchBarController = SearchController();
|
||||||
|
_searchBarController.addListener(() {
|
||||||
|
if (_searchController.text != _searchBarController.text) {
|
||||||
|
_rawSearchInput = _searchBarController.text;
|
||||||
|
_searchController.text = _rawSearchInput;
|
||||||
|
_onSearchChanged(_searchBarController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchContacts() async {
|
void _fetchContacts() async {
|
||||||
_allContacts = await _contactService.fetchContacts();
|
_allContacts = await _contactService.fetchContacts();
|
||||||
setState(() {});
|
_contactSuggestions = List.from(_allContacts);
|
||||||
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSearch() {
|
void _clearSearch() {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
|
_searchBarController.clear();
|
||||||
|
_rawSearchInput = '';
|
||||||
_onSearchChanged('');
|
_onSearchChanged('');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchChanged(String query) {
|
void _onSearchChanged(String query) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
_contactSuggestions = List.from(_allContacts); // Reset suggestions
|
_contactSuggestions = List.from(_allContacts);
|
||||||
} else {
|
} else {
|
||||||
|
final normalizedQuery = _normalizeString(query.toLowerCase());
|
||||||
_contactSuggestions = _allContacts.where((contact) {
|
_contactSuggestions = _allContacts.where((contact) {
|
||||||
return contact.displayName
|
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
|
||||||
.toLowerCase()
|
return normalizedName.contains(normalizedQuery);
|
||||||
.contains(query.toLowerCase());
|
|
||||||
}).toList();
|
}).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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_searchBarController.dispose();
|
||||||
_tabController.removeListener(_handleTabIndex);
|
_tabController.removeListener(_handleTabIndex);
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -69,19 +98,18 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
void _toggleFavorite(Contact contact) async {
|
void _toggleFavorite(Contact contact) async {
|
||||||
try {
|
try {
|
||||||
if (await FlutterContacts.requestPermission()) {
|
if (await FlutterContacts.requestPermission()) {
|
||||||
Contact? fullContact = await FlutterContacts.getContact(contact.id,
|
Contact? fullContact = await FlutterContacts.getContact(
|
||||||
|
contact.id,
|
||||||
withProperties: true,
|
withProperties: true,
|
||||||
withAccounts: true,
|
withAccounts: true,
|
||||||
withPhoto: true,
|
withPhoto: true,
|
||||||
withThumbnail: true);
|
withThumbnail: true,
|
||||||
|
);
|
||||||
|
|
||||||
if (fullContact != null) {
|
if (fullContact != null) {
|
||||||
fullContact.isStarred = !fullContact.isStarred;
|
fullContact.isStarred = !fullContact.isStarred;
|
||||||
await FlutterContacts.updateContact(fullContact);
|
await FlutterContacts.updateContact(fullContact);
|
||||||
setState(() {
|
|
||||||
// Updating the contact list after toggling the favorite
|
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print("Could not fetch contact details");
|
print("Could not fetch contact details");
|
||||||
@ -100,7 +128,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Persistent Search Bar
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 24.0,
|
top: 24.0,
|
||||||
@ -118,35 +145,33 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
border: Border.all(color: Colors.grey.shade800, width: 1),
|
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||||
),
|
),
|
||||||
child: SearchAnchor(
|
child: SearchAnchor(
|
||||||
builder:
|
searchController: _searchBarController,
|
||||||
(BuildContext context, SearchController controller) {
|
builder: (BuildContext context, SearchController controller) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.openView(); // Open the search view
|
controller.openView();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color.fromARGB(255, 30, 30, 30),
|
color: const Color.fromARGB(255, 30, 30, 30),
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||||
color: Colors.grey.shade800, width: 1),
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||||
vertical: 12.0, horizontal: 16.0),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.search,
|
const Icon(Icons.search, color: Colors.grey, size: 24.0),
|
||||||
color: Colors.grey, size: 24.0),
|
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
Text(
|
Expanded(
|
||||||
_searchController.text.isEmpty
|
child: Text(
|
||||||
|
_rawSearchInput.isEmpty
|
||||||
? 'Search contacts'
|
? 'Search contacts'
|
||||||
: _searchController.text,
|
: _rawSearchInput,
|
||||||
style: const TextStyle(
|
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
|
||||||
color: Colors.grey, fontSize: 16.0),
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
),
|
||||||
if (_searchController.text.isNotEmpty)
|
if (_rawSearchInput.isNotEmpty)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _clearSearch,
|
onTap: _clearSearch,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@ -161,23 +186,24 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
viewOnChanged: (query) {
|
viewOnChanged: (query) {
|
||||||
_onSearchChanged(query); // Update immediately
|
|
||||||
|
if (_searchBarController.text != query) {
|
||||||
|
_rawSearchInput = query;
|
||||||
|
_searchBarController.text = query;
|
||||||
|
_searchController.text = query;
|
||||||
|
}
|
||||||
|
_onSearchChanged(query);
|
||||||
},
|
},
|
||||||
suggestionsBuilder:
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
(BuildContext context, SearchController controller) {
|
|
||||||
return _contactSuggestions.map((contact) {
|
return _contactSuggestions.map((contact) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
key: ValueKey(contact.id),
|
key: ValueKey(contact.id),
|
||||||
title: Text(_obfuscateService.obfuscateData(contact.displayName),
|
title: Text(
|
||||||
style: const TextStyle(color: Colors.white)),
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Clear the search text input
|
|
||||||
controller.text = '';
|
|
||||||
|
|
||||||
// Close the search view
|
|
||||||
controller.closeView(contact.displayName);
|
controller.closeView(contact.displayName);
|
||||||
|
|
||||||
// Show the ContactModal when a contact is tapped
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@ -186,34 +212,28 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
return ContactModal(
|
return ContactModal(
|
||||||
contact: contact,
|
contact: contact,
|
||||||
onEdit: () async {
|
onEdit: () async {
|
||||||
if (await FlutterContacts
|
if (await FlutterContacts.requestPermission()) {
|
||||||
.requestPermission()) {
|
final updatedContact = await FlutterContacts
|
||||||
final updatedContact =
|
|
||||||
await FlutterContacts
|
|
||||||
.openExternalEdit(contact.id);
|
.openExternalEdit(contact.id);
|
||||||
if (updatedContact != null) {
|
if (updatedContact != null) {
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'${contact.displayName} updated successfully!'),
|
'${contact.displayName} updated successfully!'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text('Edit canceled or failed.'),
|
||||||
'Edit canceled or failed.'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onToggleFavorite: () =>
|
onToggleFavorite: () => _toggleFavorite(contact),
|
||||||
_toggleFavorite(contact),
|
|
||||||
isFavorite: contact.isStarred,
|
isFavorite: contact.isStarred,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -225,7 +245,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 3-dot menu
|
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) => [
|
||||||
@ -238,8 +257,7 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
if (value == 'settings') {
|
if (value == 'settings') {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||||
builder: (context) => const SettingsPage()),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -247,7 +265,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Main content with TabBarView
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -27,14 +27,13 @@ The protocol definition will include as completed:
|
|||||||
- Handshakes
|
- Handshakes
|
||||||
- Real-time data-stream encryption (and decryption)
|
- Real-time data-stream encryption (and decryption)
|
||||||
- Encrypted stream compression
|
- Encrypted stream compression
|
||||||
- Transmission over audio stream
|
- Transmission over audio stream (at least one modulation type)
|
||||||
- Minimal error correction in audio-based transmission
|
- First steps in FEC (Forward Error Correction): detecting half of transmission errors
|
||||||
- Error handling and user prevention
|
|
||||||
|
|
||||||
And should include prototype or scratches functionalities, among which:
|
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)
|
- 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)
|
#### 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,
|
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.
|
but also that the call is coming from Jeff's phone and not an impersonator.
|
||||||
|
|
||||||
Elise is a 42 years-old extreme reporter.
|
Elise, 42 years-old, is a journalist covering sensitive topics.
|
||||||
After interviewing Russians opposition's leader, the FSB is looking to interview her.
|
Her work draws attention from people who want to know what she's saying - and to whom.
|
||||||
She tries to stay discreet and hidden, but those measures constrains her to barely receive cellular network.
|
Forced to stay discreet, with unreliable signal and a likely monitored phone line,
|
||||||
She suspects her phone line to be monitored, so the best she can do to call safely, is to use her Icing dialer.
|
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
|
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.
|
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
|
With Icing dialer, he can call his collegues and help fix the problem, completely safe.
|
||||||
problem, safe from potential Chinese spies.
|
|
||||||
|
|
||||||
## Evaluation Criteria
|
## Evaluation Criteria
|
||||||
### Protocol and lib
|
### Protocol and lib
|
||||||
|
Loading…
Reference in New Issue
Block a user