From 664dd4bb38fec7314febc3c05f8c8a8b75698349 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:27:02 +0100 Subject: [PATCH 01/17] WIP: app rework --- dialer/lib/config/routes/app_router.dart | 0 dialer/lib/config/themes/app_theme.dart | 0 dialer/lib/core/config/app_config.dart | 13 + dialer/lib/core/navigation/app_router.dart | 57 +++ dialer/lib/core/utils/color_utils.dart | 37 ++ dialer/lib/domain/services/block_service.dart | 78 +++++ dialer/lib/domain/services/call_service.dart | 183 ++++++++++ .../lib/domain/services/contact_service.dart | 86 +++++ .../asymmetric_crypto_service.dart | 296 ++++++++++++++++ .../domain/services/obfuscate_service.dart | 36 ++ dialer/lib/domain/services/qr/qr_scanner.dart | 57 +++ dialer/lib/features/call/call_page.dart | 324 ----------------- .../lib/features/call/incoming_call_page.dart | 181 ---------- .../lib/features/contacts/contact_page.dart | 26 -- .../features/favorites/favorites_page.dart | 32 -- dialer/lib/features/home/home_page.dart | 325 ------------------ .../settings/blocked/settings_blocked.dart | 167 --------- .../settings/cryptography/key_management.dart | 145 -------- dialer/lib/globals.dart | 4 + dialer/lib/main.dart | 73 ++-- dialer/lib/main_old.dart | 84 +++++ .../presentation/common/theme/app_theme.dart | 42 +++ .../common/widgets/obfuscated_avatar.dart | 53 +++ .../presentation/features/call/call_page.dart | 251 ++++++++++++++ .../features/call/incoming_call_page.dart | 164 +++++++++ .../features/composition/composition.dart | 6 +- .../features/contacts/contact_page.dart | 212 ++++++++++++ .../features/contacts/contact_state.dart | 51 ++- .../contacts/widgets/add_contact_button.dart | 2 +- .../widgets/alphabet_scroll_page.dart | 0 .../contacts/widgets/contact_modal.dart | 287 +++++++++------- .../contacts/widgets/share_own_qr.dart | 0 .../features/dialer/composition_page.dart | 288 ++++++++++++++++ .../features/favorites/favorites_page.dart | 36 ++ .../features/history/history_page.dart | 12 +- .../presentation/features/home/home_page.dart | 224 ++++++++++++ .../settings/blocked/settings_blocked.dart | 116 +++++++ .../features/settings/call/settingsCall.dart | 0 .../features/settings/call/settings_call.dart | 92 +++++ .../settings/call/settings_call_page.dart | 32 ++ .../settings/cryptography/key_management.dart | 146 ++++++++ .../features/settings/settings.dart | 9 +- .../features/settings/settings_page.dart | 0 .../features/voicemail/voicemail_page.dart | 11 +- dialer/lib/services/block_service.dart | 96 ++++-- dialer/lib/services/call_service.dart | 4 +- dialer/lib/services/contact_service.dart | 85 +++-- dialer/lib/services/qr/qr_scanner.dart | 57 +++ dialer/lib/widgets/color_darkener.dart | 19 +- .../lib/widgets/username_color_generator.dart | 21 +- 50 files changed, 3044 insertions(+), 1476 deletions(-) create mode 100644 dialer/lib/config/routes/app_router.dart create mode 100644 dialer/lib/config/themes/app_theme.dart create mode 100644 dialer/lib/core/config/app_config.dart create mode 100644 dialer/lib/core/navigation/app_router.dart create mode 100644 dialer/lib/core/utils/color_utils.dart create mode 100644 dialer/lib/domain/services/block_service.dart create mode 100644 dialer/lib/domain/services/call_service.dart create mode 100644 dialer/lib/domain/services/contact_service.dart create mode 100644 dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart create mode 100644 dialer/lib/domain/services/obfuscate_service.dart create mode 100644 dialer/lib/domain/services/qr/qr_scanner.dart delete mode 100644 dialer/lib/features/call/call_page.dart delete mode 100644 dialer/lib/features/call/incoming_call_page.dart delete mode 100644 dialer/lib/features/contacts/contact_page.dart delete mode 100644 dialer/lib/features/favorites/favorites_page.dart delete mode 100644 dialer/lib/features/home/home_page.dart delete mode 100644 dialer/lib/features/settings/blocked/settings_blocked.dart delete mode 100644 dialer/lib/features/settings/cryptography/key_management.dart create mode 100644 dialer/lib/main_old.dart create mode 100644 dialer/lib/presentation/common/theme/app_theme.dart create mode 100644 dialer/lib/presentation/common/widgets/obfuscated_avatar.dart create mode 100644 dialer/lib/presentation/features/call/call_page.dart create mode 100644 dialer/lib/presentation/features/call/incoming_call_page.dart rename dialer/lib/{ => presentation}/features/composition/composition.dart (98%) create mode 100644 dialer/lib/presentation/features/contacts/contact_page.dart rename dialer/lib/{ => presentation}/features/contacts/contact_state.dart (75%) rename dialer/lib/{ => presentation}/features/contacts/widgets/add_contact_button.dart (97%) rename dialer/lib/{ => presentation}/features/contacts/widgets/alphabet_scroll_page.dart (100%) rename dialer/lib/{ => presentation}/features/contacts/widgets/contact_modal.dart (51%) rename dialer/lib/{ => presentation}/features/contacts/widgets/share_own_qr.dart (100%) create mode 100644 dialer/lib/presentation/features/dialer/composition_page.dart create mode 100644 dialer/lib/presentation/features/favorites/favorites_page.dart rename dialer/lib/{ => presentation}/features/history/history_page.dart (98%) create mode 100644 dialer/lib/presentation/features/home/home_page.dart create mode 100644 dialer/lib/presentation/features/settings/blocked/settings_blocked.dart rename dialer/lib/{ => presentation}/features/settings/call/settingsCall.dart (100%) create mode 100644 dialer/lib/presentation/features/settings/call/settings_call.dart create mode 100644 dialer/lib/presentation/features/settings/call/settings_call_page.dart create mode 100644 dialer/lib/presentation/features/settings/cryptography/key_management.dart rename dialer/lib/{ => presentation}/features/settings/settings.dart (86%) create mode 100644 dialer/lib/presentation/features/settings/settings_page.dart rename dialer/lib/{ => presentation}/features/voicemail/voicemail_page.dart (97%) create mode 100644 dialer/lib/services/qr/qr_scanner.dart diff --git a/dialer/lib/config/routes/app_router.dart b/dialer/lib/config/routes/app_router.dart new file mode 100644 index 0000000..e69de29 diff --git a/dialer/lib/config/themes/app_theme.dart b/dialer/lib/config/themes/app_theme.dart new file mode 100644 index 0000000..e69de29 diff --git a/dialer/lib/core/config/app_config.dart b/dialer/lib/core/config/app_config.dart new file mode 100644 index 0000000..5326cf5 --- /dev/null +++ b/dialer/lib/core/config/app_config.dart @@ -0,0 +1,13 @@ +class AppConfig { + // Private constructor to prevent instantiation + AppConfig._(); + + // Global configuration + static bool isStealthMode = false; + + // App initialization + static Future initialize() async { + const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); + isStealthMode = stealthFlag.toLowerCase() == 'true'; + } +} diff --git a/dialer/lib/core/navigation/app_router.dart b/dialer/lib/core/navigation/app_router.dart new file mode 100644 index 0000000..ec72fab --- /dev/null +++ b/dialer/lib/core/navigation/app_router.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import '../../presentation/features/call/call_page.dart'; +import '../../presentation/features/call/incoming_call_page.dart'; +import '../../presentation/features/home/home_page.dart'; +import '../../presentation/features/settings/settings.dart'; // Updated import +import '../../presentation/features/contacts/contact_page.dart'; +import '../../presentation/features/dialer/composition_page.dart'; +import 'dart:typed_data'; + +class AppRouter { + static Route generateRoute(RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => const MyHomePage()); + + case '/settings': + return MaterialPageRoute(builder: (_) => const SettingsPage()); // Now correctly imported + + case '/composition': + return MaterialPageRoute(builder: (_) => const CompositionPage()); + + case '/contacts': + return MaterialPageRoute(builder: (_) => const ContactPage()); + + case '/call': + final args = settings.arguments as Map; + return MaterialPageRoute( + settings: settings, + builder: (_) => CallPage( + displayName: args['displayName'] as String, + phoneNumber: args['phoneNumber'] as String, + thumbnail: args['thumbnail'] as Uint8List?, + ), + ); + + case '/incoming_call': + final args = settings.arguments as Map; + return MaterialPageRoute( + settings: settings, + builder: (_) => IncomingCallPage( + displayName: args['displayName'] as String, + phoneNumber: args['phoneNumber'] as String, + thumbnail: args['thumbnail'] as Uint8List?, + ), + ); + + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center( + child: Text('No route defined for ${settings.name}'), + ), + ), + ); + } + } +} diff --git a/dialer/lib/core/utils/color_utils.dart b/dialer/lib/core/utils/color_utils.dart new file mode 100644 index 0000000..6677b1f --- /dev/null +++ b/dialer/lib/core/utils/color_utils.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +/// Generates a color based on a string input (typically a name) +Color generateColorFromName(String name) { + if (name.isEmpty) return Colors.grey; + + // Use the hashCode of the name to generate a consistent color + int hash = name.hashCode; + + // Use the hash to generate RGB values + final r = (hash & 0xFF0000) >> 16; + final g = (hash & 0x00FF00) >> 8; + final b = hash & 0x0000FF; + + // Create a color with these RGB values + return Color.fromARGB(255, r, g, b); +} + +/// Darkens a color by a percentage (0.0 to 1.0) +Color darken(Color color, [double amount = 0.3]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return darkened.toColor(); +} + +/// Lightens a color by a percentage (0.0 to 1.0) +Color lighten(Color color, [double amount = 0.3]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return lightened.toColor(); +} diff --git a/dialer/lib/domain/services/block_service.dart b/dialer/lib/domain/services/block_service.dart new file mode 100644 index 0000000..5ec0cb7 --- /dev/null +++ b/dialer/lib/domain/services/block_service.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Service for managing blocked phone numbers +class BlockService { + static const String _blockedNumbersKey = 'blocked_numbers'; + + // Private constructor + BlockService._privateConstructor(); + + // Singleton instance + static final BlockService _instance = BlockService._privateConstructor(); + + // Factory constructor to return the same instance + factory BlockService() { + return _instance; + } + + /// Block a phone number + Future blockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + // Don't add if already blocked + if (blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.add(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error blocking number: $e'); + return false; + } + } + + /// Unblock a phone number + Future unblockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + if (!blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.remove(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error unblocking number: $e'); + return false; + } + } + + /// Check if a number is blocked + Future isNumberBlocked(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + return blockedNumbers.contains(phoneNumber); + } catch (e) { + debugPrint('Error checking if number is blocked: $e'); + return false; + } + } + + /// Get all blocked numbers + Future> getBlockedNumbers() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(_blockedNumbersKey) ?? []; + } catch (e) { + debugPrint('Error getting blocked numbers: $e'); + return []; + } + } +} diff --git a/dialer/lib/domain/services/call_service.dart b/dialer/lib/domain/services/call_service.dart new file mode 100644 index 0000000..b46a1d3 --- /dev/null +++ b/dialer/lib/domain/services/call_service.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:typed_data'; + +class CallService { + static const MethodChannel _channel = MethodChannel('call_service'); + static String? currentPhoneNumber; + static bool _isCallPageVisible = false; + + static final GlobalKey navigatorKey = GlobalKey(); + + // Private constructor + CallService._privateConstructor() { + _initializeMethodCallHandler(); + } + + // Singleton instance + static final CallService _instance = CallService._privateConstructor(); + + // Factory constructor to return the same instance + factory CallService() { + return _instance; + } + + void _initializeMethodCallHandler() { + _channel.setMethodCallHandler((call) async { + final context = navigatorKey.currentContext; + print('CallService: Received method ${call.method} with args ${call.arguments}'); + if (context == null) { + print('CallService: Navigator context is null, cannot navigate'); + return; + } + + switch (call.method) { + case "callAdded": + _handleCallAdded(context, call.arguments); + break; + case "callStateChanged": + _handleCallStateChanged(context, call.arguments); + break; + case "callEnded": + case "callRemoved": + _handleCallEnded(context); + break; + } + }); + } + + void _handleCallAdded(BuildContext context, dynamic arguments) { + final phoneNumber = arguments["callId"] as String; + final state = arguments["state"] as String; + currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); + print('CallService: Call added, number: $currentPhoneNumber, state: $state'); + + if (state == "ringing") { + _navigateToIncomingCallPage(context); + } else { + _navigateToCallPage(context); + } + } + + void _handleCallStateChanged(BuildContext context, dynamic arguments) { + final state = arguments["state"] as String; + print('CallService: State changed to $state'); + + if (state == "disconnected" || state == "disconnecting") { + _closeCallPage(context); + } else if (state == "active" || state == "dialing") { + _navigateToCallPage(context); + } else if (state == "ringing") { + _navigateToIncomingCallPage(context); + } + } + + void _handleCallEnded(BuildContext context) { + print('CallService: Call ended/removed'); + _closeCallPage(context); + currentPhoneNumber = null; + } + + void _navigateToCallPage(BuildContext context) { + if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { + print('CallService: CallPage already visible, skipping navigation'); + return; + } + + print('CallService: Navigating to CallPage'); + Navigator.pushReplacementNamed( + context, + '/call', + arguments: { + 'displayName': currentPhoneNumber!, + 'phoneNumber': currentPhoneNumber!, + 'thumbnail': null, + } + ).then((_) { + _isCallPageVisible = false; + }); + + _isCallPageVisible = true; + } + + void _navigateToIncomingCallPage(BuildContext context) { + if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { + print('CallService: IncomingCallPage already visible, skipping navigation'); + return; + } + + print('CallService: Navigating to IncomingCallPage'); + Navigator.pushNamed( + context, + '/incoming_call', + arguments: { + 'displayName': currentPhoneNumber!, + 'phoneNumber': currentPhoneNumber!, + 'thumbnail': null, + } + ).then((_) { + _isCallPageVisible = false; + }); + + _isCallPageVisible = true; + } + + void _closeCallPage(BuildContext context) { + if (!_isCallPageVisible) { + print('CallService: CallPage not visible, skipping pop'); + return; + } + + if (Navigator.canPop(context)) { + print('CallService: Popping CallPage'); + Navigator.pop(context); + _isCallPageVisible = false; + } + } + + Future makeGsmCall( + BuildContext context, { + required String phoneNumber, + String? displayName, + Uint8List? thumbnail, + }) async { + try { + currentPhoneNumber = phoneNumber; + print('CallService: Making GSM call to $phoneNumber'); + final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); + print('CallService: makeGsmCall result: $result'); + + if (result["status"] != "calling") { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to initiate call")), + ); + } + } catch (e) { + print("CallService: Error making call: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error making call: $e")), + ); + rethrow; + } + } + + Future hangUpCall(BuildContext context) async { + try { + print('CallService: Hanging up call'); + final result = await _channel.invokeMethod('hangUpCall'); + print('CallService: hangUpCall result: $result'); + + if (result["status"] != "ended") { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to end call")), + ); + } + } catch (e) { + print("CallService: Error hanging up call: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error hanging up call: $e")), + ); + rethrow; + } + } +} diff --git a/dialer/lib/domain/services/contact_service.dart b/dialer/lib/domain/services/contact_service.dart new file mode 100644 index 0000000..d961100 --- /dev/null +++ b/dialer/lib/domain/services/contact_service.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class ContactService { + Future> fetchContacts() async { + if (await FlutterContacts.requestPermission()) { + List contacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return contacts; + } else { + // Permission denied + return []; + } + } + + Future> fetchFavoriteContacts() async { + if (await FlutterContacts.requestPermission()) { + // Get all contacts and filter for favorites + List allContacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return allContacts.where((c) => c.isStarred).toList(); + } else { + // Permission denied + return []; + } + } + + Future addNewContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + try { + return await FlutterContacts.insertContact(contact); + } catch (e) { + debugPrint('Error adding contact: $e'); + return null; + } + } + return null; + } + + void showContactQRCodeDialog(BuildContext context, Contact contact) { + final String vCard = contact.toVCard(); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: Text( + 'QR Code for ${contact.displayName}', + style: const TextStyle(color: Colors.white), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.all(16.0), + child: QrImageView( + data: vCard, + version: QrVersions.auto, + size: 200.0, + ), + ), + const SizedBox(height: 16.0), + const Text( + 'Scan this code to add this contact', + style: TextStyle(color: Colors.white70), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + }, + ); + } +} diff --git a/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart b/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart new file mode 100644 index 0000000..d52cb91 --- /dev/null +++ b/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart @@ -0,0 +1,296 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/asymmetric/api.dart'; +import 'package:asn1lib/asn1lib.dart'; + +/// Service for handling asymmetric cryptography operations +class AsymmetricCryptoService { + static const String _privateKeyTag = 'private_key'; + static const String _publicKeyTag = 'public_key'; + + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + + AsymmetricKeyPair? _currentKeyPair; + + /// Initialize with the default key pair, creating one if it doesn't exist + Future initializeDefaultKeyPair() async { + try { + // Try to load existing keys + final privateKeyStr = await _secureStorage.read(key: _privateKeyTag); + final publicKeyStr = await _secureStorage.read(key: _publicKeyTag); + + if (privateKeyStr != null && publicKeyStr != null) { + try { + // Parse existing keys + final privateKey = _parsePrivateKeyFromPem(privateKeyStr); + final publicKey = _parsePublicKeyFromPem(publicKeyStr); + + _currentKeyPair = AsymmetricKeyPair(publicKey, privateKey); + debugPrint('Loaded existing key pair successfully'); + return; + } catch (e) { + debugPrint('Error parsing stored keys: $e'); + // Continue to generate new keys + } + } + + // Generate new key pair + await generateAndStoreKeyPair(); + } catch (e) { + debugPrint('Error initializing key pair: $e'); + // Instead of rethrowing, we'll continue without encryption capability + // This ensures the app doesn't crash during startup + } + } + + /// Generate a new key pair and store it securely + Future generateAndStoreKeyPair() async { + try { + debugPrint('Generating new RSA key pair...'); + + // Generate key pair with a simpler approach + final keyPair = await _generateRSAKeyPairSimple(1024); // Smaller keys for faster generation + _currentKeyPair = keyPair; + + // Export to PEM format + final publicKeyPem = _encodePublicKeyToPem(keyPair.publicKey); + final privateKeyPem = _encodePrivateKeyToPem(keyPair.privateKey); + + // Store in secure storage + await _secureStorage.write(key: _publicKeyTag, value: publicKeyPem); + await _secureStorage.write(key: _privateKeyTag, value: privateKeyPem); + + debugPrint('New key pair generated and stored successfully'); + } catch (e) { + debugPrint('Failed to generate key pair: $e'); + // Don't throw, allow the app to continue without encryption + } + } + + /// Encrypt data with the public key + Uint8List? encrypt(String plainText) { + if (_currentKeyPair == null) { + debugPrint('No key pair available for encryption'); + return null; + } + + try { + final cipher = PKCS1Encoding(RSAEngine()) + ..init(true, PublicKeyParameter(_currentKeyPair!.publicKey)); + + final input = Uint8List.fromList(utf8.encode(plainText)); + return cipher.process(input); + } catch (e) { + debugPrint('Encryption error: $e'); + return null; + } + } + + /// Decrypt data with the private key + String? decrypt(Uint8List encryptedData) { + if (_currentKeyPair == null) { + debugPrint('No key pair available for decryption'); + return null; + } + + try { + final cipher = PKCS1Encoding(RSAEngine()) + ..init(false, PrivateKeyParameter(_currentKeyPair!.privateKey)); + + final decrypted = cipher.process(encryptedData); + return utf8.decode(decrypted); + } catch (e) { + debugPrint('Decryption error: $e'); + return null; + } + } + + /// Get the current public key in PEM format + Future getPublicKeyPem() async { + return await _secureStorage.read(key: _publicKeyTag); + } + + // Simpler RSA key pair generation that doesn't use FortunaRandom + Future> _generateRSAKeyPairSimple(int bitLength) async { + // Use a simple secure random instead of FortunaRandom + final secureRandom = _SecureRandom(); + + // Create RSA key generator + final keyGen = RSAKeyGenerator() + ..init(ParametersWithRandom( + RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), + secureRandom, + )); + + // Generate and return the key pair + return keyGen.generateKeyPair() as AsymmetricKeyPair; + } + + // Original method (but no longer used directly) + static AsymmetricKeyPair _generateRSAKeyPair(int bitLength) { + try { + final secureRandom = _SecureRandom(); + + final keyGen = RSAKeyGenerator() + ..init(ParametersWithRandom( + RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), + secureRandom, + )); + + return keyGen.generateKeyPair() as AsymmetricKeyPair; + } catch (e) { + debugPrint('Error in _generateRSAKeyPair: $e'); + rethrow; + } + } + + String _encodePublicKeyToPem(RSAPublicKey publicKey) { + final asn1Sequence = ASN1Sequence(); + + asn1Sequence.add(ASN1Integer(publicKey.modulus!)); + asn1Sequence.add(ASN1Integer(publicKey.exponent!)); + + final base64 = base64Encode(asn1Sequence.encodedBytes); + return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----'; + } + + String _encodePrivateKeyToPem(RSAPrivateKey privateKey) { + final asn1Sequence = ASN1Sequence(); + + asn1Sequence.add(ASN1Integer(BigInt.from(0))); // version + asn1Sequence.add(ASN1Integer(privateKey.modulus!)); + asn1Sequence.add(ASN1Integer(privateKey.publicExponent!)); + asn1Sequence.add(ASN1Integer(privateKey.privateExponent!)); + asn1Sequence.add(ASN1Integer(privateKey.p!)); + asn1Sequence.add(ASN1Integer(privateKey.q!)); + // d mod (p-1) + asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.p! - BigInt.from(1)))); + // d mod (q-1) + asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.q! - BigInt.from(1)))); + // q^-1 mod p + asn1Sequence.add(ASN1Integer(_modInverse(privateKey.q!, privateKey.p!))); + + final base64 = base64Encode(asn1Sequence.encodedBytes); + return '-----BEGIN RSA PRIVATE KEY-----\n$base64\n-----END RSA PRIVATE KEY-----'; + } + + RSAPrivateKey _parsePrivateKeyFromPem(String pemString) { + final pemContent = pemString + .replaceAll('-----BEGIN RSA PRIVATE KEY-----', '') + .replaceAll('-----END RSA PRIVATE KEY-----', '') + .replaceAll('\n', ''); + + final asn1Parser = ASN1Parser(base64Decode(pemContent)); + final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + + // Parse sequence values + final values = topLevelSeq.elements!.map((obj) => (obj as ASN1Integer).valueAsBigInteger).toList(); + + // Create RSA private key from components + return RSAPrivateKey( + values[1], // modulus + values[3], // privateExponent + values[4], // p + values[5], // q + ); + } + + RSAPublicKey _parsePublicKeyFromPem(String pemString) { + final pemContent = pemString + .replaceAll('-----BEGIN PUBLIC KEY-----', '') + .replaceAll('-----END PUBLIC KEY-----', '') + .replaceAll('\n', ''); + + final asn1Parser = ASN1Parser(base64Decode(pemContent)); + final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + + // Extract modulus and exponent + final modulus = (topLevelSeq.elements![0] as ASN1Integer).valueAsBigInteger; + final exponent = (topLevelSeq.elements![1] as ASN1Integer).valueAsBigInteger; + + return RSAPublicKey(modulus, exponent); + } + + // Modular multiplicative inverse + BigInt _modInverse(BigInt a, BigInt m) { + // Extended Euclidean Algorithm to find modular inverse + BigInt t = BigInt.zero, newT = BigInt.one; + BigInt r = m, newR = a; + + while (newR != BigInt.zero) { + final quotient = r ~/ newR; + + final tempT = t; + t = newT; + newT = tempT - quotient * newT; + + final tempR = r; + r = newR; + newR = tempR - quotient * newR; + } + + if (r > BigInt.one) throw Exception('$a is not invertible modulo $m'); + if (t < BigInt.zero) t += m; + + return t; + } +} + +// Simple secure random implementation that doesn't use AESEngine +class _SecureRandom implements SecureRandom { + final Random _random = Random.secure(); + + @override + String get algorithmName => 'Dart_SecureRandom'; + + @override + void seed(CipherParameters params) { + // No additional seeding required as Random.secure() is already seeded + } + + @override + BigInt nextBigInteger(int bitLength) { + final fullBytes = bitLength ~/ 8; + final remainingBits = bitLength % 8; + + final bytes = Uint8List(fullBytes + (remainingBits > 0 ? 1 : 0)); + for (var i = 0; i < bytes.length; i++) { + bytes[i] = nextUint8(); + } + + // Adjust the last byte to match the remaining bits + if (remainingBits > 0) { + bytes[bytes.length - 1] &= (1 << remainingBits) - 1; + } + + return BigInt.parse(bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''), radix: 16); + } + + @override + int nextUint16() => (_random.nextInt(1 << 16) & 0xFFFF); + + @override + int nextUint32() => (_random.nextInt(1 << 32) & 0xFFFFFFFF); + + @override + int nextUint8() => (_random.nextInt(1 << 8) & 0xFF); + + @override + void reset() { + // No reset needed for Random.secure() + } + + @override + @override + Uint8List nextBytes(int count) { + final bytes = Uint8List(count); + for (var i = 0; i < count; i++) { + bytes[i] = nextUint8(); + } + return bytes; + } +} diff --git a/dialer/lib/domain/services/obfuscate_service.dart b/dialer/lib/domain/services/obfuscate_service.dart new file mode 100644 index 0000000..7806222 --- /dev/null +++ b/dialer/lib/domain/services/obfuscate_service.dart @@ -0,0 +1,36 @@ +import '../../core/config/app_config.dart'; + +class ObfuscateService { + // Private constructor + ObfuscateService._privateConstructor(); + + // Singleton instance + static final ObfuscateService _instance = ObfuscateService._privateConstructor(); + + // Factory constructor to return the same instance + factory ObfuscateService() { + return _instance; + } + + // Public method to obfuscate data + String obfuscateData(String data) { + if (AppConfig.isStealthMode) { + return _obfuscateData(data); + } else { + return data; + } + } + + // Private helper method for obfuscation logic + String _obfuscateData(String data) { + if (data.isNotEmpty) { + // Ensure the string has at least two characters to obfuscate + if (data.length == 1) { + return '${data[0]}'; + } else { + return '${data[0]}...${data[data.length - 1]}'; + } + } + return ''; + } +} diff --git a/dialer/lib/domain/services/qr/qr_scanner.dart b/dialer/lib/domain/services/qr/qr_scanner.dart new file mode 100644 index 0000000..df405f6 --- /dev/null +++ b/dialer/lib/domain/services/qr/qr_scanner.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class QRCodeScannerScreen extends StatefulWidget { + const QRCodeScannerScreen({super.key}); + + @override + _QRCodeScannerScreenState createState() => _QRCodeScannerScreenState(); +} + +class _QRCodeScannerScreenState extends State { + MobileScannerController cameraController = MobileScannerController(); + bool _flashEnabled = false; + + @override + void dispose() { + cameraController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan QR Code'), + actions: [ + IconButton( + icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off), + onPressed: () { + setState(() { + _flashEnabled = !_flashEnabled; + cameraController.toggleTorch(); + }); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () => cameraController.switchCamera(), + ), + ], + ), + body: MobileScanner( + controller: cameraController, + onDetect: (capture) { + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + // Return the first barcode value + final String? code = barcodes.first.rawValue; + if (code != null) { + Navigator.pop(context, code); + } + } + }, + ), + ); + } +} diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart deleted file mode 100644 index 1416a98..0000000 --- a/dialer/lib/features/call/call_page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:dialer/services/call_service.dart'; -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; - -class CallPage extends StatefulWidget { - final String displayName; - final String phoneNumber; - final Uint8List? thumbnail; - - const CallPage({ - super.key, - required this.displayName, - required this.phoneNumber, - this.thumbnail, - }); - - @override - _CallPageState createState() => _CallPageState(); -} - -class _CallPageState extends State { - final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - bool isMuted = false; - bool isSpeakerOn = false; - bool isKeypadVisible = false; - bool icingProtocolOk = true; - String _typedDigits = ""; - - void _addDigit(String digit) { - setState(() { - _typedDigits += digit; - }); - } - - void _toggleMute() { - setState(() { - isMuted = !isMuted; - }); - } - - void _toggleSpeaker() { - setState(() { - isSpeakerOn = !isSpeakerOn; - }); - } - - void _toggleKeypad() { - setState(() { - isKeypadVisible = !isKeypadVisible; - }); - } - - void _toggleIcingProtocol() { - setState(() { - icingProtocolOk = !icingProtocolOk; - }); - } - - void _hangUp() async { - try { - await _callService.hangUpCall(context); - } catch (e) { - print("Error hanging up: $e"); - } - } - - @override - Widget build(BuildContext context) { - final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; - final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; - final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; - - return 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: [ - SizedBox(height: 35), - ObfuscatedAvatar( - imageBytes: widget.thumbnail, // Uses thumbnail if provided - 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, - 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( - 'Calling...', - 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.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: 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( - isSpeakerOn ? Icons.volume_up : Icons.volume_off, - color: isSpeakerOn ? 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: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () {}, - 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, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/dialer/lib/features/call/incoming_call_page.dart b/dialer/lib/features/call/incoming_call_page.dart deleted file mode 100644 index 2bad2eb..0000000 --- a/dialer/lib/features/call/incoming_call_page.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.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:dialer/features/call/call_page.dart'; - -class IncomingCallPage extends StatefulWidget { - final String displayName; - final String phoneNumber; - final Uint8List? thumbnail; - - const IncomingCallPage({ - super.key, - required this.displayName, - required this.phoneNumber, - this.thumbnail, - }); - - @override - _IncomingCallPageState createState() => _IncomingCallPageState(); -} - -class _IncomingCallPageState extends State { - static const MethodChannel _channel = MethodChannel('call_service'); - final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - bool icingProtocolOk = true; - - void _toggleIcingProtocol() { - setState(() { - icingProtocolOk = !icingProtocolOk; - }); - } - - void _answerCall() async { - try { - final result = await _channel.invokeMethod('answerCall'); - print('IncomingCallPage: Answer call result: $result'); - if (result["status"] == "answered") { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => CallPage( - displayName: widget.displayName, - phoneNumber: widget.phoneNumber, - thumbnail: widget.thumbnail, - ), - ), - ); - } - } catch (e) { - print("IncomingCallPage: Error answering call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error answering call: $e")), - ); - } - } - - void _declineCall() async { - try { - await _callService.hangUpCall(context); - } catch (e) { - print("IncomingCallPage: Error declining call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error declining call: $e")), - ); - } - } - - @override - Widget build(BuildContext context) { - const double avatarRadius = 45.0; - const double nameFontSize = 24.0; - const double statusFontSize = 16.0; - - return 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, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - _obfuscateService.obfuscateData(widget.displayName), - style: const TextStyle( - fontSize: nameFontSize, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - Text( - widget.phoneNumber, - style: const TextStyle(fontSize: statusFontSize, color: Colors.white70), - ), - const Text( - 'Incoming Call...', - style: TextStyle(fontSize: statusFontSize, color: Colors.white70), - ), - ], - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: _declineCall, - 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, - ), - ), - ), - GestureDetector( - onTap: _answerCall, - child: Container( - padding: const EdgeInsets.all(12), - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.call, - color: Colors.white, - size: 32, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/dialer/lib/features/contacts/contact_page.dart b/dialer/lib/features/contacts/contact_page.dart deleted file mode 100644 index 9f09490..0000000 --- a/dialer/lib/features/contacts/contact_page.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart'; -import 'package:flutter/material.dart'; -import 'package:dialer/widgets/loading_indicator.dart'; - -class ContactPage extends StatefulWidget { - const ContactPage({super.key}); - - @override - _ContactPageState createState() => _ContactPageState(); -} - -class _ContactPageState extends State { - @override - Widget build(BuildContext context) { - final contactState = ContactState.of(context); - return Scaffold( - body: contactState.loading - ? const LoadingIndicatorWidget() - : AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: contactState.contacts, // Use all contacts here - ), - ); - } -} diff --git a/dialer/lib/features/favorites/favorites_page.dart b/dialer/lib/features/favorites/favorites_page.dart deleted file mode 100644 index 47f74ec..0000000 --- a/dialer/lib/features/favorites/favorites_page.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart'; -import 'package:flutter/material.dart'; -import 'package:dialer/widgets/loading_indicator.dart'; - -class FavoritesPage extends StatefulWidget { - const FavoritesPage({super.key}); - - @override - _FavoritesPageState createState() => _FavoritesPageState(); -} - -class _FavoritesPageState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final contactState = ContactState.of(context); - return Scaffold( - body: contactState.loading - ? const LoadingIndicatorWidget() - : AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: - contactState.favoriteContacts, // Use only favorites here - ), - ); - } -} diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart deleted file mode 100644 index 65adaa8..0000000 --- a/dialer/lib/features/home/home_page.dart +++ /dev/null @@ -1,325 +0,0 @@ -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:flutter/material.dart'; -import 'package:dialer/features/contacts/contact_page.dart'; -import 'package:dialer/features/favorites/favorites_page.dart'; -import 'package:dialer/features/history/history_page.dart'; -import 'package:dialer/features/composition/composition.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:dialer/features/settings/settings.dart'; -import '../../services/contact_service.dart'; -import 'package:dialer/features/voicemail/voicemail_page.dart'; -import '../contacts/widgets/contact_modal.dart'; - - -class _MyHomePageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - List _allContacts = []; - List _contactSuggestions = []; - final ContactService _contactService = ContactService(); - final ObfuscateService _obfuscateService = ObfuscateService(); - final TextEditingController _searchController = TextEditingController(); - - - @override - void initState() { - super.initState(); - // Set the TabController length to 4 - _tabController = TabController(length: 4, vsync: this, initialIndex: 1); - _tabController.addListener(_handleTabIndex); - _fetchContacts(); - } - - void _fetchContacts() async { - _allContacts = await _contactService.fetchContacts(); - setState(() {}); - } - - void _clearSearch() { - _searchController.clear(); - _onSearchChanged(''); - } - - void _onSearchChanged(String query) { - setState(() { - if (query.isEmpty) { - _contactSuggestions = List.from(_allContacts); // Reset suggestions - } else { - _contactSuggestions = _allContacts.where((contact) { - return contact.displayName - .toLowerCase() - .contains(query.toLowerCase()); - }).toList(); - } - }); - } - - @override - void dispose() { - _searchController.dispose(); - _tabController.removeListener(_handleTabIndex); - _tabController.dispose(); - super.dispose(); - } - - void _handleTabIndex() { - setState(() {}); - } - - void _toggleFavorite(Contact contact) async { - try { - if (await FlutterContacts.requestPermission()) { - Contact? fullContact = await FlutterContacts.getContact(contact.id, - withProperties: true, - withAccounts: true, - withPhoto: 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"); - } - } catch (e) { - print("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update contact favorite status')), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: Column( - children: [ - // Persistent Search Bar - Padding( - padding: const EdgeInsets.only( - top: 24.0, - bottom: 10.0, - left: 16.0, - right: 16.0, - ), - child: Row( - children: [ - Expanded( - 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), - ), - child: SearchAnchor( - builder: - (BuildContext context, SearchController controller) { - return GestureDetector( - onTap: () { - controller.openView(); // Open the search view - }, - 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), - ), - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 16.0), - child: Row( - children: [ - const Icon(Icons.search, - color: Colors.grey, size: 24.0), - const SizedBox(width: 8.0), - Text( - _searchController.text.isEmpty - ? 'Search contacts' - : _searchController.text, - style: const TextStyle( - color: Colors.grey, fontSize: 16.0), - ), - const Spacer(), - if (_searchController.text.isNotEmpty) - GestureDetector( - onTap: _clearSearch, - child: const Icon( - Icons.clear, - color: Colors.grey, - size: 24.0, - ), - ), - ], - ), - ), - ); - }, - viewOnChanged: (query) { - _onSearchChanged(query); // Update immediately - }, - 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)), - 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, - backgroundColor: Colors.transparent, - builder: (context) { - return ContactModal( - contact: contact, - onEdit: () async { - if (await FlutterContacts - .requestPermission()) { - final updatedContact = - await FlutterContacts - .openExternalEdit(contact.id); - if (updatedContact != null) { - _fetchContacts(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - '${contact.displayName} updated successfully!'), - ), - ); - } else { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - 'Edit canceled or failed.'), - ), - ); - } - } - }, - onToggleFavorite: () => - _toggleFavorite(contact), - isFavorite: contact.isStarred, - ); - }, - ); - }, - ); - }).toList(); - }, - ), - ), - ), - // 3-dot menu - PopupMenuButton( - icon: const Icon(Icons.more_vert, color: Colors.white), - itemBuilder: (BuildContext context) => [ - const PopupMenuItem( - value: 'settings', - child: Text('Settings'), - ), - ], - onSelected: (String value) { - if (value == 'settings') { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsPage()), - ); - } - }, - ), - ], - ), - ), - // Main content with TabBarView - Expanded( - child: Stack( - children: [ - TabBarView( - controller: _tabController, - children: const [ - FavoritesPage(), - HistoryPage(), - ContactPage(), - VoicemailPage(), - ], - ), - Positioned( - right: 20, - bottom: 20, - child: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CompositionPage(), - ), - ); - }, - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(45), - ), - child: const Icon(Icons.dialpad, color: Colors.white), - ), - ), - ], - ), - ), - ], - ), - bottomNavigationBar: Container( - color: Colors.black, - child: TabBar( - controller: _tabController, - tabs: [ - Tab( - icon: Icon(_tabController.index == 0 - ? Icons.star - : Icons.star_border)), - Tab( - icon: Icon(_tabController.index == 1 - ? Icons.access_time_filled - : Icons.access_time_outlined)), - Tab( - icon: Icon(_tabController.index == 2 - ? Icons.contacts - : Icons.contacts_outlined)), - Tab( - icon: Icon(_tabController.index == 3 - ? Icons.voicemail - : Icons.voicemail_outlined), - ), - ], - labelColor: Colors.white, - unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), - indicatorSize: TabBarIndicatorSize.label, - indicatorColor: Colors.white, - ), - ), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - _MyHomePageState createState() => _MyHomePageState(); -} diff --git a/dialer/lib/features/settings/blocked/settings_blocked.dart b/dialer/lib/features/settings/blocked/settings_blocked.dart deleted file mode 100644 index 77c3a85..0000000 --- a/dialer/lib/features/settings/blocked/settings_blocked.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class BlockedNumbersPage extends StatefulWidget { - const BlockedNumbersPage({super.key}); - - @override - _BlockedNumbersPageState createState() => _BlockedNumbersPageState(); -} - -class _BlockedNumbersPageState extends State { - bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers - List _blockedNumbers = []; // List of blocked numbers - final TextEditingController _numberController = TextEditingController(); - - @override - void initState() { - super.initState(); - _loadPreferences(); // Load data on initialization - } - - // Load preferences from local storage - Future _loadPreferences() async { - final prefs = await SharedPreferences.getInstance(); - setState(() { - _blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false; - _blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - }); - } - - // Save preferences to local storage - Future _savePreferences() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('blockUnknownNumbers', _blockUnknownNumbers); - await prefs.setStringList('blockedNumbers', _blockedNumbers); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - title: const Text('Blocked Numbers'), - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - SwitchListTile( - title: const Text( - 'Block Unknown Numbers', - style: TextStyle(color: Colors.white), - ), - value: _blockUnknownNumbers, - onChanged: (bool value) { - setState(() { - _blockUnknownNumbers = value; - _savePreferences(); // Save the state to local storage - }); - }, - ), - const SizedBox(height: 16), - ListTile( - title: const Text( - 'Blocked Numbers', - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), - ), - subtitle: _blockedNumbers.isEmpty - ? const Text( - 'No blocked numbers', - style: TextStyle(color: Colors.grey), - ) - : null, - ), - ..._blockedNumbers.map( - (number) => ListTile( - title: Text( - number, - style: const TextStyle(color: Colors.white), - ), - trailing: IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () => _unblockNumber(number), - ), - ), - ), - const Divider(color: Colors.grey), - ListTile( - title: const Text( - 'Block a Number', - style: TextStyle(color: Colors.white), - ), - trailing: const Icon(Icons.add, color: Colors.white), - onTap: () => _showBlockNumberDialog(), - ), - ], - ), - ); - } - - // Function to block a number - void _blockNumber(String number) { - if (number.isNotEmpty && !_blockedNumbers.contains(number)) { - setState(() { - _blockedNumbers.add(number); - _savePreferences(); // Save the updated list - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$number has been blocked')), - ); - } - } - - // Function to unblock a number - void _unblockNumber(String number) { - setState(() { - _blockedNumbers.remove(number); - _savePreferences(); // Save the updated list - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$number has been unblocked')), - ); - } - - // Dialog for blocking a new number - void _showBlockNumberDialog() { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Block a Number', style: TextStyle(color: Colors.white)), - content: TextField( - controller: _numberController, - keyboardType: TextInputType.phone, - decoration: const InputDecoration( - hintText: 'Enter number', - hintStyle: TextStyle(color: Colors.grey), - ), - style: const TextStyle(color: Colors.white), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel', style: TextStyle(color: Colors.white)), - ), - TextButton( - onPressed: () { - _blockNumber(_numberController.text); - _numberController.clear(); - Navigator.pop(context); - }, - child: const Text('Block', style: TextStyle(color: Colors.red)), - ), - ], - ); - }, - ); - } - - @override - void dispose() { - _numberController.dispose(); - super.dispose(); - } -} diff --git a/dialer/lib/features/settings/cryptography/key_management.dart b/dialer/lib/features/settings/cryptography/key_management.dart deleted file mode 100644 index f766ebc..0000000 --- a/dialer/lib/features/settings/cryptography/key_management.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; - -class ManageKeysPage extends StatefulWidget { - const ManageKeysPage({Key? key}) : super(key: key); - - @override - _ManageKeysPageState createState() => _ManageKeysPageState(); -} - -class _ManageKeysPageState extends State { - final AsymmetricCryptoService _cryptoService = AsymmetricCryptoService(); - List> _keys = []; - bool _isLoading = false; - - @override - void initState() { - super.initState(); - _loadKeys(); - } - - Future _loadKeys() async { - setState(() { - _isLoading = true; - }); - try { - List> keys = await _cryptoService.getAllKeys(); - setState(() { - _keys = keys; - }); - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error loading keys: $e'))); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - Future _generateKey() async { - setState(() { - _isLoading = true; - }); - try { - await _cryptoService.generateKeyPair(); - await _loadKeys(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Key generated successfully'))); - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error generating key: $e'))); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - Future _deleteKey(String alias) async { - setState(() { - _isLoading = true; - }); - try { - await _cryptoService.deleteKeyPair(alias); - await _loadKeys(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Key deleted successfully'))); - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error deleting key: $e'))); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - Future _viewPublicKey(String alias) async { - try { - final publicKey = await _cryptoService.getPublicKey(alias); - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Public Key'), - content: SingleChildScrollView(child: Text(publicKey)), - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error retrieving public key: $e'))); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Manage Keys'), - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _keys.isEmpty - ? const Center(child: Text('No keys found')) - : ListView.builder( - itemCount: _keys.length, - itemBuilder: (context, index) { - final keyData = _keys[index]; - return ListTile( - title: Text(keyData['label'] ?? 'No label'), - subtitle: Text(keyData['alias'] ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.visibility), - tooltip: 'View Public Key', - onPressed: () => _viewPublicKey(keyData['alias']), - ), - IconButton( - icon: const Icon(Icons.delete), - tooltip: 'Delete Key', - onPressed: () => _deleteKey(keyData['alias']), - ), - ], - ), - ); - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: _generateKey, - child: const Icon(Icons.add), - tooltip: 'Generate New Key', - ), - ); - } -} \ No newline at end of file diff --git a/dialer/lib/globals.dart b/dialer/lib/globals.dart index 2750ab2..a8c1ab7 100644 --- a/dialer/lib/globals.dart +++ b/dialer/lib/globals.dart @@ -1 +1,5 @@ +// Global variables accessible throughout the app +library globals; + +// Whether the app is in stealth mode (obfuscated content) bool isStealthMode = false; \ No newline at end of file diff --git a/dialer/lib/main.dart b/dialer/lib/main.dart index d3513cf..7ebebfc 100644 --- a/dialer/lib/main.dart +++ b/dialer/lib/main.dart @@ -1,24 +1,34 @@ -import 'package:dialer/features/home/home_page.dart'; import 'package:flutter/material.dart'; -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/services/call_service.dart'; import 'package:flutter/services.dart'; -import 'globals.dart' as globals; -import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'core/config/app_config.dart'; +import 'core/navigation/app_router.dart'; +import 'domain/services/call_service.dart'; +import 'domain/services/cryptography/asymmetric_crypto_service.dart'; +import 'presentation/common/theme/app_theme.dart'; +import 'presentation/features/contacts/contact_state.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); - const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); - globals.isStealthMode = stealthFlag.toLowerCase() == 'true'; + + // Initialize app configuration + await AppConfig.initialize(); + // Initialize cryptography service with error handling final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); - await cryptoService.initializeDefaultKeyPair(); + try { + await cryptoService.initializeDefaultKeyPair(); + } catch (e) { + debugPrint('Error initializing cryptography: $e'); + // Continue app initialization even if crypto fails + } // Request permissions before running the app await _requestPermissions(); + // Initialize call service CallService(); runApp( @@ -28,38 +38,49 @@ void main() async { create: (_) => cryptoService, ), ], - child: Dialer(), + child: const DialerApp(), ), ); } Future _requestPermissions() async { - Map statuses = await [ - Permission.phone, - Permission.contacts, - Permission.microphone, - ].request(); - if (statuses.values.every((status) => status.isGranted)) { - print("All required permissions granted"); - const channel = MethodChannel('call_service'); - await channel.invokeMethod('permissionsGranted'); - } else { - print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); + try { + Map statuses = await [ + Permission.phone, + Permission.contacts, + Permission.microphone, + ].request(); + + if (statuses.values.every((status) => status.isGranted)) { + debugPrint("All required permissions granted"); + const channel = MethodChannel('call_service'); + await channel.invokeMethod('permissionsGranted'); + } else { + debugPrint("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); + } + } catch (e) { + debugPrint("Error requesting permissions: $e"); } } -class Dialer extends StatelessWidget { - const Dialer({super.key}); +class DialerApp extends StatelessWidget { + const DialerApp({super.key}); @override Widget build(BuildContext context) { return ContactState( child: MaterialApp( + title: 'Dialer App', navigatorKey: CallService.navigatorKey, - theme: ThemeData( - brightness: Brightness.dark, - ), - home: SafeArea(child: MyHomePage()), + theme: AppTheme.darkTheme, + onGenerateRoute: AppRouter.generateRoute, + initialRoute: '/', + // Add a builder to wrap all routes with SafeArea + builder: (context, child) { + return SafeArea( + child: child ?? const SizedBox.shrink(), + ); + }, ), ); } diff --git a/dialer/lib/main_old.dart b/dialer/lib/main_old.dart new file mode 100644 index 0000000..5175346 --- /dev/null +++ b/dialer/lib/main_old.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; + +import 'services/call_service.dart'; +import 'services/cryptography/asymmetric_crypto_service.dart'; +import 'globals.dart' as globals; +import 'presentation/features/home/home_page.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Check for stealth mode + const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); + globals.isStealthMode = stealthFlag.toLowerCase() == 'true'; + + // Initialize cryptography service with error handling + final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); + try { + await cryptoService.initializeDefaultKeyPair(); + } catch (e) { + debugPrint('Error initializing cryptography: $e'); + // Continue app initialization even if crypto fails + } + + // Request permissions before running the app + await _requestPermissions(); + + // Initialize call service + CallService(); + + runApp( + MultiProvider( + providers: [ + Provider( + create: (_) => cryptoService, + ), + ], + child: const DialerApp(), + ), + ); +} + +Future _requestPermissions() async { + try { + Map statuses = await [ + Permission.phone, + Permission.contacts, + Permission.microphone, + ].request(); + + if (statuses.values.every((status) => status.isGranted)) { + debugPrint("All required permissions granted"); + const channel = MethodChannel('call_service'); + await channel.invokeMethod('permissionsGranted'); + } else { + debugPrint("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); + } + } catch (e) { + debugPrint("Error requesting permissions: $e"); + } +} + +class DialerApp extends StatelessWidget { + const DialerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Dialer App', + navigatorKey: CallService.navigatorKey, + theme: ThemeData.dark().copyWith( + scaffoldBackgroundColor: Colors.black, + primaryColor: Colors.blue, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + elevation: 0, + ), + ), + home: const MyHomePage(), + ); + } +} diff --git a/dialer/lib/presentation/common/theme/app_theme.dart b/dialer/lib/presentation/common/theme/app_theme.dart new file mode 100644 index 0000000..f45e68d --- /dev/null +++ b/dialer/lib/presentation/common/theme/app_theme.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData get darkTheme => ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: Colors.black, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + elevation: 0, + ), + tabBarTheme: const TabBarTheme( + labelColor: Colors.white, + unselectedLabelColor: Color.fromARGB(255, 158, 158, 158), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Colors.white, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.black, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + textTheme: const TextTheme( + bodyLarge: TextStyle(color: Colors.white), + bodyMedium: TextStyle(color: Colors.white), + titleLarge: TextStyle(color: Colors.white), + ), + snackBarTheme: const SnackBarThemeData( + backgroundColor: Color(0xFF303030), + contentTextStyle: TextStyle(color: Colors.white), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: const Color.fromARGB(255, 30, 30, 30), + ), + ); +} diff --git a/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart b/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart new file mode 100644 index 0000000..36d49dd --- /dev/null +++ b/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import '../../../core/config/app_config.dart'; +import '../../../core/utils/color_utils.dart'; + +class ObfuscatedAvatar extends StatelessWidget { + final Uint8List? imageBytes; + final double radius; + final Color backgroundColor; + final String? fallbackInitial; + + const ObfuscatedAvatar({ + Key? key, + required this.imageBytes, + this.radius = 25, + this.backgroundColor = Colors.grey, + this.fallbackInitial, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (imageBytes != null && imageBytes!.isNotEmpty) { + return ClipOval( + child: ImageFiltered( + imageFilter: AppConfig.isStealthMode + ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Image.memory( + imageBytes!, + fit: BoxFit.cover, + width: radius * 2, + height: radius * 2, + ), + ), + ); + } else { + return CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + child: Text( + fallbackInitial != null && fallbackInitial!.isNotEmpty + ? fallbackInitial![0].toUpperCase() + : '?', + style: TextStyle( + color: darken(backgroundColor), + fontSize: radius, + ), + ), + ); + } + } +} diff --git a/dialer/lib/presentation/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart new file mode 100644 index 0000000..6471361 --- /dev/null +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:typed_data'; +import '../../../domain/services/call_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../../core/utils/color_utils.dart'; +import '../../../presentation/common/widgets/obfuscated_avatar.dart'; + +class CallPage extends StatefulWidget { + final String displayName; + final String phoneNumber; + final Uint8List? thumbnail; + + const CallPage({ + super.key, + required this.displayName, + required this.phoneNumber, + this.thumbnail, + }); + + @override + _CallPageState createState() => _CallPageState(); +} + +class _CallPageState extends State { + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + bool isMuted = false; + bool isSpeakerOn = false; + bool isKeypadVisible = false; + bool icingProtocolOk = true; + String _typedDigits = ""; + + void _addDigit(String digit) { + setState(() { + _typedDigits += digit; + }); + } + + void _toggleMute() { + setState(() { + isMuted = !isMuted; + }); + } + + void _toggleSpeaker() { + setState(() { + isSpeakerOn = !isSpeakerOn; + }); + } + + void _toggleKeypad() { + setState(() { + isKeypadVisible = !isKeypadVisible; + }); + } + + void _hangUp() async { + try { + await _callService.hangUpCall(context); + } catch (e) { + debugPrint("Error hanging up: $e"); + } + } + + @override + Widget build(BuildContext context) { + final double avatarRadius = 45.0; + + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ObfuscatedAvatar( + imageBytes: widget.thumbnail, + radius: avatarRadius, + backgroundColor: generateColorFromName(widget.displayName), + fallbackInitial: widget.displayName, + ), + const SizedBox(height: 16), + Text( + _obfuscateService.obfuscateData(widget.displayName), + style: const TextStyle( + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.phoneNumber, + style: const TextStyle(fontSize: 16, color: Colors.white70), + ), + const SizedBox(height: 8), + Text( + 'Calling...', + style: const TextStyle(fontSize: 16, color: Colors.white70), + ), + ], + ), + ), + + Expanded( + child: isKeypadVisible + ? _buildKeypad() + : _buildCallControls(), + ), + + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: GestureDetector( + onTap: _hangUp, + child: Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.call_end, + color: Colors.white, + size: 32, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildKeypad() { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Text( + _typedDigits, + style: const TextStyle( + fontSize: 24, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 16), + Expanded( + child: GridView.count( + crossAxisCount: 3, + childAspectRatio: 1.5, + children: [ + _buildDialButton('1'), + _buildDialButton('2'), + _buildDialButton('3'), + _buildDialButton('4'), + _buildDialButton('5'), + _buildDialButton('6'), + _buildDialButton('7'), + _buildDialButton('8'), + _buildDialButton('9'), + _buildDialButton('*'), + _buildDialButton('0'), + _buildDialButton('#'), + ], + ), + ), + TextButton( + onPressed: _toggleKeypad, + child: const Text('Hide Keypad'), + ), + ], + ); + } + + Widget _buildCallControls() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildControlButton( + icon: isMuted ? Icons.mic_off : Icons.mic, + label: isMuted ? 'Unmute' : 'Mute', + onPressed: _toggleMute, + ), + _buildControlButton( + icon: Icons.dialpad, + label: 'Keypad', + onPressed: _toggleKeypad, + ), + _buildControlButton( + icon: isSpeakerOn ? Icons.volume_up : Icons.volume_off, + label: 'Speaker', + onPressed: _toggleSpeaker, + iconColor: isSpeakerOn ? Colors.amber : Colors.white, + ), + ], + ), + ], + ); + } + + Widget _buildDialButton(String digit) { + return InkWell( + onTap: () => _addDigit(digit), + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[800], + ), + child: Center( + child: Text( + digit, + style: const TextStyle( + fontSize: 24, + color: Colors.white, + ), + ), + ), + ), + ); + } + + Widget _buildControlButton({ + required IconData icon, + required String label, + required VoidCallback onPressed, + Color iconColor = Colors.white, + }) { + return Column( + children: [ + IconButton( + iconSize: 32, + icon: Icon(icon, color: iconColor), + onPressed: onPressed, + ), + Text( + label, + style: const TextStyle(color: Colors.white), + ), + ], + ); + } +} \ No newline at end of file diff --git a/dialer/lib/presentation/features/call/incoming_call_page.dart b/dialer/lib/presentation/features/call/incoming_call_page.dart new file mode 100644 index 0000000..c127b74 --- /dev/null +++ b/dialer/lib/presentation/features/call/incoming_call_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:typed_data'; +import '../../../domain/services/call_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../../core/utils/color_utils.dart'; +import '../../../presentation/common/widgets/obfuscated_avatar.dart'; +import 'call_page.dart'; + +class IncomingCallPage extends StatefulWidget { + final String displayName; + final String phoneNumber; + final Uint8List? thumbnail; + + const IncomingCallPage({ + super.key, + required this.displayName, + required this.phoneNumber, + this.thumbnail, + }); + + @override + _IncomingCallPageState createState() => _IncomingCallPageState(); +} + +class _IncomingCallPageState extends State { + static const MethodChannel _channel = MethodChannel('call_service'); + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + + void _answerCall() async { + try { + final result = await _channel.invokeMethod('answerCall'); + debugPrint('IncomingCallPage: Answer call result: $result'); + + if (result["status"] == "answered") { + if (!mounted) return; + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => CallPage( + displayName: widget.displayName, + phoneNumber: widget.phoneNumber, + thumbnail: widget.thumbnail, + ), + ), + ); + } + } catch (e) { + debugPrint("IncomingCallPage: Error answering call: $e"); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error answering call: $e")), + ); + } + } + + void _declineCall() async { + try { + await _callService.hangUpCall(context); + } catch (e) { + debugPrint("IncomingCallPage: Error declining call: $e"); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error declining call: $e")), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + children: [ + const Spacer(), + ObfuscatedAvatar( + imageBytes: widget.thumbnail, + radius: 60, + backgroundColor: generateColorFromName(widget.displayName), + fallbackInitial: widget.displayName, + ), + const SizedBox(height: 24), + Text( + _obfuscateService.obfuscateData(widget.displayName), + style: const TextStyle( + fontSize: 28, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Text( + widget.phoneNumber, + style: const TextStyle(fontSize: 18, color: Colors.white70), + ), + const SizedBox(height: 16), + const Text( + 'Incoming Call', + style: TextStyle(fontSize: 20, color: Colors.white70), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 48.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildActionButton( + icon: Icons.call_end, + color: Colors.red, + onPressed: _declineCall, + label: 'Decline', + ), + _buildActionButton( + icon: Icons.call, + color: Colors.green, + onPressed: _answerCall, + label: 'Answer', + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required Color color, + required VoidCallback onPressed, + required String label, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: onPressed, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 32, + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle(color: Colors.white), + ), + ], + ); + } +} \ No newline at end of file diff --git a/dialer/lib/features/composition/composition.dart b/dialer/lib/presentation/features/composition/composition.dart similarity index 98% rename from dialer/lib/features/composition/composition.dart rename to dialer/lib/presentation/features/composition/composition.dart index b807ce4..1497325 100644 --- a/dialer/lib/features/composition/composition.dart +++ b/dialer/lib/presentation/features/composition/composition.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../services/contact_service.dart'; -import '../../services/obfuscate_service.dart'; -import '../../services/call_service.dart'; +import '../../../services/contact_service.dart'; +import '../../../services/obfuscate_service.dart'; +import '../../../services/call_service.dart'; import '../contacts/widgets/add_contact_button.dart'; class CompositionPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart new file mode 100644 index 0000000..00b227e --- /dev/null +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../contacts/contact_state.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../common/widgets/obfuscated_avatar.dart'; +import '../../../core/utils/color_utils.dart'; +import '../../../domain/services/call_service.dart'; +import '../../../domain/services/block_service.dart'; +import 'widgets/contact_modal.dart'; +import 'widgets/alphabet_scroll_page.dart'; + +class ContactPage extends StatefulWidget { + const ContactPage({super.key}); + + @override + _ContactPageState createState() => _ContactPageState(); +} + +class _ContactPageState extends State { + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + int? _expandedIndex; + + Future _refreshContacts() async { + final contactState = ContactState.of(context); + try { + await contactState.fetchContacts(); + } catch (e) { + debugPrint('Error refreshing contacts: $e'); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Failed to refresh contacts'))); + } + } + } + + void _toggleFavorite(Contact contact) async { + try { + if (await FlutterContacts.requestPermission()) { + Contact? fullContact = await FlutterContacts.getContact(contact.id, + withProperties: true, + withAccounts: true, + withPhoto: true, + withThumbnail: true); + + if (fullContact != null) { + fullContact.isStarred = !fullContact.isStarred; + await FlutterContacts.updateContact(fullContact); + } + await _refreshContacts(); + } else { + debugPrint("Could not fetch contact details"); + } + } catch (e) { + debugPrint("Error updating favorite status: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update favorite status'))); + } + } + + @override + Widget build(BuildContext context) { + final contactState = ContactState.of(context); + + if (contactState.loading) { + return const Center(child: CircularProgressIndicator()); + } + + final contacts = contactState.contacts; + + if (contacts.isEmpty) { + return const Center( + child: Text( + 'No contacts found.\nAdd contacts to get started.', + style: TextStyle(color: Colors.white60), + textAlign: TextAlign.center, + ), + ); + } + + return Scaffold( + backgroundColor: Colors.black, + body: AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: contacts, + ), + ); + } + + Widget _getCallIcon(Contact contact) { + if (!contact.phones.isNotEmpty) return const SizedBox.shrink(); + return Icon( + contact.isStarred ? Icons.star : Icons.star_border, + color: contact.isStarred ? Colors.amber : Colors.grey, + ); + } + + Future _launchSms(Contact contact) async { + if (!contact.phones.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No phone number available')), + ); + return; + } + + final Uri smsUri = Uri( + scheme: 'sms', + path: contact.phones.first.number, + ); + + if (await canLaunchUrl(smsUri)) { + await launchUrl(smsUri); + } else { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not launch SMS')), + ); + } + } + + void _editContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = await FlutterContacts.openExternalEdit(contact.id); + if (updatedContact != null) { + await _refreshContacts(); + setState(() => _expandedIndex = null); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${contact.displayName} updated successfully!')), + ); + } + } + } + + void _toggleBlock(Contact contact, bool isBlocked) async { + if (!contact.phones.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No phone number to block')), + ); + return; + } + + final phoneNumber = contact.phones.first.number; + if (isBlocked) { + await BlockService().unblockNumber(phoneNumber); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber unblocked')), + ); + } else { + await BlockService().blockNumber(phoneNumber); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber blocked')), + ); + } + setState(() {}); + } + + void _showContactModal(Contact contact) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => ContactModal( + contact: contact, + onEdit: () async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = await FlutterContacts.openExternalEdit(contact.id); + if (updatedContact != null) { + await _refreshContacts(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${contact.displayName} updated successfully!')), + ); + } + } + }, + onToggleFavorite: () => _toggleFavorite(contact), + isFavorite: contact.isStarred, + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required VoidCallback onPressed, + }) { + return InkWell( + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: Colors.white), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } +} diff --git a/dialer/lib/features/contacts/contact_state.dart b/dialer/lib/presentation/features/contacts/contact_state.dart similarity index 75% rename from dialer/lib/features/contacts/contact_state.dart rename to dialer/lib/presentation/features/contacts/contact_state.dart index 58e7574..5d6ca9b 100644 --- a/dialer/lib/features/contacts/contact_state.dart +++ b/dialer/lib/presentation/features/contacts/contact_state.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; -import '../../services/contact_service.dart'; +import '../../../domain/services/contact_service.dart'; class ContactState extends StatefulWidget { final Widget child; @@ -23,7 +23,7 @@ class _ContactStateState extends State { List _favoriteContacts = []; bool _loading = true; double _scrollOffset = 0.0; - Contact? _selfContact = Contact(); + Contact? _selfContact; // Getters for all contacts and favorites List get contacts => _allContacts; @@ -39,7 +39,9 @@ class _ContactStateState extends State { FlutterContacts.addListener(_onContactChange); } - void _onContactChange() => fetchContacts(); + void _onContactChange() async { + await fetchContacts(); + } @override void dispose() { @@ -49,27 +51,43 @@ class _ContactStateState extends State { // Fetch all contacts Future fetchContacts() async { + if (!mounted) return; + setState(() => _loading = true); try { List contacts = await _contactService.fetchContacts(); _processContacts(contacts); + } catch (e) { + debugPrint('Error fetching contacts: $e'); } finally { - setState(() => _loading = false); + if (mounted) { + setState(() => _loading = false); + } } } // Fetch only favorite contacts Future fetchFavoriteContacts() async { + if (!mounted) return; + setState(() => _loading = true); try { List contacts = await _contactService.fetchFavoriteContacts(); - setState(() => _favoriteContacts = contacts); + if (mounted) { + setState(() => _favoriteContacts = contacts); + } + } catch (e) { + debugPrint('Error fetching favorite contacts: $e'); } finally { - setState(() => _loading = false); + if (mounted) { + setState(() => _loading = false); + } } } void _processContacts(List contacts) { + if (!mounted) return; + _selfContact = contacts.firstWhere( (contact) => contact.displayName.toLowerCase() == "user", orElse: () => Contact(), @@ -87,7 +105,6 @@ class _ContactStateState extends State { _allContacts = contacts; _favoriteContacts = contacts.where((contact) => contact.isStarred).toList(); - _selfContact = _selfContact; }); } @@ -102,25 +119,6 @@ class _ContactStateState extends State { }); } - bool doesContactExist(Contact contact) { - // Example: consider it "existing" if there's a matching phone number - for (final existing in _allContacts) { - if (existing.toVCard() == contact.toVCard()) { - return true; - } - // for (final phone in existing.phones) { - // for (final newPhone in contact.phones) { - // // Simple exact match; you can do more advanced logic - // if (phone.normalizedNumber == newPhone.normalizedNumber) { - // return true; - // } - // } - // } We might switch to finer and smarter logic later, ex: remove trailing spaces, capitals - } - return false; - } - - @override Widget build(BuildContext context) { return _InheritedContactState( @@ -130,7 +128,6 @@ class _ContactStateState extends State { } } - class _InheritedContactState extends InheritedWidget { final _ContactStateState data; diff --git a/dialer/lib/features/contacts/widgets/add_contact_button.dart b/dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart similarity index 97% rename from dialer/lib/features/contacts/widgets/add_contact_button.dart rename to dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart index 399ab48..947bbcf 100644 --- a/dialer/lib/features/contacts/widgets/add_contact_button.dart +++ b/dialer/lib/presentation/features/contacts/widgets/add_contact_button.dart @@ -1,6 +1,6 @@ -import 'package:dialer/widgets/qr_scanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import '../../../../domain/services/qr/qr_scanner.dart'; class AddContactButton extends StatelessWidget { const AddContactButton({super.key}); diff --git a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart similarity index 100% rename from dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart rename to dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart similarity index 51% rename from dialer/lib/features/contacts/widgets/contact_modal.dart rename to dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index 198f614..4f5d659 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -1,11 +1,12 @@ -import 'package:dialer/services/obfuscate_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; -import '../../../services/block_service.dart'; -import '../../../services/contact_service.dart'; -import '../../../services/call_service.dart'; +import '../../../../widgets/username_color_generator.dart'; +import '../../../../widgets/color_darkener.dart'; +import '../../../../services/obfuscate_service.dart'; +import '../../../../services/block_service.dart'; +import '../../../../services/contact_service.dart'; +import '../../../../services/call_service.dart'; class ContactModal extends StatefulWidget { final Contact contact; @@ -30,6 +31,7 @@ class _ContactModalState extends State { bool isBlocked = false; final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); + final ContactService _contactService = ContactService(); @override void initState() { @@ -43,9 +45,11 @@ class _ContactModalState extends State { Future _checkIfBlocked() async { if (phoneNumber != 'No phone number') { bool blocked = await BlockService().isNumberBlocked(phoneNumber); - setState(() { - isBlocked = blocked; - }); + if (mounted) { + setState(() { + isBlocked = blocked; + }); + } } } @@ -54,22 +58,32 @@ class _ContactModalState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No phone number to block or unblock')), ); - } else if (isBlocked) { + return; + } + + if (isBlocked) { await BlockService().unblockNumber(phoneNumber); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber has been unblocked')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber has been unblocked')), + ); + } } else { await BlockService().blockNumber(phoneNumber); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber has been blocked')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$phoneNumber has been blocked')), + ); + } } - if (phoneNumber != 'No phone number') { + if (phoneNumber != 'No phone number' && mounted) { _checkIfBlocked(); } - Navigator.of(context).pop(); + + if (mounted) { + Navigator.of(context).pop(); + } } void _launchPhoneDialer(String phoneNumber) async { @@ -119,34 +133,38 @@ class _ContactModalState extends State { ), ); - if (shouldDelete) { + if (shouldDelete && mounted) { try { // Delete the contact await FlutterContacts.deleteContact(widget.contact); // Show success message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), + ); - // Close the modal - Navigator.of(context).pop(); + // Close the modal + Navigator.of(context).pop(); + } } catch (e) { // Handle errors and show a failure message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to delete ${widget.contact.displayName}: $e')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to delete ${widget.contact.displayName}: $e')), + ); + } } } } void _shareContactAsQRCode() { // Use the ContactService to show the QR code for the contact's vCard - ContactService().showContactQRCodeDialog(context, widget.contact); + _contactService.showContactQRCodeDialog(context, widget.contact); } @override @@ -155,6 +173,8 @@ class _ContactModalState extends State { ? _obfuscateService.obfuscateData(widget.contact.emails.first.address) : 'No email'; + final avatarColor = generateColorFromName(widget.contact.displayName); + return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( @@ -162,6 +182,7 @@ class _ContactModalState extends State { child: GestureDetector( onTap: () {}, child: FractionallySizedBox( + heightFactor: 0.8, child: Container( decoration: BoxDecoration( color: Colors.grey[900], @@ -205,10 +226,6 @@ class _ContactModalState extends State { }, itemBuilder: (BuildContext context) { return [ - const PopupMenuItem( - value: 'show_associated_contacts', - child: Text('Show associated contacts'), - ), const PopupMenuItem( value: 'delete', child: Text('Delete'), @@ -217,15 +234,6 @@ class _ContactModalState extends State { value: 'share', child: Text('Share (via QR code)'), ), - const PopupMenuItem( - value: 'create_shortcut', - child: - Text('Create shortcut (to home screen)'), - ), - const PopupMenuItem( - value: 'set_ringtone', - child: Text('Set ringtone'), - ), ]; }, ), @@ -238,13 +246,28 @@ class _ContactModalState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - ObfuscatedAvatar( - imageBytes: widget.contact.thumbnail, - radius: 50, - backgroundColor: - generateColorFromName(widget.contact.displayName), - fallbackInitial: widget.contact.displayName, - ), + widget.contact.thumbnail != null && widget.contact.thumbnail!.isNotEmpty + ? ClipOval( + child: Image.memory( + widget.contact.thumbnail!, + fit: BoxFit.cover, + width: 100, + height: 100, + ), + ) + : CircleAvatar( + backgroundColor: avatarColor, + radius: 50, + child: Text( + widget.contact.displayName.isNotEmpty + ? widget.contact.displayName[0].toUpperCase() + : '?', + style: TextStyle( + color: darken(avatarColor), + fontSize: 40, + ), + ), + ), const SizedBox(height: 10), Text( _obfuscateService @@ -259,89 +282,97 @@ class _ContactModalState extends State { ), const Divider(color: Colors.grey), // Contact Actions - ListTile( - leading: const Icon(Icons.phone, color: Colors.green), - title: Text( - _obfuscateService.obfuscateData(phoneNumber), - style: const TextStyle(color: Colors.white), - ), - onTap: () async { - if (widget.contact.phones.isNotEmpty) { - await _callService.makeGsmCall(context, - phoneNumber: phoneNumber); - } - }, - ), - ListTile( - leading: const Icon(Icons.message, color: Colors.blue), - title: Text( - _obfuscateService.obfuscateData(phoneNumber), - style: const TextStyle(color: Colors.white), - ), - onTap: () { - if (widget.contact.phones.isNotEmpty) { - _launchSms(phoneNumber); - } - }, - ), - ListTile( - leading: const Icon(Icons.email, color: Colors.orange), - title: Text( - email, - style: const TextStyle(color: Colors.white), - ), - onTap: () { - if (widget.contact.emails.isNotEmpty) { - _launchEmail(email); - } - }, - ), - const Divider(color: Colors.grey), - // Favorite, Edit, and Block/Unblock Buttons - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - // Favorite button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - widget.onToggleFavorite(); + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.phone, color: Colors.green), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), + onTap: () async { + if (widget.contact.phones.isNotEmpty) { + await _callService.makeGsmCall(context, + phoneNumber: phoneNumber); + } }, - icon: Icon(widget.isFavorite - ? Icons.star - : Icons.star_border), - label: Text( - widget.isFavorite ? 'Unfavorite' : 'Favorite'), ), - ), - const SizedBox(height: 10), - // Edit button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () => widget.onEdit(), - icon: const Icon(Icons.edit), - label: const Text('Edit Contact'), + ListTile( + leading: const Icon(Icons.message, color: Colors.blue), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), + onTap: () { + if (widget.contact.phones.isNotEmpty) { + _launchSms(phoneNumber); + } + }, ), - ), - const SizedBox(height: 10), - // Block/Unblock button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _toggleBlockState, - icon: Icon( - isBlocked ? Icons.block : Icons.block_flipped), - label: Text(isBlocked ? 'Unblock' : 'Block'), + ListTile( + leading: const Icon(Icons.email, color: Colors.orange), + title: Text( + email, + style: const TextStyle(color: Colors.white), + ), + onTap: () { + if (widget.contact.emails.isNotEmpty) { + _launchEmail(email); + } + }, ), - ), - ], + const Divider(color: Colors.grey), + // Favorite, Edit, and Block/Unblock Buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + // Favorite button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + widget.onToggleFavorite(); + }, + icon: Icon(widget.isFavorite + ? Icons.star + : Icons.star_border), + label: Text( + widget.isFavorite ? 'Unfavorite' : 'Favorite'), + ), + ), + const SizedBox(height: 10), + // Edit button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => widget.onEdit(), + icon: const Icon(Icons.edit), + label: const Text('Edit Contact'), + ), + ), + const SizedBox(height: 10), + // Block/Unblock button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _toggleBlockState, + icon: Icon( + isBlocked ? Icons.block : Icons.block_flipped), + label: Text(isBlocked ? 'Unblock' : 'Block'), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ), ), ), - const SizedBox(height: 16), ], ), ), diff --git a/dialer/lib/features/contacts/widgets/share_own_qr.dart b/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart similarity index 100% rename from dialer/lib/features/contacts/widgets/share_own_qr.dart rename to dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart diff --git a/dialer/lib/presentation/features/dialer/composition_page.dart b/dialer/lib/presentation/features/dialer/composition_page.dart new file mode 100644 index 0000000..b6c8b30 --- /dev/null +++ b/dialer/lib/presentation/features/dialer/composition_page.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../domain/services/contact_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../../domain/services/call_service.dart'; +import '../contacts/widgets/add_contact_button.dart'; + +class CompositionPage extends StatefulWidget { + const CompositionPage({super.key}); + + @override + _CompositionPageState createState() => _CompositionPageState(); +} + +class _CompositionPageState extends State { + String dialedNumber = ""; + List _allContacts = []; + List _filteredContacts = []; + final ContactService _contactService = ContactService(); + final ObfuscateService _obfuscateService = ObfuscateService(); + final CallService _callService = CallService(); + + @override + void initState() { + super.initState(); + _fetchContacts(); + } + + Future _fetchContacts() async { + _allContacts = await _contactService.fetchContacts(); + _filteredContacts = _allContacts; + setState(() {}); + } + + void _filterContacts() { + setState(() { + _filteredContacts = _allContacts.where((contact) { + final phoneMatch = contact.phones.any((phone) => + phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber)); + final nameMatch = contact.displayName + .toLowerCase() + .contains(dialedNumber.toLowerCase()); + return phoneMatch || nameMatch; + }).toList(); + }); + } + + void _onNumberPress(String number) { + setState(() { + dialedNumber += number; + _filterContacts(); + }); + } + + void _onDeletePress() { + setState(() { + if (dialedNumber.isNotEmpty) { + dialedNumber = dialedNumber.substring(0, dialedNumber.length - 1); + _filterContacts(); + } + }); + } + + void _onClearPress() { + setState(() { + dialedNumber = ""; + _filteredContacts = _allContacts; + }); + } + + void _makeCall(String phoneNumber) async { + try { + await _callService.makeGsmCall(context, phoneNumber: phoneNumber); + setState(() { + dialedNumber = phoneNumber; + }); + } catch (e) { + debugPrint("Error making call: $e"); + } + } + + void _launchSms(String phoneNumber) async { + final uri = Uri(scheme: 'sms', path: phoneNumber); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + debugPrint('Could not send SMS to $phoneNumber'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Column( + children: [ + // Top half: Display contacts matching dialed number + Expanded( + flex: 2, + child: Container( + padding: const EdgeInsets.only( + top: 42.0, left: 16.0, right: 16.0, bottom: 16.0), + color: Colors.black, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView( + children: _filteredContacts.isNotEmpty + ? _filteredContacts.map((contact) { + final phoneNumber = contact.phones.isNotEmpty + ? contact.phones.first.number + : 'No phone number'; + return ListTile( + title: Text( + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.grey), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.phone, + color: Colors.green[300], + size: 20), + onPressed: () { + _makeCall(phoneNumber); + }, + ), + IconButton( + icon: Icon(Icons.message, + color: Colors.blue[300], + size: 20), + onPressed: () { + _launchSms(phoneNumber); + }, + ), + ], + ), + onTap: () { + // Handle contact selection if needed + }, + ); + }).toList() + : [], + ), + ), + ], + ), + ), + ), + + // Bottom half: Dialpad and Dialed number display with erase button + Expanded( + flex: 2, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Display dialed number with erase button + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Align( + alignment: Alignment.center, + child: Text( + dialedNumber, + style: const TextStyle( + fontSize: 24, color: Colors.white), + overflow: TextOverflow.ellipsis, + ), + ), + ), + IconButton( + onPressed: _onClearPress, + icon: const Icon(Icons.backspace, + color: Colors.white), + ), + ], + ), + const SizedBox(height: 10), + + // Dialpad + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildDialButton('1'), + _buildDialButton('2'), + _buildDialButton('3'), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildDialButton('4'), + _buildDialButton('5'), + _buildDialButton('6'), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildDialButton('7'), + _buildDialButton('8'), + _buildDialButton('9'), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildDialButton('*'), + _buildDialButton('0'), + _buildDialButton('#'), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + + // Add Contact Button + Positioned( + bottom: 20.0, + left: 0, + right: 0, + child: Center( + child: AddContactButton(), + ), + ), + + // Top Row with Back Arrow + Positioned( + top: 40.0, + left: 16.0, + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ], + ), + ); + } + + Widget _buildDialButton(String number) { + return ElevatedButton( + onPressed: () => _onNumberPress(number), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + shape: const CircleBorder(), + padding: const EdgeInsets.all(16), + ), + child: Text( + number, + style: const TextStyle( + fontSize: 24, + color: Colors.white, + ), + ), + ); + } +} diff --git a/dialer/lib/presentation/features/favorites/favorites_page.dart b/dialer/lib/presentation/features/favorites/favorites_page.dart new file mode 100644 index 0000000..ca8d39f --- /dev/null +++ b/dialer/lib/presentation/features/favorites/favorites_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import '../contacts/contact_state.dart'; +import '../contacts/widgets/alphabet_scroll_page.dart'; + +class FavoritesPage extends StatelessWidget { + const FavoritesPage({super.key}); + + @override + Widget build(BuildContext context) { + final contactState = ContactState.of(context); + + if (contactState.loading) { + return const Center(child: CircularProgressIndicator()); + } + + final favorites = contactState.favoriteContacts; + + if (favorites.isEmpty) { + return const Center( + child: Text( + 'No favorites yet.\nStar your contacts to add them here.', + style: TextStyle(color: Colors.white60), + textAlign: TextAlign.center, + ), + ); + } + + return Scaffold( + backgroundColor: Colors.black, + body: AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: favorites, + ), + ); + } +} diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart similarity index 98% rename from dialer/lib/features/history/history_page.dart rename to dialer/lib/presentation/features/history/history_page.dart index 2ce20b8..ceeee4b 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -1,17 +1,17 @@ import 'dart:async'; -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/color_darkener.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:dialer/features/contacts/contact_state.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; -import '../../services/block_service.dart'; +import '../../../services/obfuscate_service.dart'; +import '../../../widgets/color_darkener.dart'; +import '../../../widgets/username_color_generator.dart'; +import '../../../services/block_service.dart'; +import '../../../services/call_service.dart'; +import '../contacts/contact_state.dart'; import '../contacts/widgets/contact_modal.dart'; -import '../../services/call_service.dart'; class History { final Contact contact; diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart new file mode 100644 index 0000000..b033971 --- /dev/null +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -0,0 +1,224 @@ +import 'package:flutter/material.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../contacts/contact_page.dart'; +import '../favorites/favorites_page.dart'; +import '../history/history_page.dart'; +import '../dialer/composition_page.dart'; +import '../settings/settings_page.dart'; +import '../voicemail/voicemail_page.dart'; +import '../contacts/contact_state.dart'; +import '../contacts/widgets/contact_modal.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + List _allContacts = []; + List _contactSuggestions = []; + final ObfuscateService _obfuscateService = ObfuscateService(); + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + // Set the TabController length to 4 + _tabController = TabController(length: 4, vsync: this, initialIndex: 1); + _tabController.addListener(_handleTabIndex); + _fetchContacts(); + } + + void _fetchContacts() async { + final contactState = ContactState.of(context); + await contactState.fetchContacts(); + setState(() { + _allContacts = contactState.contacts; + }); + } + + void _clearSearch() { + _searchController.clear(); + _onSearchChanged(''); + } + + void _onSearchChanged(String query) { + setState(() { + if (query.isEmpty) { + _contactSuggestions = List.from(_allContacts); // Reset suggestions + } else { + _contactSuggestions = _allContacts.where((contact) { + return contact.displayName + .toLowerCase() + .contains(query.toLowerCase()); + }).toList(); + } + }); + } + + @override + void dispose() { + _searchController.dispose(); + _tabController.removeListener(_handleTabIndex); + _tabController.dispose(); + super.dispose(); + } + + void _handleTabIndex() { + setState(() {}); + } + + void _toggleFavorite(Contact contact) async { + try { + if (await FlutterContacts.requestPermission()) { + Contact? fullContact = await FlutterContacts.getContact(contact.id, + withProperties: true, + withAccounts: true, + withPhoto: true, + withThumbnail: true); + + if (fullContact != null) { + fullContact.isStarred = !fullContact.isStarred; + await FlutterContacts.updateContact(fullContact); + _fetchContacts(); + } + } + } catch (e) { + debugPrint("Error updating favorite status: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Column( + children: [ + // Persistent Search Bar + Padding( + padding: const EdgeInsets.only( + top: 24.0, + bottom: 10.0, + left: 16.0, + right: 16.0, + ), + child: Row( + children: [ + Expanded( + 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), + ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search contacts', + hintStyle: const TextStyle(color: Colors.grey), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, color: Colors.grey), + onPressed: _clearSearch, + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 16.0), + ), + style: const TextStyle(color: Colors.white), + onChanged: _onSearchChanged, + ), + ), + ), + // 3-dot menu + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: 'settings', + child: Text('Settings'), + ), + ], + onSelected: (String value) { + if (value == 'settings') { + Navigator.pushNamed(context, '/settings'); + } + }, + ), + ], + ), + ), + // Main content with TabBarView + Expanded( + child: Stack( + children: [ + TabBarView( + controller: _tabController, + children: const [ + FavoritesPage(), + HistoryPage(), + ContactPage(), + VoicemailPage(), + ], + ), + Positioned( + right: 20, + bottom: 20, + child: FloatingActionButton( + onPressed: () { + Navigator.pushNamed(context, '/composition'); + }, + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(45), + ), + child: const Icon(Icons.dialpad, color: Colors.white), + ), + ), + ], + ), + ), + ], + ), + bottomNavigationBar: Container( + color: Colors.black, + child: TabBar( + controller: _tabController, + tabs: [ + Tab( + icon: Icon(_tabController.index == 0 + ? Icons.star + : Icons.star_border)), + Tab( + icon: Icon(_tabController.index == 1 + ? Icons.access_time_filled + : Icons.access_time_outlined)), + Tab( + icon: Icon(_tabController.index == 2 + ? Icons.contacts + : Icons.contacts_outlined)), + Tab( + icon: Icon(_tabController.index == 3 + ? Icons.voicemail + : Icons.voicemail_outlined), + ), + ], + labelColor: Colors.white, + unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Colors.white, + ), + ), + ); + } +} diff --git a/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart new file mode 100644 index 0000000..388f49e --- /dev/null +++ b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import '../../../../domain/services/block_service.dart'; + +class BlockedNumbersPage extends StatefulWidget { + const BlockedNumbersPage({super.key}); + + @override + _BlockedNumbersPageState createState() => _BlockedNumbersPageState(); +} + +class _BlockedNumbersPageState extends State { + List _blockedNumbers = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadBlockedNumbers(); + } + + Future _loadBlockedNumbers() async { + final numbers = await BlockService().getBlockedNumbers(); + setState(() { + _blockedNumbers = numbers; + _loading = false; + }); + } + + Future _removeBlockedNumber(String number) async { + await BlockService().unblockNumber(number); + await _loadBlockedNumbers(); + } + + Future _addBlockedNumber(String number) async { + await BlockService().blockNumber(number); + await _loadBlockedNumbers(); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return const Scaffold( + backgroundColor: Colors.black, + body: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Blocked Numbers'), + ), + body: _blockedNumbers.isEmpty + ? const Center( + child: Text( + 'No blocked numbers', + style: TextStyle(color: Colors.white70), + ), + ) + : ListView.builder( + itemCount: _blockedNumbers.length, + itemBuilder: (context, index) { + return ListTile( + title: Text( + _blockedNumbers[index], + style: const TextStyle(color: Colors.white), + ), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeBlockedNumber(_blockedNumbers[index]), + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + final TextEditingController controller = TextEditingController(); + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Block a Number', style: TextStyle(color: Colors.white)), + content: TextField( + controller: controller, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + hintText: 'Enter phone number', + hintStyle: TextStyle(color: Colors.grey), + ), + keyboardType: TextInputType.phone, + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: const Text('Block'), + onPressed: () { + if (controller.text.trim().isNotEmpty) { + _addBlockedNumber(controller.text.trim()); + Navigator.of(context).pop(); + } + }, + ), + ], + ); + }, + ); + }, + ), + ); + } +} diff --git a/dialer/lib/features/settings/call/settingsCall.dart b/dialer/lib/presentation/features/settings/call/settingsCall.dart similarity index 100% rename from dialer/lib/features/settings/call/settingsCall.dart rename to dialer/lib/presentation/features/settings/call/settingsCall.dart diff --git a/dialer/lib/presentation/features/settings/call/settings_call.dart b/dialer/lib/presentation/features/settings/call/settings_call.dart new file mode 100644 index 0000000..09928ab --- /dev/null +++ b/dialer/lib/presentation/features/settings/call/settings_call.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +class SettingsCallPage extends StatefulWidget { + const SettingsCallPage({super.key}); + + @override + _SettingsCallPageState createState() => _SettingsCallPageState(); +} + +class _SettingsCallPageState extends State { + bool _enableVoicemail = true; + bool _enableCallRecording = false; + String _ringtone = 'Default'; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Calling settings'), + ), + body: ListView( + children: [ + SwitchListTile( + title: const Text('Enable Voicemail', style: TextStyle(color: Colors.white)), + value: _enableVoicemail, + onChanged: (bool value) { + setState(() { + _enableVoicemail = value; + }); + }, + ), + SwitchListTile( + title: const Text('Enable call Recording', style: TextStyle(color: Colors.white)), + value: _enableCallRecording, + onChanged: (bool value) { + setState(() { + _enableCallRecording = value; + }); + }, + ), + ListTile( + title: const Text('Ringtone', style: TextStyle(color: Colors.white)), + subtitle: Text(_ringtone, style: const TextStyle(color: Colors.grey)), + trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white), + onTap: () { + _selectRingtone(context); + }, + ), + ], + ), + ); + } + + void _selectRingtone(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select Ringtone'), + children: [ + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, 'Default'); + }, + child: const Text('Default'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, 'Classic'); + }, + child: const Text('Classic'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, 'Beep'); + }, + child: const Text('Beep'), + ), + // ...existing options... + ], + ); + }, + ).then((value) { + if (value != null) { + setState(() { + _ringtone = value; + }); + } + }); + } +} diff --git a/dialer/lib/presentation/features/settings/call/settings_call_page.dart b/dialer/lib/presentation/features/settings/call/settings_call_page.dart new file mode 100644 index 0000000..5ad4de4 --- /dev/null +++ b/dialer/lib/presentation/features/settings/call/settings_call_page.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +/// Calling settings configuration page +class SettingsCallPage extends StatelessWidget { + const SettingsCallPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Call Settings'), + ), + body: ListView( + children: const [ + ListTile( + title: Text('Call Forwarding', style: TextStyle(color: Colors.white)), + subtitle: Text('Manage call forwarding options', style: TextStyle(color: Colors.grey)), + ), + ListTile( + title: Text('Call Waiting', style: TextStyle(color: Colors.white)), + subtitle: Text('Enable or disable call waiting', style: TextStyle(color: Colors.grey)), + ), + ListTile( + title: Text('Caller ID', style: TextStyle(color: Colors.white)), + subtitle: Text('Manage your caller ID settings', style: TextStyle(color: Colors.grey)), + ), + ], + ), + ); + } +} diff --git a/dialer/lib/presentation/features/settings/cryptography/key_management.dart b/dialer/lib/presentation/features/settings/cryptography/key_management.dart new file mode 100644 index 0000000..ba35987 --- /dev/null +++ b/dialer/lib/presentation/features/settings/cryptography/key_management.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import '../../../../domain/services/cryptography/asymmetric_crypto_service.dart'; +import 'package:provider/provider.dart'; + +/// Cryptographic key management page +class ManageKeysPage extends StatefulWidget { + @override + _ManageKeysPageState createState() => _ManageKeysPageState(); +} + +class _ManageKeysPageState extends State { + String? publicKey; + bool _isLoading = false; + bool _hasError = false; + String _errorMessage = ''; + + @override + void initState() { + super.initState(); + _loadPublicKey(); + } + + Future _loadPublicKey() async { + setState(() { + _isLoading = true; + _hasError = false; + }); + + try { + final cryptoService = context.read(); + final key = await cryptoService.getPublicKeyPem(); + + if (mounted) { + setState(() { + publicKey = key; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Failed to load public key: $e'; + _isLoading = false; + }); + } + } + } + + Future _regenerateKeyPair() async { + setState(() { + _isLoading = true; + _hasError = false; + }); + + try { + final cryptoService = context.read(); + await cryptoService.generateAndStoreKeyPair(); + await _loadPublicKey(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('New key pair generated!')), + ); + } + } catch (e) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Failed to generate key pair: $e'; + _isLoading = false; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error generating key pair: $e')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Key Management'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Your Public Key', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(8), + ), + child: _hasError + ? Text( + _errorMessage, + style: const TextStyle( + color: Colors.red, + fontFamily: 'monospace', + fontSize: 12, + ), + ) + : Text( + publicKey ?? 'No key available', + style: const TextStyle( + color: Colors.white70, + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _regenerateKeyPair, + child: const Text('Generate New Key Pair'), + ), + if (_hasError) ...[ + const SizedBox(height: 16), + const Text( + 'Note: Encryption features may be limited due to error.', + style: TextStyle(color: Colors.yellow), + ), + ], + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/dialer/lib/features/settings/settings.dart b/dialer/lib/presentation/features/settings/settings.dart similarity index 86% rename from dialer/lib/features/settings/settings.dart rename to dialer/lib/presentation/features/settings/settings.dart index 410cc7f..d6f1808 100644 --- a/dialer/lib/features/settings/settings.dart +++ b/dialer/lib/presentation/features/settings/settings.dart @@ -1,10 +1,7 @@ -// settings.dart - import 'package:flutter/material.dart'; -import 'package:dialer/features/settings/call/settingsCall.dart'; -// import 'package:dialer/features/settings/cryptography/'; -import 'package:dialer/features/settings/blocked/settings_blocked.dart'; -import 'cryptography/key_management.dart'; +import 'package:dialer/presentation/features/settings/call/settings_call.dart'; +import 'package:dialer/presentation/features/settings/cryptography/key_management.dart'; +import 'package:dialer/presentation/features/settings/blocked/settings_blocked.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); diff --git a/dialer/lib/presentation/features/settings/settings_page.dart b/dialer/lib/presentation/features/settings/settings_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/dialer/lib/features/voicemail/voicemail_page.dart b/dialer/lib/presentation/features/voicemail/voicemail_page.dart similarity index 97% rename from dialer/lib/features/voicemail/voicemail_page.dart rename to dialer/lib/presentation/features/voicemail/voicemail_page.dart index 1fadac8..1fea74d 100644 --- a/dialer/lib/features/voicemail/voicemail_page.dart +++ b/dialer/lib/presentation/features/voicemail/voicemail_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; class VoicemailPage extends StatefulWidget { - const VoicemailPage({Key? key}) : super(key: key); + const VoicemailPage({super.key}); @override State createState() => _VoicemailPageState(); @@ -14,6 +14,7 @@ class _VoicemailPageState extends State { Duration _duration = Duration.zero; Duration _position = Duration.zero; late AudioPlayer _audioPlayer; + bool _loading = false; @override void initState() { @@ -50,12 +51,12 @@ class _VoicemailPageState extends State { @override Widget build(BuildContext context) { + if (_loading) { + return const Center(child: CircularProgressIndicator()); + } + return Scaffold( backgroundColor: Colors.black, - // appBar: AppBar( - // // title: const Text('Voicemail'), - // backgroundColor: Colors.black, - // ), body: ListView( children: [ GestureDetector( diff --git a/dialer/lib/services/block_service.dart b/dialer/lib/services/block_service.dart index d8aaaf0..5ec0cb7 100644 --- a/dialer/lib/services/block_service.dart +++ b/dialer/lib/services/block_service.dart @@ -1,52 +1,78 @@ +import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// Service for managing blocked phone numbers class BlockService { - static final BlockService _instance = BlockService._internal(); + static const String _blockedNumbersKey = 'blocked_numbers'; + // Private constructor + BlockService._privateConstructor(); + + // Singleton instance + static final BlockService _instance = BlockService._privateConstructor(); + + // Factory constructor to return the same instance factory BlockService() { return _instance; } - BlockService._internal(); - - // Function to add a number to the blocked list - Future blockNumber(String number) async { - if (number.isEmpty) return; - - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - - if (!blockedNumbers.contains(number)) { - blockedNumbers.add(number); - await prefs.setStringList('blockedNumbers', blockedNumbers); - print('$number has been blocked'); - } else { - print('$number is already blocked'); + /// Block a phone number + Future blockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + // Don't add if already blocked + if (blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.add(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error blocking number: $e'); + return false; } } - // Function to remove a number from the blocked list - Future unblockNumber(String number) async { - if (number.isEmpty) return; - - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - - if (blockedNumbers.contains(number)) { - blockedNumbers.remove(number); - await prefs.setStringList('blockedNumbers', blockedNumbers); - print('$number has been unblocked'); - } else { - print('$number is not blocked'); + /// Unblock a phone number + Future unblockNumber(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + + if (!blockedNumbers.contains(phoneNumber)) { + return true; + } + + blockedNumbers.remove(phoneNumber); + return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); + } catch (e) { + debugPrint('Error unblocking number: $e'); + return false; } } - // Check if a number is blocked - Future isNumberBlocked(String number) async { - if (number.isEmpty) return false; + /// Check if a number is blocked + Future isNumberBlocked(String phoneNumber) async { + try { + final prefs = await SharedPreferences.getInstance(); + final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; + return blockedNumbers.contains(phoneNumber); + } catch (e) { + debugPrint('Error checking if number is blocked: $e'); + return false; + } + } - final prefs = await SharedPreferences.getInstance(); - List blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; - return blockedNumbers.contains(number); + /// Get all blocked numbers + Future> getBlockedNumbers() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(_blockedNumbersKey) ?? []; + } catch (e) { + debugPrint('Error getting blocked numbers: $e'); + return []; + } } } diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index d42326e..903a13c 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,7 +1,7 @@ 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 '../presentation/features/call/call_page.dart'; +import '../presentation/features/call/incoming_call_page.dart'; // Import the new page class CallService { static const MethodChannel _channel = MethodChannel('call_service'); diff --git a/dialer/lib/services/contact_service.dart b/dialer/lib/services/contact_service.dart index ef00185..d961100 100644 --- a/dialer/lib/services/contact_service.dart +++ b/dialer/lib/services/contact_service.dart @@ -2,48 +2,83 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:qr_flutter/qr_flutter.dart'; -// Service to manage contact-related operations class ContactService { Future> fetchContacts() async { if (await FlutterContacts.requestPermission()) { - return await FlutterContacts.getContacts( - withProperties: true, - withThumbnail: true, - withAccounts: true, - withGroups: true, - withPhoto: true); + List contacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return contacts; + } else { + // Permission denied + return []; } - return []; } Future> fetchFavoriteContacts() async { - List contacts = await fetchContacts(); - return contacts.where((contact) => contact.isStarred).toList(); + if (await FlutterContacts.requestPermission()) { + // Get all contacts and filter for favorites + List allContacts = await FlutterContacts.getContacts( + withProperties: true, + withThumbnail: true, + ); + return allContacts.where((c) => c.isStarred).toList(); + } else { + // Permission denied + return []; + } } - Future addNewContact(Contact contact) async { - await FlutterContacts.insertContact(contact); + Future addNewContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + try { + return await FlutterContacts.insertContact(contact); + } catch (e) { + debugPrint('Error adding contact: $e'); + return null; + } + } + return null; } - // Function to show an AlertDialog with a QR code for the contact's vCard void showContactQRCodeDialog(BuildContext context, Contact contact) { + final String vCard = contact.toVCard(); + showDialog( - barrierColor: Colors.white24, context: context, - barrierDismissible: true, builder: (BuildContext context) { return AlertDialog( - backgroundColor: Colors.black, - content: SizedBox( - width: 200, - height: 220, - child: QrImageView( - data: contact.toVCard(), // Generate vCard QR code - version: QrVersions.auto, - backgroundColor: Colors.white, // Make sure QR code is visible on black background - size: 200.0, - ), + backgroundColor: Colors.grey[900], + title: Text( + 'QR Code for ${contact.displayName}', + style: const TextStyle(color: Colors.white), ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.all(16.0), + child: QrImageView( + data: vCard, + version: QrVersions.auto, + size: 200.0, + ), + ), + const SizedBox(height: 16.0), + const Text( + 'Scan this code to add this contact', + style: TextStyle(color: Colors.white70), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], ); }, ); diff --git a/dialer/lib/services/qr/qr_scanner.dart b/dialer/lib/services/qr/qr_scanner.dart new file mode 100644 index 0000000..df405f6 --- /dev/null +++ b/dialer/lib/services/qr/qr_scanner.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class QRCodeScannerScreen extends StatefulWidget { + const QRCodeScannerScreen({super.key}); + + @override + _QRCodeScannerScreenState createState() => _QRCodeScannerScreenState(); +} + +class _QRCodeScannerScreenState extends State { + MobileScannerController cameraController = MobileScannerController(); + bool _flashEnabled = false; + + @override + void dispose() { + cameraController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan QR Code'), + actions: [ + IconButton( + icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off), + onPressed: () { + setState(() { + _flashEnabled = !_flashEnabled; + cameraController.toggleTorch(); + }); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () => cameraController.switchCamera(), + ), + ], + ), + body: MobileScanner( + controller: cameraController, + onDetect: (capture) { + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + // Return the first barcode value + final String? code = barcodes.first.rawValue; + if (code != null) { + Navigator.pop(context, code); + } + } + }, + ), + ); + } +} diff --git a/dialer/lib/widgets/color_darkener.dart b/dialer/lib/widgets/color_darkener.dart index 8442304..be581cd 100644 --- a/dialer/lib/widgets/color_darkener.dart +++ b/dialer/lib/widgets/color_darkener.dart @@ -1,10 +1,21 @@ import 'package:flutter/material.dart'; -Color darken(Color color, [double amount = .1]) { +/// Darkens a color by a given percentage +Color darken(Color color, [double amount = 0.3]) { assert(amount >= 0 && amount <= 1); - + final hsl = HSLColor.fromColor(color); - final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return darkened.toColor(); +} - return hslDark.toColor(); +/// Lightens a color by a given percentage +Color lighten(Color color, [double amount = 0.3]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(color); + final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return lightened.toColor(); } \ No newline at end of file diff --git a/dialer/lib/widgets/username_color_generator.dart b/dialer/lib/widgets/username_color_generator.dart index 5686a48..480cef4 100644 --- a/dialer/lib/widgets/username_color_generator.dart +++ b/dialer/lib/widgets/username_color_generator.dart @@ -1,12 +1,17 @@ -import 'dart:math'; import 'package:flutter/material.dart'; +/// Generates a deterministic color from a string input Color generateColorFromName(String name) { - final random = Random(name.hashCode); - return Color.fromARGB( - 255, - random.nextInt(256), - random.nextInt(256), - random.nextInt(256), - ); + if (name.isEmpty) return Colors.grey; + + // Use the hashCode of the name to generate a consistent color + int hash = name.hashCode; + + // Use the hash to generate RGB values + final r = (hash & 0xFF0000) >> 16; + final g = (hash & 0x00FF00) >> 8; + final b = hash & 0x0000FF; + + // Create a color with these RGB values + return Color.fromARGB(255, r, g, b); } -- 2.45.2 From cef7a27e882bea6fd0de13bcc2dee02111cbf125 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Mon, 31 Mar 2025 00:50:35 +0200 Subject: [PATCH 02/17] Updated key management to match current version --- .../settings/cryptography/key_management.dart | 209 +++++++++--------- 1 file changed, 104 insertions(+), 105 deletions(-) diff --git a/dialer/lib/presentation/features/settings/cryptography/key_management.dart b/dialer/lib/presentation/features/settings/cryptography/key_management.dart index ba35987..f766ebc 100644 --- a/dialer/lib/presentation/features/settings/cryptography/key_management.dart +++ b/dialer/lib/presentation/features/settings/cryptography/key_management.dart @@ -1,145 +1,144 @@ import 'package:flutter/material.dart'; -import '../../../../domain/services/cryptography/asymmetric_crypto_service.dart'; -import 'package:provider/provider.dart'; +import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; -/// Cryptographic key management page class ManageKeysPage extends StatefulWidget { + const ManageKeysPage({Key? key}) : super(key: key); + @override _ManageKeysPageState createState() => _ManageKeysPageState(); } class _ManageKeysPageState extends State { - String? publicKey; + final AsymmetricCryptoService _cryptoService = AsymmetricCryptoService(); + List> _keys = []; bool _isLoading = false; - bool _hasError = false; - String _errorMessage = ''; @override void initState() { super.initState(); - _loadPublicKey(); + _loadKeys(); } - Future _loadPublicKey() async { + Future _loadKeys() async { setState(() { _isLoading = true; - _hasError = false; }); - try { - final cryptoService = context.read(); - final key = await cryptoService.getPublicKeyPem(); - - if (mounted) { - setState(() { - publicKey = key; - _isLoading = false; - }); - } + List> keys = await _cryptoService.getAllKeys(); + setState(() { + _keys = keys; + }); } catch (e) { - if (mounted) { - setState(() { - _hasError = true; - _errorMessage = 'Failed to load public key: $e'; - _isLoading = false; - }); - } + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error loading keys: $e'))); + } finally { + setState(() { + _isLoading = false; + }); } } - Future _regenerateKeyPair() async { + Future _generateKey() async { setState(() { _isLoading = true; - _hasError = false; }); - try { - final cryptoService = context.read(); - await cryptoService.generateAndStoreKeyPair(); - await _loadPublicKey(); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('New key pair generated!')), - ); - } + await _cryptoService.generateKeyPair(); + await _loadKeys(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Key generated successfully'))); } catch (e) { - if (mounted) { - setState(() { - _hasError = true; - _errorMessage = 'Failed to generate key pair: $e'; - _isLoading = false; - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error generating key pair: $e')), - ); - } + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error generating key: $e'))); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _deleteKey(String alias) async { + setState(() { + _isLoading = true; + }); + try { + await _cryptoService.deleteKeyPair(alias); + await _loadKeys(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Key deleted successfully'))); + } catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error deleting key: $e'))); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _viewPublicKey(String alias) async { + try { + final publicKey = await _cryptoService.getPublicKey(alias); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Public Key'), + content: SingleChildScrollView(child: Text(publicKey)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); + } catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error retrieving public key: $e'))); } } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.black, appBar: AppBar( - title: const Text('Key Management'), + title: const Text('Manage Keys'), ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Your Public Key', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(8), - ), - child: _hasError - ? Text( - _errorMessage, - style: const TextStyle( - color: Colors.red, - fontFamily: 'monospace', - fontSize: 12, - ), - ) - : Text( - publicKey ?? 'No key available', - style: const TextStyle( - color: Colors.white70, - fontFamily: 'monospace', - fontSize: 12, - ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _keys.isEmpty + ? const Center(child: Text('No keys found')) + : ListView.builder( + itemCount: _keys.length, + itemBuilder: (context, index) { + final keyData = _keys[index]; + return ListTile( + title: Text(keyData['label'] ?? 'No label'), + subtitle: Text(keyData['alias'] ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: 'View Public Key', + onPressed: () => _viewPublicKey(keyData['alias']), ), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: _regenerateKeyPair, - child: const Text('Generate New Key Pair'), - ), - if (_hasError) ...[ - const SizedBox(height: 16), - const Text( - 'Note: Encryption features may be limited due to error.', - style: TextStyle(color: Colors.yellow), - ), - ], - ], - ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Delete Key', + onPressed: () => _deleteKey(keyData['alias']), + ), + ], + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: _generateKey, + child: const Icon(Icons.add), + tooltip: 'Generate New Key', ), ); } -- 2.45.2 From 62d48dc0843651df9050481fbc22df60a1266b55 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:13:32 +0200 Subject: [PATCH 03/17] refactor: improve contact fetching and initialization logic for better performance and clarity --- .../features/contacts/contact_state.dart | 50 ++++++++++++++++--- .../presentation/features/home/home_page.dart | 29 +++++++++-- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/contact_state.dart b/dialer/lib/presentation/features/contacts/contact_state.dart index 5d6ca9b..f555d66 100644 --- a/dialer/lib/presentation/features/contacts/contact_state.dart +++ b/dialer/lib/presentation/features/contacts/contact_state.dart @@ -35,10 +35,20 @@ class _ContactStateState extends State { @override void initState() { super.initState(); - fetchContacts(); // Fetch all contacts by default + _initializeContacts(); // Rename to make it clear this is initialization FlutterContacts.addListener(_onContactChange); } + // Private method to initialize contacts without setState during build + Future _initializeContacts() async { + try { + List contacts = await _contactService.fetchContacts(); + _processContactsInitial(contacts); + } catch (e) { + debugPrint('Error fetching contacts: $e'); + } + } + void _onContactChange() async { await fetchContacts(); } @@ -49,14 +59,16 @@ class _ContactStateState extends State { super.dispose(); } - // Fetch all contacts + // Fetch all contacts - public method that can be called after build Future fetchContacts() async { if (!mounted) return; setState(() => _loading = true); try { List contacts = await _contactService.fetchContacts(); - _processContacts(contacts); + if (mounted) { + _processContacts(contacts); + } } catch (e) { debugPrint('Error fetching contacts: $e'); } finally { @@ -85,6 +97,34 @@ class _ContactStateState extends State { } } + // Process contacts without setState for initial loading + void _processContactsInitial(List contacts) { + if (!mounted) return; + + _selfContact = contacts.firstWhere( + (contact) => contact.displayName.toLowerCase() == "user", + orElse: () => Contact(), + ); + + if (_selfContact!.phones.isEmpty) { + _selfContact = null; + } + + contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList(); + contacts.sort((a, b) => a.displayName.compareTo(b.displayName)); + + _allContacts = contacts; + _favoriteContacts = contacts.where((contact) => contact.isStarred).toList(); + _loading = false; + + // Force a rebuild after initialization is complete + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + } + void _processContacts(List contacts) { if (!mounted) return; @@ -94,7 +134,6 @@ class _ContactStateState extends State { ); if (_selfContact!.phones.isEmpty) { - debugPrint("Self contact has no phone numbers"); _selfContact = null; } @@ -103,8 +142,7 @@ class _ContactStateState extends State { setState(() { _allContacts = contacts; - _favoriteContacts = - contacts.where((contact) => contact.isStarred).toList(); + _favoriteContacts = contacts.where((contact) => contact.isStarred).toList(); }); } diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index b033971..16f9487 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -24,22 +24,41 @@ class _MyHomePageState extends State List _contactSuggestions = []; final ObfuscateService _obfuscateService = ObfuscateService(); final TextEditingController _searchController = TextEditingController(); + bool _isInitialized = false; @override void initState() { super.initState(); - // Set the TabController length to 4 _tabController = TabController(length: 4, vsync: this, initialIndex: 1); _tabController.addListener(_handleTabIndex); - _fetchContacts(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_isInitialized) { + // Use a post-frame callback to avoid setState during build + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchContacts(); + }); + _isInitialized = true; + } } void _fetchContacts() async { final contactState = ContactState.of(context); + // Wait for initial load to finish if it hasn't already + if (contactState.loading) { + await Future.delayed(const Duration(milliseconds: 100)); + } + // Then explicitly fetch contacts (which will call setState safely) await contactState.fetchContacts(); - setState(() { - _allContacts = contactState.contacts; - }); + if (mounted) { + setState(() { + _allContacts = contactState.contacts; + _contactSuggestions = _allContacts; + }); + } } void _clearSearch() { -- 2.45.2 From 91a739a0cd102ed9b4967b0554e17304b8b23d0a Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:54:41 +0200 Subject: [PATCH 04/17] refactor: remove old main file and enhance blocked numbers settings with local storage preferences --- dialer/lib/main_old.dart | 84 ------- .../settings/blocked/settings_blocked.dart | 213 +++++++++++------- 2 files changed, 132 insertions(+), 165 deletions(-) delete mode 100644 dialer/lib/main_old.dart diff --git a/dialer/lib/main_old.dart b/dialer/lib/main_old.dart deleted file mode 100644 index 5175346..0000000 --- a/dialer/lib/main_old.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:provider/provider.dart'; - -import 'services/call_service.dart'; -import 'services/cryptography/asymmetric_crypto_service.dart'; -import 'globals.dart' as globals; -import 'presentation/features/home/home_page.dart'; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // Check for stealth mode - const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); - globals.isStealthMode = stealthFlag.toLowerCase() == 'true'; - - // Initialize cryptography service with error handling - final AsymmetricCryptoService cryptoService = AsymmetricCryptoService(); - try { - await cryptoService.initializeDefaultKeyPair(); - } catch (e) { - debugPrint('Error initializing cryptography: $e'); - // Continue app initialization even if crypto fails - } - - // Request permissions before running the app - await _requestPermissions(); - - // Initialize call service - CallService(); - - runApp( - MultiProvider( - providers: [ - Provider( - create: (_) => cryptoService, - ), - ], - child: const DialerApp(), - ), - ); -} - -Future _requestPermissions() async { - try { - Map statuses = await [ - Permission.phone, - Permission.contacts, - Permission.microphone, - ].request(); - - if (statuses.values.every((status) => status.isGranted)) { - debugPrint("All required permissions granted"); - const channel = MethodChannel('call_service'); - await channel.invokeMethod('permissionsGranted'); - } else { - debugPrint("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}"); - } - } catch (e) { - debugPrint("Error requesting permissions: $e"); - } -} - -class DialerApp extends StatelessWidget { - const DialerApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Dialer App', - navigatorKey: CallService.navigatorKey, - theme: ThemeData.dark().copyWith( - scaffoldBackgroundColor: Colors.black, - primaryColor: Colors.blue, - appBarTheme: const AppBarTheme( - backgroundColor: Colors.black, - elevation: 0, - ), - ), - home: const MyHomePage(), - ); - } -} diff --git a/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart index 388f49e..77c3a85 100644 --- a/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart +++ b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../../../domain/services/block_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class BlockedNumbersPage extends StatefulWidget { const BlockedNumbersPage({super.key}); @@ -9,108 +9,159 @@ class BlockedNumbersPage extends StatefulWidget { } class _BlockedNumbersPageState extends State { - List _blockedNumbers = []; - bool _loading = true; + bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers + List _blockedNumbers = []; // List of blocked numbers + final TextEditingController _numberController = TextEditingController(); @override void initState() { super.initState(); - _loadBlockedNumbers(); + _loadPreferences(); // Load data on initialization } - Future _loadBlockedNumbers() async { - final numbers = await BlockService().getBlockedNumbers(); + // Load preferences from local storage + Future _loadPreferences() async { + final prefs = await SharedPreferences.getInstance(); setState(() { - _blockedNumbers = numbers; - _loading = false; + _blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false; + _blockedNumbers = prefs.getStringList('blockedNumbers') ?? []; }); } - Future _removeBlockedNumber(String number) async { - await BlockService().unblockNumber(number); - await _loadBlockedNumbers(); - } - - Future _addBlockedNumber(String number) async { - await BlockService().blockNumber(number); - await _loadBlockedNumbers(); + // Save preferences to local storage + Future _savePreferences() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('blockUnknownNumbers', _blockUnknownNumbers); + await prefs.setStringList('blockedNumbers', _blockedNumbers); } @override Widget build(BuildContext context) { - if (_loading) { - return const Scaffold( - backgroundColor: Colors.black, - body: Center(child: CircularProgressIndicator()), - ); - } - return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('Blocked Numbers'), ), - body: _blockedNumbers.isEmpty - ? const Center( - child: Text( - 'No blocked numbers', - style: TextStyle(color: Colors.white70), - ), - ) - : ListView.builder( - itemCount: _blockedNumbers.length, - itemBuilder: (context, index) { - return ListTile( - title: Text( - _blockedNumbers[index], - style: const TextStyle(color: Colors.white), - ), - trailing: IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () => _removeBlockedNumber(_blockedNumbers[index]), - ), - ); - }, + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + SwitchListTile( + title: const Text( + 'Block Unknown Numbers', + style: TextStyle(color: Colors.white), ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - final TextEditingController controller = TextEditingController(); - return AlertDialog( - backgroundColor: Colors.grey[900], - title: const Text('Block a Number', style: TextStyle(color: Colors.white)), - content: TextField( - controller: controller, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - hintText: 'Enter phone number', - hintStyle: TextStyle(color: Colors.grey), - ), - keyboardType: TextInputType.phone, - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - child: const Text('Block'), - onPressed: () { - if (controller.text.trim().isNotEmpty) { - _addBlockedNumber(controller.text.trim()); - Navigator.of(context).pop(); - } - }, - ), - ], - ); + value: _blockUnknownNumbers, + onChanged: (bool value) { + setState(() { + _blockUnknownNumbers = value; + _savePreferences(); // Save the state to local storage + }); }, - ); - }, + ), + const SizedBox(height: 16), + ListTile( + title: const Text( + 'Blocked Numbers', + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + ), + subtitle: _blockedNumbers.isEmpty + ? const Text( + 'No blocked numbers', + style: TextStyle(color: Colors.grey), + ) + : null, + ), + ..._blockedNumbers.map( + (number) => ListTile( + title: Text( + number, + style: const TextStyle(color: Colors.white), + ), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _unblockNumber(number), + ), + ), + ), + const Divider(color: Colors.grey), + ListTile( + title: const Text( + 'Block a Number', + style: TextStyle(color: Colors.white), + ), + trailing: const Icon(Icons.add, color: Colors.white), + onTap: () => _showBlockNumberDialog(), + ), + ], ), ); } + + // Function to block a number + void _blockNumber(String number) { + if (number.isNotEmpty && !_blockedNumbers.contains(number)) { + setState(() { + _blockedNumbers.add(number); + _savePreferences(); // Save the updated list + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$number has been blocked')), + ); + } + } + + // Function to unblock a number + void _unblockNumber(String number) { + setState(() { + _blockedNumbers.remove(number); + _savePreferences(); // Save the updated list + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$number has been unblocked')), + ); + } + + // Dialog for blocking a new number + void _showBlockNumberDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text('Block a Number', style: TextStyle(color: Colors.white)), + content: TextField( + controller: _numberController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: 'Enter number', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(color: Colors.white), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + TextButton( + onPressed: () { + _blockNumber(_numberController.text); + _numberController.clear(); + Navigator.pop(context); + }, + child: const Text('Block', style: TextStyle(color: Colors.red)), + ), + ], + ); + }, + ); + } + + @override + void dispose() { + _numberController.dispose(); + super.dispose(); + } } -- 2.45.2 From 64089f2b3e52e57fcbb453404936f598633b0e24 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:41:43 +0200 Subject: [PATCH 05/17] refactor: update search functionality with SearchAnchor and improve navigation to settings and composition pages --- .../presentation/features/home/home_page.dart | 145 ++++++++++++++---- 1 file changed, 111 insertions(+), 34 deletions(-) diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index 16f9487..f19fa01 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -4,7 +4,7 @@ import '../contacts/contact_page.dart'; import '../favorites/favorites_page.dart'; import '../history/history_page.dart'; import '../dialer/composition_page.dart'; -import '../settings/settings_page.dart'; +import '../settings/settings.dart'; import '../voicemail/voicemail_page.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/contact_modal.dart'; @@ -23,7 +23,7 @@ class _MyHomePageState extends State List _allContacts = []; List _contactSuggestions = []; final ObfuscateService _obfuscateService = ObfuscateService(); - final TextEditingController _searchController = TextEditingController(); + final SearchController _searchController = SearchController(); bool _isInitialized = false; @override @@ -61,11 +61,6 @@ class _MyHomePageState extends State } } - void _clearSearch() { - _searchController.clear(); - _onSearchChanged(''); - } - void _onSearchChanged(String query) { setState(() { if (query.isEmpty) { @@ -132,31 +127,105 @@ class _MyHomePageState extends State child: Row( children: [ Expanded( - 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), - ), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Search contacts', - hintStyle: const TextStyle(color: Colors.grey), - prefixIcon: const Icon(Icons.search, color: Colors.grey), - suffixIcon: _searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, color: Colors.grey), - onPressed: _clearSearch, - ) - : null, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 16.0), - ), - style: const TextStyle(color: Colors.white), - onChanged: _onSearchChanged, - ), + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return GestureDetector( + onTap: () { + controller.openView(); // Open the search view + }, + 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), + ), + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 16.0), + child: Row( + children: [ + const Icon(Icons.search, + color: Colors.grey, size: 24.0), + const SizedBox(width: 8.0), + Text( + controller.text.isEmpty + ? 'Search contacts' + : controller.text, + style: const TextStyle( + color: Colors.grey, fontSize: 16.0), + ), + const Spacer(), + if (controller.text.isNotEmpty) + GestureDetector( + onTap: () { + controller.clear(); + _onSearchChanged(''); + }, + child: const Icon( + Icons.clear, + color: Colors.grey, + size: 24.0, + ), + ), + ], + ), + ), + ); + }, + viewOnChanged: _onSearchChanged, // Update immediately + 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)), + 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, + backgroundColor: Colors.transparent, + builder: (context) { + return ContactModal( + contact: contact, + onEdit: () async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = + await FlutterContacts.openExternalEdit(contact.id); + if (updatedContact != null) { + _fetchContacts(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${contact.displayName} updated successfully!'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Edit canceled or failed.'), + ), + ); + } + } + }, + onToggleFavorite: () => _toggleFavorite(contact), + isFavorite: contact.isStarred, + ); + }, + ); + }, + ); + }).toList(); + }, ), ), // 3-dot menu @@ -170,7 +239,10 @@ class _MyHomePageState extends State ], onSelected: (String value) { if (value == 'settings') { - Navigator.pushNamed(context, '/settings'); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsPage()), + ); } }, ), @@ -195,7 +267,12 @@ class _MyHomePageState extends State bottom: 20, child: FloatingActionButton( onPressed: () { - Navigator.pushNamed(context, '/composition'); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CompositionPage(), + ), + ); }, backgroundColor: Colors.blue, shape: RoundedRectangleBorder( -- 2.45.2 From fd9c469fc125883baa456fb90a2133beaa40b8e7 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:44:37 +0200 Subject: [PATCH 06/17] refactor: remove SettingsCallPage to streamline call settings management --- .../features/settings/call/settingsCall.dart | 92 ------------------- 1 file changed, 92 deletions(-) delete mode 100644 dialer/lib/presentation/features/settings/call/settingsCall.dart diff --git a/dialer/lib/presentation/features/settings/call/settingsCall.dart b/dialer/lib/presentation/features/settings/call/settingsCall.dart deleted file mode 100644 index a052b29..0000000 --- a/dialer/lib/presentation/features/settings/call/settingsCall.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsCallPage extends StatefulWidget { - const SettingsCallPage({super.key}); - - @override - _SettingsCallPageState createState() => _SettingsCallPageState(); -} - -class _SettingsCallPageState extends State { - bool _enableVoicemail = true; - bool _enableCallRecording = false; - String _ringtone = 'Default'; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - title: const Text('Calling settings'), - ), - body: ListView( - children: [ - SwitchListTile( - title: const Text('Enable Voicemail', style: TextStyle(color: Colors.white)), - value: _enableVoicemail, - onChanged: (bool value) { - setState(() { - _enableVoicemail = value; - }); - }, - ), - SwitchListTile( - title: const Text('Enable call Recording', style: TextStyle(color: Colors.white)), - value: _enableCallRecording, - onChanged: (bool value) { - setState(() { - _enableCallRecording = value; - }); - }, - ), - ListTile( - title: const Text('Ringtone', style: TextStyle(color: Colors.white)), - subtitle: Text(_ringtone, style: const TextStyle(color: Colors.grey)), - trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white), - onTap: () { - _selectRingtone(context); - }, - ), - ], - ), - ); - } - - void _selectRingtone(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return SimpleDialog( - title: const Text('Select Ringtone'), - children: [ - SimpleDialogOption( - onPressed: () { - Navigator.pop(context, 'Default'); - }, - child: const Text('Default'), - ), - SimpleDialogOption( - onPressed: () { - Navigator.pop(context, 'Classic'); - }, - child: const Text('Classic'), - ), - SimpleDialogOption( - onPressed: () { - Navigator.pop(context, 'Beep'); - }, - child: const Text('Beep'), - ), - // Add more ringtone options - ], - ); - }, - ).then((value) { - if (value != null) { - setState(() { - _ringtone = value; - }); - } - }); - } -} -- 2.45.2 From 9e04185da1f22d0cef080420ebafa88e257c7fec Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:45:51 +0200 Subject: [PATCH 07/17] refactor: remove unused app_router and app_theme files to clean up project structure --- dialer/lib/config/routes/app_router.dart | 0 dialer/lib/config/themes/app_theme.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dialer/lib/config/routes/app_router.dart delete mode 100644 dialer/lib/config/themes/app_theme.dart diff --git a/dialer/lib/config/routes/app_router.dart b/dialer/lib/config/routes/app_router.dart deleted file mode 100644 index e69de29..0000000 diff --git a/dialer/lib/config/themes/app_theme.dart b/dialer/lib/config/themes/app_theme.dart deleted file mode 100644 index e69de29..0000000 -- 2.45.2 From 8cb206a64033b12c0f291dbdfd415802a4fb4bd0 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:38:32 +0200 Subject: [PATCH 08/17] refactor: optimize contact fetching and processing for improved performance --- .../features/contacts/contact_page.dart | 16 +++++++----- .../features/contacts/contact_state.dart | 24 +++++++++-------- .../presentation/features/home/home_page.dart | 26 ++++++++++++------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index 00b227e..ad9b2b9 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -38,24 +38,28 @@ class _ContactPageState extends State { void _toggleFavorite(Contact contact) async { try { if (await FlutterContacts.requestPermission()) { + // Avoid full contact fetch by using minimal properties Contact? fullContact = await FlutterContacts.getContact(contact.id, withProperties: true, - withAccounts: true, - withPhoto: true, - withThumbnail: true); + withAccounts: false, // Don't need accounts for favorite toggle + withPhoto: false, // Don't need photo for favorite toggle + withThumbnail: false // Don't need thumbnail for favorite toggle + ); if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); + await _refreshContacts(); } - await _refreshContacts(); } else { debugPrint("Could not fetch contact details"); } } catch (e) { debugPrint("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update favorite status'))); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update favorite status'))); + } } } diff --git a/dialer/lib/presentation/features/contacts/contact_state.dart b/dialer/lib/presentation/features/contacts/contact_state.dart index f555d66..2cf192d 100644 --- a/dialer/lib/presentation/features/contacts/contact_state.dart +++ b/dialer/lib/presentation/features/contacts/contact_state.dart @@ -101,6 +101,10 @@ class _ContactStateState extends State { void _processContactsInitial(List contacts) { if (!mounted) return; + // Optimize by doing a single pass through contacts instead of multiple iterations + final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList() + ..sort((a, b) => a.displayName.compareTo(b.displayName)); + _selfContact = contacts.firstWhere( (contact) => contact.displayName.toLowerCase() == "user", orElse: () => Contact(), @@ -109,12 +113,9 @@ class _ContactStateState extends State { if (_selfContact!.phones.isEmpty) { _selfContact = null; } - - contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList(); - contacts.sort((a, b) => a.displayName.compareTo(b.displayName)); - - _allContacts = contacts; - _favoriteContacts = contacts.where((contact) => contact.isStarred).toList(); + + _allContacts = filteredContacts; + _favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList(); _loading = false; // Force a rebuild after initialization is complete @@ -128,6 +129,10 @@ class _ContactStateState extends State { void _processContacts(List contacts) { if (!mounted) return; + // Same optimization as above + final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList() + ..sort((a, b) => a.displayName.compareTo(b.displayName)); + _selfContact = contacts.firstWhere( (contact) => contact.displayName.toLowerCase() == "user", orElse: () => Contact(), @@ -137,12 +142,9 @@ class _ContactStateState extends State { _selfContact = null; } - contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList(); - contacts.sort((a, b) => a.displayName.compareTo(b.displayName)); - setState(() { - _allContacts = contacts; - _favoriteContacts = contacts.where((contact) => contact.isStarred).toList(); + _allContacts = filteredContacts; + _favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList(); }); } diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index f19fa01..fb43fc6 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -62,16 +62,24 @@ class _MyHomePageState extends State } void _onSearchChanged(String query) { - setState(() { - if (query.isEmpty) { + if (query.isEmpty) { + setState(() { _contactSuggestions = List.from(_allContacts); // Reset suggestions - } else { - _contactSuggestions = _allContacts.where((contact) { - return contact.displayName - .toLowerCase() - .contains(query.toLowerCase()); - }).toList(); - } + }); + return; + } + + // Convert query to lowercase once for efficiency + final lowerQuery = query.toLowerCase(); + + // Use where with efficient filter + final filtered = _allContacts.where((contact) { + final name = contact.displayName.toLowerCase(); + return name.contains(lowerQuery); + }).toList(); + + setState(() { + _contactSuggestions = filtered; }); } -- 2.45.2 From f89c5440fca0cd69b6d9c13efc93f092fa5c840e Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:55:21 +0200 Subject: [PATCH 09/17] refactor: introduce LoadingIndicatorWidget and streamline contact page structure --- .../common/widgets/loading_indicator.dart | 10 + .../features/contacts/contact_page.dart | 201 +----------------- .../widgets/alphabet_scroll_page.dart | 26 +-- 3 files changed, 30 insertions(+), 207 deletions(-) create mode 100644 dialer/lib/presentation/common/widgets/loading_indicator.dart diff --git a/dialer/lib/presentation/common/widgets/loading_indicator.dart b/dialer/lib/presentation/common/widgets/loading_indicator.dart new file mode 100644 index 0000000..ecb22c1 --- /dev/null +++ b/dialer/lib/presentation/common/widgets/loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class LoadingIndicatorWidget extends StatelessWidget { + const LoadingIndicatorWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index ad9b2b9..ead0797 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -1,14 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../contacts/contact_state.dart'; +import '../contacts/widgets/alphabet_scroll_page.dart'; +import '../../common/widgets/loading_indicator.dart'; import '../../../domain/services/obfuscate_service.dart'; -import '../../common/widgets/obfuscated_avatar.dart'; -import '../../../core/utils/color_utils.dart'; -import '../../../domain/services/call_service.dart'; -import '../../../domain/services/block_service.dart'; -import 'widgets/contact_modal.dart'; -import 'widgets/alphabet_scroll_page.dart'; class ContactPage extends StatefulWidget { const ContactPage({super.key}); @@ -19,198 +13,17 @@ class ContactPage extends StatefulWidget { class _ContactPageState extends State { final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - int? _expandedIndex; - - Future _refreshContacts() async { - final contactState = ContactState.of(context); - try { - await contactState.fetchContacts(); - } catch (e) { - debugPrint('Error refreshing contacts: $e'); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Failed to refresh contacts'))); - } - } - } - - void _toggleFavorite(Contact contact) async { - try { - if (await FlutterContacts.requestPermission()) { - // Avoid full contact fetch by using minimal properties - Contact? fullContact = await FlutterContacts.getContact(contact.id, - withProperties: true, - withAccounts: false, // Don't need accounts for favorite toggle - withPhoto: false, // Don't need photo for favorite toggle - withThumbnail: false // Don't need thumbnail for favorite toggle - ); - - if (fullContact != null) { - fullContact.isStarred = !fullContact.isStarred; - await FlutterContacts.updateContact(fullContact); - await _refreshContacts(); - } - } else { - debugPrint("Could not fetch contact details"); - } - } catch (e) { - debugPrint("Error updating favorite status: $e"); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update favorite status'))); - } - } - } @override Widget build(BuildContext context) { final contactState = ContactState.of(context); - - if (contactState.loading) { - return const Center(child: CircularProgressIndicator()); - } - - final contacts = contactState.contacts; - - if (contacts.isEmpty) { - return const Center( - child: Text( - 'No contacts found.\nAdd contacts to get started.', - style: TextStyle(color: Colors.white60), - textAlign: TextAlign.center, - ), - ); - } - return Scaffold( - backgroundColor: Colors.black, - body: AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: contacts, - ), - ); - } - - Widget _getCallIcon(Contact contact) { - if (!contact.phones.isNotEmpty) return const SizedBox.shrink(); - return Icon( - contact.isStarred ? Icons.star : Icons.star_border, - color: contact.isStarred ? Colors.amber : Colors.grey, - ); - } - - Future _launchSms(Contact contact) async { - if (!contact.phones.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No phone number available')), - ); - return; - } - - final Uri smsUri = Uri( - scheme: 'sms', - path: contact.phones.first.number, - ); - - if (await canLaunchUrl(smsUri)) { - await launchUrl(smsUri); - } else { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not launch SMS')), - ); - } - } - - void _editContact(Contact contact) async { - if (await FlutterContacts.requestPermission()) { - final updatedContact = await FlutterContacts.openExternalEdit(contact.id); - if (updatedContact != null) { - await _refreshContacts(); - setState(() => _expandedIndex = null); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${contact.displayName} updated successfully!')), - ); - } - } - } - - void _toggleBlock(Contact contact, bool isBlocked) async { - if (!contact.phones.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No phone number to block')), - ); - return; - } - - final phoneNumber = contact.phones.first.number; - if (isBlocked) { - await BlockService().unblockNumber(phoneNumber); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber unblocked')), - ); - } else { - await BlockService().blockNumber(phoneNumber); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$phoneNumber blocked')), - ); - } - setState(() {}); - } - - void _showContactModal(Contact contact) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => ContactModal( - contact: contact, - onEdit: () async { - if (await FlutterContacts.requestPermission()) { - final updatedContact = await FlutterContacts.openExternalEdit(contact.id); - if (updatedContact != null) { - await _refreshContacts(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${contact.displayName} updated successfully!')), - ); - } - } - }, - onToggleFavorite: () => _toggleFavorite(contact), - isFavorite: contact.isStarred, - ), - ); - } - - Widget _buildActionButton({ - required IconData icon, - required String label, - required VoidCallback onPressed, - }) { - return InkWell( - onTap: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: Colors.white), - const SizedBox(height: 4), - Text( - label, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), + body: contactState.loading + ? const Center(child: CircularProgressIndicator()) + : AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: contactState.contacts, ), - ], - ), - ), ); } } diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index 822c36e..d34f3a7 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -1,7 +1,8 @@ -import 'package:dialer/services/obfuscate_service.dart'; -import 'package:dialer/widgets/username_color_generator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import '../../../../domain/services/obfuscate_service.dart'; +import '../../../../core/utils/color_utils.dart'; +import '../../../common/widgets/obfuscated_avatar.dart'; import '../contact_state.dart'; import 'add_contact_button.dart'; import 'contact_modal.dart'; @@ -23,7 +24,6 @@ class AlphabetScrollPage extends StatefulWidget { class _AlphabetScrollPageState extends State { late ScrollController _scrollController; - final ObfuscateService _obfuscateService = ObfuscateService(); @override @@ -43,7 +43,7 @@ class _AlphabetScrollPageState extends State { try { await contactState.fetchContacts(); } catch (e) { - print('Error refreshing contacts: $e'); + debugPrint('Error refreshing contacts: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to refresh contacts')), ); @@ -55,9 +55,9 @@ class _AlphabetScrollPageState extends State { if (await FlutterContacts.requestPermission()) { Contact? fullContact = await FlutterContacts.getContact(contact.id, withProperties: true, - withAccounts: true, - withPhoto: true, - withThumbnail: true); + withAccounts: false, + withPhoto: false, + withThumbnail: false); if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; @@ -65,10 +65,10 @@ class _AlphabetScrollPageState extends State { } await _refreshContacts(); } else { - print("Could not fetch contact details"); + debugPrint("Could not fetch contact details"); } } catch (e) { - print("Error updating favorite status: $e"); + debugPrint("Error updating favorite status: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to update contact favorite status')), ); @@ -139,7 +139,7 @@ class _AlphabetScrollPageState extends State { ? _obfuscateService.obfuscateData(contact.phones.first.number) : 'No phone number'; Color avatarColor = - generateColorFromName(contact.displayName); + generateColorFromName(contact.displayName); return ListTile( leading: ObfuscatedAvatar( imageBytes: contact.thumbnail, @@ -166,8 +166,8 @@ class _AlphabetScrollPageState extends State { onEdit: () async { if (await FlutterContacts.requestPermission()) { final updatedContact = - await FlutterContacts.openExternalEdit( - contact.id); + await FlutterContacts.openExternalEdit( + contact.id); if (updatedContact != null) { await _refreshContacts(); Navigator.of(context).pop(); @@ -183,7 +183,7 @@ class _AlphabetScrollPageState extends State { .showSnackBar( SnackBar( content: - Text('Edit canceled or failed.'), + Text('Edit canceled or failed.'), ), ); } -- 2.45.2 From 612cccc381c5194a6bcbeaa414cc67b2d51896df Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:48:34 +0200 Subject: [PATCH 10/17] Refactor obfuscate service and avatar widget; move to domain layer and remove unused files - Moved ObfuscateService and ObfuscatedAvatar from services to domain/services. - Updated references to use globals for stealth mode instead of AppConfig. - Removed old obfuscated_avatar.dart file and updated imports in call and incoming call pages. - Cleaned up imports in composition, contact, and history pages to reflect new structure. - Deleted unused BlockService, CallService, ContactService, AsymmetricCryptoService, and QRScanner classes. --- dialer/lib/domain/services/call_service.dart | 117 ++--- .../asymmetric_crypto_service.dart | 442 +++++++----------- .../domain/services/obfuscate_service.dart | 59 ++- .../common/widgets/obfuscated_avatar.dart | 53 --- .../presentation/features/call/call_page.dart | 2 +- .../features/call/incoming_call_page.dart | 1 - .../features/composition/composition.dart | 4 +- .../features/contacts/contact_page.dart | 4 - .../widgets/alphabet_scroll_page.dart | 1 - .../contacts/widgets/contact_modal.dart | 8 +- .../contacts/widgets/share_own_qr.dart | 2 +- .../features/history/history_page.dart | 6 +- .../settings/cryptography/key_management.dart | 2 +- dialer/lib/services/block_service.dart | 78 ---- dialer/lib/services/call_service.dart | 154 ------ dialer/lib/services/contact_service.dart | 86 ---- .../asymmetric_crypto_service.dart | 170 ------- dialer/lib/services/obfuscate_service.dart | 91 ---- dialer/lib/services/qr/qr_scanner.dart | 57 --- 19 files changed, 271 insertions(+), 1066 deletions(-) delete mode 100644 dialer/lib/presentation/common/widgets/obfuscated_avatar.dart delete mode 100644 dialer/lib/services/block_service.dart delete mode 100644 dialer/lib/services/call_service.dart delete mode 100644 dialer/lib/services/contact_service.dart delete mode 100644 dialer/lib/services/cryptography/asymmetric_crypto_service.dart delete mode 100644 dialer/lib/services/obfuscate_service.dart delete mode 100644 dialer/lib/services/qr/qr_scanner.dart diff --git a/dialer/lib/domain/services/call_service.dart b/dialer/lib/domain/services/call_service.dart index b46a1d3..c348b59 100644 --- a/dialer/lib/domain/services/call_service.dart +++ b/dialer/lib/domain/services/call_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'dart:typed_data'; +import '../../presentation/features/call/call_page.dart'; +import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page class CallService { static const MethodChannel _channel = MethodChannel('call_service'); @@ -9,20 +10,7 @@ class CallService { static final GlobalKey navigatorKey = GlobalKey(); - // Private constructor - CallService._privateConstructor() { - _initializeMethodCallHandler(); - } - - // Singleton instance - static final CallService _instance = CallService._privateConstructor(); - - // Factory constructor to return the same instance - factory CallService() { - return _instance; - } - - void _initializeMethodCallHandler() { + CallService() { _channel.setMethodCallHandler((call) async { final context = navigatorKey.currentContext; print('CallService: Received method ${call.method} with args ${call.arguments}'); @@ -33,70 +21,56 @@ class CallService { switch (call.method) { case "callAdded": - _handleCallAdded(context, call.arguments); + final phoneNumber = call.arguments["callId"] as String; + final state = call.arguments["state"] as String; + currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); + print('CallService: Call added, number: $currentPhoneNumber, state: $state'); + if (state == "ringing") { + _navigateToIncomingCallPage(context); + } else { + _navigateToCallPage(context); + } break; case "callStateChanged": - _handleCallStateChanged(context, call.arguments); + final state = call.arguments["state"] as String; + print('CallService: State changed to $state'); + if (state == "disconnected" || state == "disconnecting") { + _closeCallPage(context); + } else if (state == "active" || state == "dialing") { + _navigateToCallPage(context); + } else if (state == "ringing") { + _navigateToIncomingCallPage(context); + } break; case "callEnded": case "callRemoved": - _handleCallEnded(context); + print('CallService: Call ended/removed'); + _closeCallPage(context); + currentPhoneNumber = null; break; } }); } - void _handleCallAdded(BuildContext context, dynamic arguments) { - final phoneNumber = arguments["callId"] as String; - final state = arguments["state"] as String; - currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); - print('CallService: Call added, number: $currentPhoneNumber, state: $state'); - - if (state == "ringing") { - _navigateToIncomingCallPage(context); - } else { - _navigateToCallPage(context); - } - } - - void _handleCallStateChanged(BuildContext context, dynamic arguments) { - final state = arguments["state"] as String; - print('CallService: State changed to $state'); - - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); - } else if (state == "ringing") { - _navigateToIncomingCallPage(context); - } - } - - void _handleCallEnded(BuildContext context) { - print('CallService: Call ended/removed'); - _closeCallPage(context); - currentPhoneNumber = null; - } - void _navigateToCallPage(BuildContext context) { if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { print('CallService: CallPage already visible, skipping navigation'); return; } - print('CallService: Navigating to CallPage'); - Navigator.pushReplacementNamed( - context, - '/call', - arguments: { - 'displayName': currentPhoneNumber!, - 'phoneNumber': currentPhoneNumber!, - 'thumbnail': null, - } + Navigator.pushReplacement( + context, + MaterialPageRoute( + settings: const RouteSettings(name: '/call'), + builder: (context) => CallPage( + displayName: currentPhoneNumber!, + phoneNumber: currentPhoneNumber!, + thumbnail: null, + ), + ), ).then((_) { _isCallPageVisible = false; }); - _isCallPageVisible = true; } @@ -105,20 +79,20 @@ class CallService { print('CallService: IncomingCallPage already visible, skipping navigation'); return; } - print('CallService: Navigating to IncomingCallPage'); - Navigator.pushNamed( + Navigator.push( context, - '/incoming_call', - arguments: { - 'displayName': currentPhoneNumber!, - 'phoneNumber': currentPhoneNumber!, - 'thumbnail': null, - } + MaterialPageRoute( + settings: const RouteSettings(name: '/incoming_call'), + builder: (context) => IncomingCallPage( + displayName: currentPhoneNumber!, + phoneNumber: currentPhoneNumber!, + thumbnail: null, + ), + ), ).then((_) { _isCallPageVisible = false; }); - _isCallPageVisible = true; } @@ -127,7 +101,6 @@ class CallService { print('CallService: CallPage not visible, skipping pop'); return; } - if (Navigator.canPop(context)) { print('CallService: Popping CallPage'); Navigator.pop(context); @@ -146,7 +119,6 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - if (result["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), @@ -166,7 +138,6 @@ class CallService { print('CallService: Hanging up call'); final result = await _channel.invokeMethod('hangUpCall'); print('CallService: hangUpCall result: $result'); - if (result["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), @@ -180,4 +151,4 @@ class CallService { rethrow; } } -} +} \ No newline at end of file diff --git a/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart b/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart index d52cb91..07c2964 100644 --- a/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart +++ b/dialer/lib/domain/services/cryptography/asymmetric_crypto_service.dart @@ -1,296 +1,170 @@ +import 'dart:async'; import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:pointycastle/export.dart'; -import 'package:pointycastle/asymmetric/api.dart'; -import 'package:asn1lib/asn1lib.dart'; +import 'package:uuid/uuid.dart'; -/// Service for handling asymmetric cryptography operations class AsymmetricCryptoService { - static const String _privateKeyTag = 'private_key'; - static const String _publicKeyTag = 'public_key'; - - final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); - - AsymmetricKeyPair? _currentKeyPair; - - /// Initialize with the default key pair, creating one if it doesn't exist - Future initializeDefaultKeyPair() async { + static const MethodChannel _channel = MethodChannel('com.example.keystore'); + final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + final String _aliasPrefix = 'icing_'; + final Uuid _uuid = Uuid(); + + /// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata. + Future generateKeyPair({String? label}) async { try { - // Try to load existing keys - final privateKeyStr = await _secureStorage.read(key: _privateKeyTag); - final publicKeyStr = await _secureStorage.read(key: _publicKeyTag); + // Generate a unique identifier for the key + final String uuid = _uuid.v4(); + final String alias = '$_aliasPrefix$uuid'; + + // Invoke native method to generate the key pair + await _channel.invokeMethod('generateKeyPair', {'alias': alias}); + + // Store key metadata securely + final Map keyMetadata = { + 'alias': alias, + 'label': label ?? 'Key $uuid', + 'created_at': DateTime.now().toIso8601String(), + }; + + // Retrieve existing keys + final String? existingKeys = await _secureStorage.read(key: 'keys'); + List keysList = existingKeys != null ? jsonDecode(existingKeys) : []; + + // Add the new key + keysList.add(keyMetadata); + + // Save updated keys list + await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); + + return alias; + } on PlatformException catch (e) { + throw Exception("Failed to generate key pair: ${e.message}"); + } + } + + /// Signs data using the specified key alias. + Future signData(String alias, String data) async { + try { + final String signature = await _channel.invokeMethod('signData', { + 'alias': alias, + 'data': data, + }); + return signature; + } on PlatformException catch (e) { + throw Exception("Failed to sign data with alias '$alias': ${e.message}"); + } + } + + /// Retrieves the public key for the specified alias. + Future getPublicKey(String alias) async { + try { + final String publicKey = await _channel.invokeMethod('getPublicKey', { + 'alias': alias, + }); + return publicKey; + } on PlatformException catch (e) { + throw Exception("Failed to retrieve public key: ${e.message}"); + } + } + + /// Deletes the key pair associated with the specified alias and removes its metadata. + Future deleteKeyPair(String alias) async { + try { + await _channel.invokeMethod('deleteKeyPair', {'alias': alias}); - if (privateKeyStr != null && publicKeyStr != null) { - try { - // Parse existing keys - final privateKey = _parsePrivateKeyFromPem(privateKeyStr); - final publicKey = _parsePublicKeyFromPem(publicKeyStr); - - _currentKeyPair = AsymmetricKeyPair(publicKey, privateKey); - debugPrint('Loaded existing key pair successfully'); - return; - } catch (e) { - debugPrint('Error parsing stored keys: $e'); - // Continue to generate new keys + final String? existingKeys = await _secureStorage.read(key: 'keys'); + if (existingKeys != null) { + List keysList = jsonDecode(existingKeys); + keysList.removeWhere((key) => key['alias'] == alias); + await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); + } + } on PlatformException catch (e) { + throw Exception("Failed to delete key pair: ${e.message}"); + } + } + + /// Retrieves all stored key metadata. + Future>> getAllKeys() async { + try { + final String? existingKeys = await _secureStorage.read(key: 'keys'); + if (existingKeys == null) { + print("No keys found"); + return []; + } + List keysList = jsonDecode(existingKeys); + return keysList.cast>(); + } catch (e) { + throw Exception("Failed to retrieve keys: $e"); + } + } + + /// Checks if a key pair exists for the given alias. + Future keyPairExists(String alias) async { + try { + final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias}); + return exists; + } on PlatformException catch (e) { + throw Exception("Failed to check key pair existence: ${e.message}"); + } + } + + /// Initializes the default key pair if it doesn't exist. + Future initializeDefaultKeyPair() async { + const String defaultAlias = 'icing_default'; + final List> keys = await getAllKeys(); + + // Check if the key exists in metadata + final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias); + + if (!defaultKeyExists) { + await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias}); + + final Map keyMetadata = { + 'alias': defaultAlias, + 'label': 'Default Key', + 'created_at': DateTime.now().toIso8601String(), + }; + + keys.add(keyMetadata); + await _secureStorage.write(key: 'keys', value: jsonEncode(keys)); + } + } + + /// Updates the label of a key with the specified alias. + /// + /// [alias]: The unique alias of the key to update. + /// [newLabel]: The new label to assign to the key. + /// + /// Throws an exception if the key is not found or the update fails. + Future updateKeyLabel(String alias, String newLabel) async { + try { + // Retrieve existing keys + final String? existingKeys = await _secureStorage.read(key: 'keys'); + if (existingKeys == null) { + throw Exception("No keys found to update."); + } + + List keysList = jsonDecode(existingKeys); + + // Find the key with the specified alias + bool keyFound = false; + for (var key in keysList) { + if (key['alias'] == alias) { + key['label'] = newLabel; + keyFound = true; + break; } } - - // Generate new key pair - await generateAndStoreKeyPair(); + + if (!keyFound) { + throw Exception("Key with alias \"$alias\" not found."); + } + + // Save the updated keys list + await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); } catch (e) { - debugPrint('Error initializing key pair: $e'); - // Instead of rethrowing, we'll continue without encryption capability - // This ensures the app doesn't crash during startup + throw Exception("Failed to update key label: $e"); } } - - /// Generate a new key pair and store it securely - Future generateAndStoreKeyPair() async { - try { - debugPrint('Generating new RSA key pair...'); - - // Generate key pair with a simpler approach - final keyPair = await _generateRSAKeyPairSimple(1024); // Smaller keys for faster generation - _currentKeyPair = keyPair; - - // Export to PEM format - final publicKeyPem = _encodePublicKeyToPem(keyPair.publicKey); - final privateKeyPem = _encodePrivateKeyToPem(keyPair.privateKey); - - // Store in secure storage - await _secureStorage.write(key: _publicKeyTag, value: publicKeyPem); - await _secureStorage.write(key: _privateKeyTag, value: privateKeyPem); - - debugPrint('New key pair generated and stored successfully'); - } catch (e) { - debugPrint('Failed to generate key pair: $e'); - // Don't throw, allow the app to continue without encryption - } - } - - /// Encrypt data with the public key - Uint8List? encrypt(String plainText) { - if (_currentKeyPair == null) { - debugPrint('No key pair available for encryption'); - return null; - } - - try { - final cipher = PKCS1Encoding(RSAEngine()) - ..init(true, PublicKeyParameter(_currentKeyPair!.publicKey)); - - final input = Uint8List.fromList(utf8.encode(plainText)); - return cipher.process(input); - } catch (e) { - debugPrint('Encryption error: $e'); - return null; - } - } - - /// Decrypt data with the private key - String? decrypt(Uint8List encryptedData) { - if (_currentKeyPair == null) { - debugPrint('No key pair available for decryption'); - return null; - } - - try { - final cipher = PKCS1Encoding(RSAEngine()) - ..init(false, PrivateKeyParameter(_currentKeyPair!.privateKey)); - - final decrypted = cipher.process(encryptedData); - return utf8.decode(decrypted); - } catch (e) { - debugPrint('Decryption error: $e'); - return null; - } - } - - /// Get the current public key in PEM format - Future getPublicKeyPem() async { - return await _secureStorage.read(key: _publicKeyTag); - } - - // Simpler RSA key pair generation that doesn't use FortunaRandom - Future> _generateRSAKeyPairSimple(int bitLength) async { - // Use a simple secure random instead of FortunaRandom - final secureRandom = _SecureRandom(); - - // Create RSA key generator - final keyGen = RSAKeyGenerator() - ..init(ParametersWithRandom( - RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), - secureRandom, - )); - - // Generate and return the key pair - return keyGen.generateKeyPair() as AsymmetricKeyPair; - } - - // Original method (but no longer used directly) - static AsymmetricKeyPair _generateRSAKeyPair(int bitLength) { - try { - final secureRandom = _SecureRandom(); - - final keyGen = RSAKeyGenerator() - ..init(ParametersWithRandom( - RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), - secureRandom, - )); - - return keyGen.generateKeyPair() as AsymmetricKeyPair; - } catch (e) { - debugPrint('Error in _generateRSAKeyPair: $e'); - rethrow; - } - } - - String _encodePublicKeyToPem(RSAPublicKey publicKey) { - final asn1Sequence = ASN1Sequence(); - - asn1Sequence.add(ASN1Integer(publicKey.modulus!)); - asn1Sequence.add(ASN1Integer(publicKey.exponent!)); - - final base64 = base64Encode(asn1Sequence.encodedBytes); - return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----'; - } - - String _encodePrivateKeyToPem(RSAPrivateKey privateKey) { - final asn1Sequence = ASN1Sequence(); - - asn1Sequence.add(ASN1Integer(BigInt.from(0))); // version - asn1Sequence.add(ASN1Integer(privateKey.modulus!)); - asn1Sequence.add(ASN1Integer(privateKey.publicExponent!)); - asn1Sequence.add(ASN1Integer(privateKey.privateExponent!)); - asn1Sequence.add(ASN1Integer(privateKey.p!)); - asn1Sequence.add(ASN1Integer(privateKey.q!)); - // d mod (p-1) - asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.p! - BigInt.from(1)))); - // d mod (q-1) - asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.q! - BigInt.from(1)))); - // q^-1 mod p - asn1Sequence.add(ASN1Integer(_modInverse(privateKey.q!, privateKey.p!))); - - final base64 = base64Encode(asn1Sequence.encodedBytes); - return '-----BEGIN RSA PRIVATE KEY-----\n$base64\n-----END RSA PRIVATE KEY-----'; - } - - RSAPrivateKey _parsePrivateKeyFromPem(String pemString) { - final pemContent = pemString - .replaceAll('-----BEGIN RSA PRIVATE KEY-----', '') - .replaceAll('-----END RSA PRIVATE KEY-----', '') - .replaceAll('\n', ''); - - final asn1Parser = ASN1Parser(base64Decode(pemContent)); - final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; - - // Parse sequence values - final values = topLevelSeq.elements!.map((obj) => (obj as ASN1Integer).valueAsBigInteger).toList(); - - // Create RSA private key from components - return RSAPrivateKey( - values[1], // modulus - values[3], // privateExponent - values[4], // p - values[5], // q - ); - } - - RSAPublicKey _parsePublicKeyFromPem(String pemString) { - final pemContent = pemString - .replaceAll('-----BEGIN PUBLIC KEY-----', '') - .replaceAll('-----END PUBLIC KEY-----', '') - .replaceAll('\n', ''); - - final asn1Parser = ASN1Parser(base64Decode(pemContent)); - final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; - - // Extract modulus and exponent - final modulus = (topLevelSeq.elements![0] as ASN1Integer).valueAsBigInteger; - final exponent = (topLevelSeq.elements![1] as ASN1Integer).valueAsBigInteger; - - return RSAPublicKey(modulus, exponent); - } - - // Modular multiplicative inverse - BigInt _modInverse(BigInt a, BigInt m) { - // Extended Euclidean Algorithm to find modular inverse - BigInt t = BigInt.zero, newT = BigInt.one; - BigInt r = m, newR = a; - - while (newR != BigInt.zero) { - final quotient = r ~/ newR; - - final tempT = t; - t = newT; - newT = tempT - quotient * newT; - - final tempR = r; - r = newR; - newR = tempR - quotient * newR; - } - - if (r > BigInt.one) throw Exception('$a is not invertible modulo $m'); - if (t < BigInt.zero) t += m; - - return t; - } -} - -// Simple secure random implementation that doesn't use AESEngine -class _SecureRandom implements SecureRandom { - final Random _random = Random.secure(); - - @override - String get algorithmName => 'Dart_SecureRandom'; - - @override - void seed(CipherParameters params) { - // No additional seeding required as Random.secure() is already seeded - } - - @override - BigInt nextBigInteger(int bitLength) { - final fullBytes = bitLength ~/ 8; - final remainingBits = bitLength % 8; - - final bytes = Uint8List(fullBytes + (remainingBits > 0 ? 1 : 0)); - for (var i = 0; i < bytes.length; i++) { - bytes[i] = nextUint8(); - } - - // Adjust the last byte to match the remaining bits - if (remainingBits > 0) { - bytes[bytes.length - 1] &= (1 << remainingBits) - 1; - } - - return BigInt.parse(bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''), radix: 16); - } - - @override - int nextUint16() => (_random.nextInt(1 << 16) & 0xFFFF); - - @override - int nextUint32() => (_random.nextInt(1 << 32) & 0xFFFFFFFF); - - @override - int nextUint8() => (_random.nextInt(1 << 8) & 0xFF); - - @override - void reset() { - // No reset needed for Random.secure() - } - - @override - @override - Uint8List nextBytes(int count) { - final bytes = Uint8List(count); - for (var i = 0; i < count; i++) { - bytes[i] = nextUint8(); - } - return bytes; - } } diff --git a/dialer/lib/domain/services/obfuscate_service.dart b/dialer/lib/domain/services/obfuscate_service.dart index 7806222..6eaf43f 100644 --- a/dialer/lib/domain/services/obfuscate_service.dart +++ b/dialer/lib/domain/services/obfuscate_service.dart @@ -1,4 +1,10 @@ -import '../../core/config/app_config.dart'; +// lib/services/obfuscate_service.dart +import 'package:dialer/widgets/color_darkener.dart'; + +import '../../globals.dart' as globals; +import 'dart:ui'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; class ObfuscateService { // Private constructor @@ -14,7 +20,7 @@ class ObfuscateService { // Public method to obfuscate data String obfuscateData(String data) { - if (AppConfig.isStealthMode) { + if (globals.isStealthMode) { return _obfuscateData(data); } else { return data; @@ -34,3 +40,52 @@ class ObfuscateService { return ''; } } + + +class ObfuscatedAvatar extends StatelessWidget { + final Uint8List? imageBytes; + final double radius; + final Color backgroundColor; + final String? fallbackInitial; + + const ObfuscatedAvatar({ + Key? key, + required this.imageBytes, + this.radius = 25, + this.backgroundColor = Colors.grey, + this.fallbackInitial, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (imageBytes != null && imageBytes!.isNotEmpty) { + return ClipOval( + child: ImageFiltered( + imageFilter: globals.isStealthMode + ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Image.memory( + imageBytes!, + fit: BoxFit.cover, + width: radius * 2, + height: radius * 2, + ), + ), + ); + } else { + return CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + child: Text( + fallbackInitial != null && fallbackInitial!.isNotEmpty + ? fallbackInitial![0].toUpperCase() + : '?', + style: TextStyle( + color: darken(backgroundColor), + fontSize: radius, + ), + ), + ); + } + } +} diff --git a/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart b/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart deleted file mode 100644 index 36d49dd..0000000 --- a/dialer/lib/presentation/common/widgets/obfuscated_avatar.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:typed_data'; -import 'dart:ui'; -import 'package:flutter/material.dart'; -import '../../../core/config/app_config.dart'; -import '../../../core/utils/color_utils.dart'; - -class ObfuscatedAvatar extends StatelessWidget { - final Uint8List? imageBytes; - final double radius; - final Color backgroundColor; - final String? fallbackInitial; - - const ObfuscatedAvatar({ - Key? key, - required this.imageBytes, - this.radius = 25, - this.backgroundColor = Colors.grey, - this.fallbackInitial, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (imageBytes != null && imageBytes!.isNotEmpty) { - return ClipOval( - child: ImageFiltered( - imageFilter: AppConfig.isStealthMode - ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Image.memory( - imageBytes!, - fit: BoxFit.cover, - width: radius * 2, - height: radius * 2, - ), - ), - ); - } else { - return CircleAvatar( - radius: radius, - backgroundColor: backgroundColor, - child: Text( - fallbackInitial != null && fallbackInitial!.isNotEmpty - ? fallbackInitial![0].toUpperCase() - : '?', - style: TextStyle( - color: darken(backgroundColor), - fontSize: radius, - ), - ), - ); - } - } -} diff --git a/dialer/lib/presentation/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart index 6471361..137e7cf 100644 --- a/dialer/lib/presentation/features/call/call_page.dart +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import '../../../domain/services/call_service.dart'; import '../../../domain/services/obfuscate_service.dart'; import '../../../core/utils/color_utils.dart'; -import '../../../presentation/common/widgets/obfuscated_avatar.dart'; +// import '../../../presentation/common/widgets/obfuscated_avatar.dart'; class CallPage extends StatefulWidget { final String displayName; diff --git a/dialer/lib/presentation/features/call/incoming_call_page.dart b/dialer/lib/presentation/features/call/incoming_call_page.dart index c127b74..1abe72d 100644 --- a/dialer/lib/presentation/features/call/incoming_call_page.dart +++ b/dialer/lib/presentation/features/call/incoming_call_page.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import '../../../domain/services/call_service.dart'; import '../../../domain/services/obfuscate_service.dart'; import '../../../core/utils/color_utils.dart'; -import '../../../presentation/common/widgets/obfuscated_avatar.dart'; import 'call_page.dart'; class IncomingCallPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/composition/composition.dart b/dialer/lib/presentation/features/composition/composition.dart index 1497325..da81e67 100644 --- a/dialer/lib/presentation/features/composition/composition.dart +++ b/dialer/lib/presentation/features/composition/composition.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../services/contact_service.dart'; -import '../../../services/obfuscate_service.dart'; -import '../../../services/call_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; +import '../../../domain/services/call_service.dart'; import '../contacts/widgets/add_contact_button.dart'; class CompositionPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index ead0797..73f8ea3 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/alphabet_scroll_page.dart'; -import '../../common/widgets/loading_indicator.dart'; -import '../../../domain/services/obfuscate_service.dart'; class ContactPage extends StatefulWidget { const ContactPage({super.key}); @@ -12,8 +10,6 @@ class ContactPage extends StatefulWidget { } class _ContactPageState extends State { - final ObfuscateService _obfuscateService = ObfuscateService(); - @override Widget build(BuildContext context) { final contactState = ContactState.of(context); diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index d34f3a7..9e3ded9 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import '../../../../domain/services/obfuscate_service.dart'; import '../../../../core/utils/color_utils.dart'; -import '../../../common/widgets/obfuscated_avatar.dart'; import '../contact_state.dart'; import 'add_contact_button.dart'; import 'contact_modal.dart'; diff --git a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index 4f5d659..21109ec 100644 --- a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -3,10 +3,10 @@ import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../widgets/username_color_generator.dart'; import '../../../../widgets/color_darkener.dart'; -import '../../../../services/obfuscate_service.dart'; -import '../../../../services/block_service.dart'; -import '../../../../services/contact_service.dart'; -import '../../../../services/call_service.dart'; +import '../../../../domain/services/obfuscate_service.dart'; +import '../../../../domain/services/block_service.dart'; +import '../../../../domain/services/contact_service.dart'; +import '../../../../domain/services/call_service.dart'; class ContactModal extends StatefulWidget { final Contact contact; diff --git a/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart b/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart index 058ed35..10648b7 100644 --- a/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart +++ b/dialer/lib/presentation/features/contacts/widgets/share_own_qr.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/contact.dart'; -import 'package:dialer/services/contact_service.dart'; +import 'package:dialer/domain/services/contact_service.dart'; class QRCodeButton extends StatelessWidget { final List contacts; diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index ceeee4b..317f0e2 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -5,11 +5,11 @@ import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../services/obfuscate_service.dart'; +import '../../../domain/services/obfuscate_service.dart'; import '../../../widgets/color_darkener.dart'; import '../../../widgets/username_color_generator.dart'; -import '../../../services/block_service.dart'; -import '../../../services/call_service.dart'; +import '../../../domain/services/block_service.dart'; +import '../../../domain/services/call_service.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/contact_modal.dart'; diff --git a/dialer/lib/presentation/features/settings/cryptography/key_management.dart b/dialer/lib/presentation/features/settings/cryptography/key_management.dart index f766ebc..681db26 100644 --- a/dialer/lib/presentation/features/settings/cryptography/key_management.dart +++ b/dialer/lib/presentation/features/settings/cryptography/key_management.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart'; +import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart'; class ManageKeysPage extends StatefulWidget { const ManageKeysPage({Key? key}) : super(key: key); diff --git a/dialer/lib/services/block_service.dart b/dialer/lib/services/block_service.dart deleted file mode 100644 index 5ec0cb7..0000000 --- a/dialer/lib/services/block_service.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// Service for managing blocked phone numbers -class BlockService { - static const String _blockedNumbersKey = 'blocked_numbers'; - - // Private constructor - BlockService._privateConstructor(); - - // Singleton instance - static final BlockService _instance = BlockService._privateConstructor(); - - // Factory constructor to return the same instance - factory BlockService() { - return _instance; - } - - /// Block a phone number - Future blockNumber(String phoneNumber) async { - try { - final prefs = await SharedPreferences.getInstance(); - final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; - - // Don't add if already blocked - if (blockedNumbers.contains(phoneNumber)) { - return true; - } - - blockedNumbers.add(phoneNumber); - return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); - } catch (e) { - debugPrint('Error blocking number: $e'); - return false; - } - } - - /// Unblock a phone number - Future unblockNumber(String phoneNumber) async { - try { - final prefs = await SharedPreferences.getInstance(); - final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; - - if (!blockedNumbers.contains(phoneNumber)) { - return true; - } - - blockedNumbers.remove(phoneNumber); - return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); - } catch (e) { - debugPrint('Error unblocking number: $e'); - return false; - } - } - - /// Check if a number is blocked - Future isNumberBlocked(String phoneNumber) async { - try { - final prefs = await SharedPreferences.getInstance(); - final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; - return blockedNumbers.contains(phoneNumber); - } catch (e) { - debugPrint('Error checking if number is blocked: $e'); - return false; - } - } - - /// Get all blocked numbers - Future> getBlockedNumbers() async { - try { - final prefs = await SharedPreferences.getInstance(); - return prefs.getStringList(_blockedNumbersKey) ?? []; - } catch (e) { - debugPrint('Error getting blocked numbers: $e'); - return []; - } - } -} diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart deleted file mode 100644 index 903a13c..0000000 --- a/dialer/lib/services/call_service.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import '../presentation/features/call/call_page.dart'; -import '../presentation/features/call/incoming_call_page.dart'; // Import the new page - -class CallService { - static const MethodChannel _channel = MethodChannel('call_service'); - static String? currentPhoneNumber; - static bool _isCallPageVisible = false; - - static final GlobalKey navigatorKey = GlobalKey(); - - CallService() { - _channel.setMethodCallHandler((call) async { - final context = navigatorKey.currentContext; - print('CallService: Received method ${call.method} with args ${call.arguments}'); - if (context == null) { - print('CallService: Navigator context is null, cannot navigate'); - return; - } - - switch (call.method) { - case "callAdded": - final phoneNumber = call.arguments["callId"] as String; - final state = call.arguments["state"] as String; - currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); - print('CallService: Call added, number: $currentPhoneNumber, state: $state'); - if (state == "ringing") { - _navigateToIncomingCallPage(context); - } else { - _navigateToCallPage(context); - } - break; - case "callStateChanged": - final state = call.arguments["state"] as String; - print('CallService: State changed to $state'); - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); - } 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') { - print('CallService: CallPage already visible, skipping navigation'); - return; - } - print('CallService: Navigating to CallPage'); - Navigator.pushReplacement( - context, - MaterialPageRoute( - settings: const RouteSettings(name: '/call'), - builder: (context) => CallPage( - displayName: currentPhoneNumber!, - phoneNumber: currentPhoneNumber!, - thumbnail: null, - ), - ), - ).then((_) { - _isCallPageVisible = false; - }); - _isCallPageVisible = true; - } - - void _navigateToIncomingCallPage(BuildContext context) { - if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { - print('CallService: IncomingCallPage already visible, skipping navigation'); - return; - } - print('CallService: Navigating to IncomingCallPage'); - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: '/incoming_call'), - builder: (context) => IncomingCallPage( - displayName: currentPhoneNumber!, - phoneNumber: currentPhoneNumber!, - thumbnail: null, - ), - ), - ).then((_) { - _isCallPageVisible = false; - }); - _isCallPageVisible = true; - } - - void _closeCallPage(BuildContext context) { - if (!_isCallPageVisible) { - print('CallService: CallPage not visible, skipping pop'); - return; - } - if (Navigator.canPop(context)) { - print('CallService: Popping CallPage'); - Navigator.pop(context); - _isCallPageVisible = false; - } - } - - Future makeGsmCall( - BuildContext context, { - required String phoneNumber, - String? displayName, - Uint8List? thumbnail, - }) async { - try { - currentPhoneNumber = phoneNumber; - print('CallService: Making GSM call to $phoneNumber'); - final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); - print('CallService: makeGsmCall result: $result'); - if (result["status"] != "calling") { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Failed to initiate call")), - ); - } - } catch (e) { - print("CallService: Error making call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error making call: $e")), - ); - rethrow; - } - } - - Future hangUpCall(BuildContext context) async { - try { - print('CallService: Hanging up call'); - final result = await _channel.invokeMethod('hangUpCall'); - print('CallService: hangUpCall result: $result'); - if (result["status"] != "ended") { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Failed to end call")), - ); - } - } catch (e) { - print("CallService: Error hanging up call: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error hanging up call: $e")), - ); - rethrow; - } - } -} \ No newline at end of file diff --git a/dialer/lib/services/contact_service.dart b/dialer/lib/services/contact_service.dart deleted file mode 100644 index d961100..0000000 --- a/dialer/lib/services/contact_service.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:qr_flutter/qr_flutter.dart'; - -class ContactService { - Future> fetchContacts() async { - if (await FlutterContacts.requestPermission()) { - List contacts = await FlutterContacts.getContacts( - withProperties: true, - withThumbnail: true, - ); - return contacts; - } else { - // Permission denied - return []; - } - } - - Future> fetchFavoriteContacts() async { - if (await FlutterContacts.requestPermission()) { - // Get all contacts and filter for favorites - List allContacts = await FlutterContacts.getContacts( - withProperties: true, - withThumbnail: true, - ); - return allContacts.where((c) => c.isStarred).toList(); - } else { - // Permission denied - return []; - } - } - - Future addNewContact(Contact contact) async { - if (await FlutterContacts.requestPermission()) { - try { - return await FlutterContacts.insertContact(contact); - } catch (e) { - debugPrint('Error adding contact: $e'); - return null; - } - } - return null; - } - - void showContactQRCodeDialog(BuildContext context, Contact contact) { - final String vCard = contact.toVCard(); - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.grey[900], - title: Text( - 'QR Code for ${contact.displayName}', - style: const TextStyle(color: Colors.white), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - color: Colors.white, - padding: const EdgeInsets.all(16.0), - child: QrImageView( - data: vCard, - version: QrVersions.auto, - size: 200.0, - ), - ), - const SizedBox(height: 16.0), - const Text( - 'Scan this code to add this contact', - style: TextStyle(color: Colors.white70), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], - ); - }, - ); - } -} diff --git a/dialer/lib/services/cryptography/asymmetric_crypto_service.dart b/dialer/lib/services/cryptography/asymmetric_crypto_service.dart deleted file mode 100644 index 07c2964..0000000 --- a/dialer/lib/services/cryptography/asymmetric_crypto_service.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/services.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:uuid/uuid.dart'; - -class AsymmetricCryptoService { - static const MethodChannel _channel = MethodChannel('com.example.keystore'); - final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); - final String _aliasPrefix = 'icing_'; - final Uuid _uuid = Uuid(); - - /// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata. - Future generateKeyPair({String? label}) async { - try { - // Generate a unique identifier for the key - final String uuid = _uuid.v4(); - final String alias = '$_aliasPrefix$uuid'; - - // Invoke native method to generate the key pair - await _channel.invokeMethod('generateKeyPair', {'alias': alias}); - - // Store key metadata securely - final Map keyMetadata = { - 'alias': alias, - 'label': label ?? 'Key $uuid', - 'created_at': DateTime.now().toIso8601String(), - }; - - // Retrieve existing keys - final String? existingKeys = await _secureStorage.read(key: 'keys'); - List keysList = existingKeys != null ? jsonDecode(existingKeys) : []; - - // Add the new key - keysList.add(keyMetadata); - - // Save updated keys list - await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); - - return alias; - } on PlatformException catch (e) { - throw Exception("Failed to generate key pair: ${e.message}"); - } - } - - /// Signs data using the specified key alias. - Future signData(String alias, String data) async { - try { - final String signature = await _channel.invokeMethod('signData', { - 'alias': alias, - 'data': data, - }); - return signature; - } on PlatformException catch (e) { - throw Exception("Failed to sign data with alias '$alias': ${e.message}"); - } - } - - /// Retrieves the public key for the specified alias. - Future getPublicKey(String alias) async { - try { - final String publicKey = await _channel.invokeMethod('getPublicKey', { - 'alias': alias, - }); - return publicKey; - } on PlatformException catch (e) { - throw Exception("Failed to retrieve public key: ${e.message}"); - } - } - - /// Deletes the key pair associated with the specified alias and removes its metadata. - Future deleteKeyPair(String alias) async { - try { - await _channel.invokeMethod('deleteKeyPair', {'alias': alias}); - - final String? existingKeys = await _secureStorage.read(key: 'keys'); - if (existingKeys != null) { - List keysList = jsonDecode(existingKeys); - keysList.removeWhere((key) => key['alias'] == alias); - await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); - } - } on PlatformException catch (e) { - throw Exception("Failed to delete key pair: ${e.message}"); - } - } - - /// Retrieves all stored key metadata. - Future>> getAllKeys() async { - try { - final String? existingKeys = await _secureStorage.read(key: 'keys'); - if (existingKeys == null) { - print("No keys found"); - return []; - } - List keysList = jsonDecode(existingKeys); - return keysList.cast>(); - } catch (e) { - throw Exception("Failed to retrieve keys: $e"); - } - } - - /// Checks if a key pair exists for the given alias. - Future keyPairExists(String alias) async { - try { - final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias}); - return exists; - } on PlatformException catch (e) { - throw Exception("Failed to check key pair existence: ${e.message}"); - } - } - - /// Initializes the default key pair if it doesn't exist. - Future initializeDefaultKeyPair() async { - const String defaultAlias = 'icing_default'; - final List> keys = await getAllKeys(); - - // Check if the key exists in metadata - final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias); - - if (!defaultKeyExists) { - await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias}); - - final Map keyMetadata = { - 'alias': defaultAlias, - 'label': 'Default Key', - 'created_at': DateTime.now().toIso8601String(), - }; - - keys.add(keyMetadata); - await _secureStorage.write(key: 'keys', value: jsonEncode(keys)); - } - } - - /// Updates the label of a key with the specified alias. - /// - /// [alias]: The unique alias of the key to update. - /// [newLabel]: The new label to assign to the key. - /// - /// Throws an exception if the key is not found or the update fails. - Future updateKeyLabel(String alias, String newLabel) async { - try { - // Retrieve existing keys - final String? existingKeys = await _secureStorage.read(key: 'keys'); - if (existingKeys == null) { - throw Exception("No keys found to update."); - } - - List keysList = jsonDecode(existingKeys); - - // Find the key with the specified alias - bool keyFound = false; - for (var key in keysList) { - if (key['alias'] == alias) { - key['label'] = newLabel; - keyFound = true; - break; - } - } - - if (!keyFound) { - throw Exception("Key with alias \"$alias\" not found."); - } - - // Save the updated keys list - await _secureStorage.write(key: 'keys', value: jsonEncode(keysList)); - } catch (e) { - throw Exception("Failed to update key label: $e"); - } - } -} diff --git a/dialer/lib/services/obfuscate_service.dart b/dialer/lib/services/obfuscate_service.dart deleted file mode 100644 index 6eaf43f..0000000 --- a/dialer/lib/services/obfuscate_service.dart +++ /dev/null @@ -1,91 +0,0 @@ -// lib/services/obfuscate_service.dart -import 'package:dialer/widgets/color_darkener.dart'; - -import '../../globals.dart' as globals; -import 'dart:ui'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; - -class ObfuscateService { - // Private constructor - ObfuscateService._privateConstructor(); - - // Singleton instance - static final ObfuscateService _instance = ObfuscateService._privateConstructor(); - - // Factory constructor to return the same instance - factory ObfuscateService() { - return _instance; - } - - // Public method to obfuscate data - String obfuscateData(String data) { - if (globals.isStealthMode) { - return _obfuscateData(data); - } else { - return data; - } - } - - // Private helper method for obfuscation logic - String _obfuscateData(String data) { - if (data.isNotEmpty) { - // Ensure the string has at least two characters to obfuscate - if (data.length == 1) { - return '${data[0]}'; - } else { - return '${data[0]}...${data[data.length - 1]}'; - } - } - return ''; - } -} - - -class ObfuscatedAvatar extends StatelessWidget { - final Uint8List? imageBytes; - final double radius; - final Color backgroundColor; - final String? fallbackInitial; - - const ObfuscatedAvatar({ - Key? key, - required this.imageBytes, - this.radius = 25, - this.backgroundColor = Colors.grey, - this.fallbackInitial, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (imageBytes != null && imageBytes!.isNotEmpty) { - return ClipOval( - child: ImageFiltered( - imageFilter: globals.isStealthMode - ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) - : ImageFilter.blur(sigmaX: 0, sigmaY: 0), - child: Image.memory( - imageBytes!, - fit: BoxFit.cover, - width: radius * 2, - height: radius * 2, - ), - ), - ); - } else { - return CircleAvatar( - radius: radius, - backgroundColor: backgroundColor, - child: Text( - fallbackInitial != null && fallbackInitial!.isNotEmpty - ? fallbackInitial![0].toUpperCase() - : '?', - style: TextStyle( - color: darken(backgroundColor), - fontSize: radius, - ), - ), - ); - } - } -} diff --git a/dialer/lib/services/qr/qr_scanner.dart b/dialer/lib/services/qr/qr_scanner.dart deleted file mode 100644 index df405f6..0000000 --- a/dialer/lib/services/qr/qr_scanner.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; - -class QRCodeScannerScreen extends StatefulWidget { - const QRCodeScannerScreen({super.key}); - - @override - _QRCodeScannerScreenState createState() => _QRCodeScannerScreenState(); -} - -class _QRCodeScannerScreenState extends State { - MobileScannerController cameraController = MobileScannerController(); - bool _flashEnabled = false; - - @override - void dispose() { - cameraController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Scan QR Code'), - actions: [ - IconButton( - icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off), - onPressed: () { - setState(() { - _flashEnabled = !_flashEnabled; - cameraController.toggleTorch(); - }); - }, - ), - IconButton( - icon: const Icon(Icons.flip_camera_ios), - onPressed: () => cameraController.switchCamera(), - ), - ], - ), - body: MobileScanner( - controller: cameraController, - onDetect: (capture) { - final List barcodes = capture.barcodes; - if (barcodes.isNotEmpty) { - // Return the first barcode value - final String? code = barcodes.first.rawValue; - if (code != null) { - Navigator.pop(context, code); - } - } - }, - ), - ); - } -} -- 2.45.2 From 96427d78d9438e07cb6525d76d6f4139f79310dd Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:57:37 +0200 Subject: [PATCH 11/17] refactor: update contact fetching to include accounts, photo, and thumbnail --- .../features/contacts/widgets/alphabet_scroll_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index 9e3ded9..63ba9f4 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -54,9 +54,9 @@ class _AlphabetScrollPageState extends State { if (await FlutterContacts.requestPermission()) { Contact? fullContact = await FlutterContacts.getContact(contact.id, withProperties: true, - withAccounts: false, - withPhoto: false, - withThumbnail: false); + withAccounts: true, + withPhoto: true, + withThumbnail: true); if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; -- 2.45.2 From 918265744e9b922b3d285083275f058145027aba Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:12:31 +0200 Subject: [PATCH 12/17] refactor: enhance stealth mode handling and clean up imports across services --- dialer/lib/core/config/app_config.dart | 1 + dialer/lib/domain/services/obfuscate_service.dart | 6 +++--- dialer/lib/globals.dart | 4 +++- .../lib/presentation/features/composition/composition.dart | 2 +- dialer/lib/presentation/features/contacts/contact_page.dart | 4 ++++ 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/dialer/lib/core/config/app_config.dart b/dialer/lib/core/config/app_config.dart index 5326cf5..d937e6c 100644 --- a/dialer/lib/core/config/app_config.dart +++ b/dialer/lib/core/config/app_config.dart @@ -9,5 +9,6 @@ class AppConfig { static Future initialize() async { const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); isStealthMode = stealthFlag.toLowerCase() == 'true'; + print('Stealth mode is ${isStealthMode ? 'enabled' : 'disabled'}'); } } diff --git a/dialer/lib/domain/services/obfuscate_service.dart b/dialer/lib/domain/services/obfuscate_service.dart index 6eaf43f..4d5bba1 100644 --- a/dialer/lib/domain/services/obfuscate_service.dart +++ b/dialer/lib/domain/services/obfuscate_service.dart @@ -1,7 +1,7 @@ // lib/services/obfuscate_service.dart import 'package:dialer/widgets/color_darkener.dart'; -import '../../globals.dart' as globals; +import '../../core/config/app_config.dart'; import 'dart:ui'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -20,7 +20,7 @@ class ObfuscateService { // Public method to obfuscate data String obfuscateData(String data) { - if (globals.isStealthMode) { + if (AppConfig.isStealthMode) { return _obfuscateData(data); } else { return data; @@ -61,7 +61,7 @@ class ObfuscatedAvatar extends StatelessWidget { if (imageBytes != null && imageBytes!.isNotEmpty) { return ClipOval( child: ImageFiltered( - imageFilter: globals.isStealthMode + imageFilter: AppConfig.isStealthMode ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Image.memory( diff --git a/dialer/lib/globals.dart b/dialer/lib/globals.dart index a8c1ab7..c1410d0 100644 --- a/dialer/lib/globals.dart +++ b/dialer/lib/globals.dart @@ -1,5 +1,7 @@ // Global variables accessible throughout the app library globals; +import 'core/config/app_config.dart'; + // Whether the app is in stealth mode (obfuscated content) -bool isStealthMode = false; \ No newline at end of file +bool get isStealthMode => AppConfig.isStealthMode; \ No newline at end of file diff --git a/dialer/lib/presentation/features/composition/composition.dart b/dialer/lib/presentation/features/composition/composition.dart index da81e67..3b5d6ce 100644 --- a/dialer/lib/presentation/features/composition/composition.dart +++ b/dialer/lib/presentation/features/composition/composition.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../services/contact_service.dart'; +import '../../../domain/services/contact_service.dart'; import '../../../domain/services/obfuscate_service.dart'; import '../../../domain/services/call_service.dart'; import '../contacts/widgets/add_contact_button.dart'; diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index 73f8ea3..ead0797 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/alphabet_scroll_page.dart'; +import '../../common/widgets/loading_indicator.dart'; +import '../../../domain/services/obfuscate_service.dart'; class ContactPage extends StatefulWidget { const ContactPage({super.key}); @@ -10,6 +12,8 @@ class ContactPage extends StatefulWidget { } class _ContactPageState extends State { + final ObfuscateService _obfuscateService = ObfuscateService(); + @override Widget build(BuildContext context) { final contactState = ContactState.of(context); -- 2.45.2 From 1a020bbbd82aa500882cd1fa7660f0a13260799d Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:13:07 +0200 Subject: [PATCH 13/17] refactor: remove unused imports and obfuscate service from contact page --- dialer/lib/presentation/features/contacts/contact_page.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index ead0797..bbbe935 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/alphabet_scroll_page.dart'; -import '../../common/widgets/loading_indicator.dart'; -import '../../../domain/services/obfuscate_service.dart'; class ContactPage extends StatefulWidget { const ContactPage({super.key}); @@ -12,7 +10,6 @@ class ContactPage extends StatefulWidget { } class _ContactPageState extends State { - final ObfuscateService _obfuscateService = ObfuscateService(); @override Widget build(BuildContext context) { -- 2.45.2 From 00e7d850b03f8433f328b011adde4888d25e8cf8 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:24:56 +0200 Subject: [PATCH 14/17] refactor: convert ContactPage to StatelessWidget and optimize AlphabetScrollPage for better performance --- .../features/contacts/contact_page.dart | 8 +- .../widgets/alphabet_scroll_page.dart | 230 ++++++++++-------- .../contacts/widgets/contact_modal.dart | 2 + .../features/dialer/composition_page.dart | 35 ++- .../features/favorites/favorites_page.dart | 31 +-- .../features/history/history_page.dart | 15 +- .../presentation/features/home/home_page.dart | 13 +- 7 files changed, 190 insertions(+), 144 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index bbbe935..bebaad3 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -2,15 +2,9 @@ import 'package:flutter/material.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/alphabet_scroll_page.dart'; -class ContactPage extends StatefulWidget { +class ContactPage extends StatelessWidget { const ContactPage({super.key}); - @override - _ContactPageState createState() => _ContactPageState(); -} - -class _ContactPageState extends State { - @override Widget build(BuildContext context) { final contactState = ContactState.of(context); diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index 63ba9f4..c0af877 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -24,12 +24,34 @@ class AlphabetScrollPage extends StatefulWidget { class _AlphabetScrollPageState extends State { late ScrollController _scrollController; final ObfuscateService _obfuscateService = ObfuscateService(); + late Map> _alphabetizedContacts; + late List _alphabetKeys; @override void initState() { super.initState(); _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); _scrollController.addListener(_onScroll); + _organizeContacts(); + } + + @override + void didUpdateWidget(AlphabetScrollPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.contacts != widget.contacts) { + _organizeContacts(); + } + } + + void _organizeContacts() { + _alphabetizedContacts = {}; + for (var contact in widget.contacts) { + String firstLetter = contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '#'; + (_alphabetizedContacts[firstLetter] ??= []).add(contact); + } + _alphabetKeys = _alphabetizedContacts.keys.toList()..sort(); } void _onScroll() { @@ -49,7 +71,7 @@ class _AlphabetScrollPageState extends State { } } - void _toggleFavorite(Contact contact) async { + Future _toggleFavorite(Contact contact) async { try { if (await FlutterContacts.requestPermission()) { Contact? fullContact = await FlutterContacts.getContact(contact.id, @@ -62,36 +84,29 @@ class _AlphabetScrollPageState extends State { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } - await _refreshContacts(); + + // Check if widget is still mounted before calling functions that use context + if (mounted) { + await _refreshContacts(); + } } else { debugPrint("Could not fetch contact details"); } } catch (e) { debugPrint("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update contact favorite status')), - ); + // Only show snackbar if still mounted + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } } } @override Widget build(BuildContext context) { - final contacts = widget.contacts; final selfContact = ContactState.of(context).selfContact; - Map> alphabetizedContacts = {}; - for (var contact in contacts) { - String firstLetter = contact.displayName.isNotEmpty - ? contact.displayName[0].toUpperCase() - : '#'; - if (!alphabetizedContacts.containsKey(firstLetter)) { - alphabetizedContacts[firstLetter] = []; - } - alphabetizedContacts[firstLetter]!.add(contact); - } - - List alphabetKeys = alphabetizedContacts.keys.toList()..sort(); - return Scaffold( backgroundColor: Colors.black, body: Column( @@ -104,7 +119,7 @@ class _AlphabetScrollPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AddContactButton(), - QRCodeButton(contacts: contacts, selfContact: selfContact), + QRCodeButton(contacts: widget.contacts, selfContact: selfContact), ], ), ), @@ -112,94 +127,11 @@ class _AlphabetScrollPageState extends State { Expanded( child: ListView.builder( controller: _scrollController, - itemCount: alphabetKeys.length, + itemCount: _alphabetKeys.length, itemBuilder: (context, index) { - String letter = alphabetKeys[index]; - List contactsForLetter = alphabetizedContacts[letter]!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Alphabet Letter Header - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - letter, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - // Contact Entries - ...contactsForLetter.map((contact) { - String phoneNumber = contact.phones.isNotEmpty - ? _obfuscateService.obfuscateData(contact.phones.first.number) - : 'No phone number'; - Color avatarColor = - generateColorFromName(contact.displayName); - return ListTile( - leading: ObfuscatedAvatar( - imageBytes: contact.thumbnail, - radius: 25, - backgroundColor: avatarColor, - fallbackInitial: contact.displayName, - ), - title: Text( - _obfuscateService.obfuscateData(contact.displayName), - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - phoneNumber, - style: const TextStyle(color: Colors.white70), - ), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return ContactModal( - contact: contact, - onEdit: () async { - if (await FlutterContacts.requestPermission()) { - final updatedContact = - await FlutterContacts.openExternalEdit( - contact.id); - if (updatedContact != null) { - await _refreshContacts(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - '${contact.displayName} updated successfully!'), - ), - ); - } else { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: - Text('Edit canceled or failed.'), - ), - ); - } - } - }, - onToggleFavorite: () { - _toggleFavorite(contact); - }, - isFavorite: contact.isStarred, - ); - }, - ); - }, - ); - }), - ], - ); + String letter = _alphabetKeys[index]; + List contactsForLetter = _alphabetizedContacts[letter]!; + return _buildLetterSection(letter, contactsForLetter); }, ), ), @@ -208,6 +140,90 @@ class _AlphabetScrollPageState extends State { ); } + Widget _buildLetterSection(String letter, List contactsForLetter) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Alphabet Letter Header + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text( + letter, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + // Contact Entries + ...contactsForLetter.map((contact) => _buildContactTile(contact)), + ], + ); + } + + Widget _buildContactTile(Contact contact) { + String phoneNumber = contact.phones.isNotEmpty + ? _obfuscateService.obfuscateData(contact.phones.first.number) + : 'No phone number'; + Color avatarColor = generateColorFromName(contact.displayName); + + return ListTile( + leading: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), + title: Text( + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + phoneNumber, + style: const TextStyle(color: Colors.white70), + ), + onTap: () => _showContactModal(contact), + ); + } + + void _showContactModal(Contact contact) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return ContactModal( + contact: contact, + onEdit: () => _onEditContact(contact), + onToggleFavorite: () => _toggleFavorite(contact), + isFavorite: contact.isStarred, + ); + }, + ); + } + + Future _onEditContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = await FlutterContacts.openExternalEdit(contact.id); + if (updatedContact != null) { + await _refreshContacts(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${contact.displayName} updated successfully!'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Edit canceled or failed.'), + ), + ); + } + } + } + @override void dispose() { _scrollController.dispose(); diff --git a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index 21109ec..a0a1dce 100644 --- a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -334,7 +334,9 @@ class _ContactModalState extends State { width: double.infinity, child: ElevatedButton.icon( onPressed: () { + // First close the modal to avoid unmounted widget issues Navigator.of(context).pop(); + // Then toggle the favorite status widget.onToggleFavorite(); }, icon: Icon(widget.isFavorite diff --git a/dialer/lib/presentation/features/dialer/composition_page.dart b/dialer/lib/presentation/features/dialer/composition_page.dart index b6c8b30..5f186c7 100644 --- a/dialer/lib/presentation/features/dialer/composition_page.dart +++ b/dialer/lib/presentation/features/dialer/composition_page.dart @@ -20,7 +20,10 @@ class _CompositionPageState extends State { final ContactService _contactService = ContactService(); final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); - + + // Cache for normalized phone numbers to avoid repeated processing + final Map _normalizedPhoneCache = {}; + @override void initState() { super.initState(); @@ -33,15 +36,35 @@ class _CompositionPageState extends State { setState(() {}); } + String _getNormalizedPhone(String phone) { + return _normalizedPhoneCache.putIfAbsent( + phone, + () => phone.replaceAll(RegExp(r'\D'), '') + ); + } + void _filterContacts() { + if (dialedNumber.isEmpty) { + setState(() { + _filteredContacts = _allContacts; + }); + return; + } + + final String normalizedDialed = dialedNumber.replaceAll(RegExp(r'\D'), ''); + final String lowerDialed = dialedNumber.toLowerCase(); + setState(() { _filteredContacts = _allContacts.where((contact) { + // Check phone numbers final phoneMatch = contact.phones.any((phone) => - phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber)); - final nameMatch = contact.displayName - .toLowerCase() - .contains(dialedNumber.toLowerCase()); - return phoneMatch || nameMatch; + _getNormalizedPhone(phone.number).contains(normalizedDialed)); + + // Only check name if phone doesn't match (optimization) + if (phoneMatch) return true; + + // Check display name + return contact.displayName.toLowerCase().contains(lowerDialed); }).toList(); }); } diff --git a/dialer/lib/presentation/features/favorites/favorites_page.dart b/dialer/lib/presentation/features/favorites/favorites_page.dart index ca8d39f..712ece6 100644 --- a/dialer/lib/presentation/features/favorites/favorites_page.dart +++ b/dialer/lib/presentation/features/favorites/favorites_page.dart @@ -10,27 +10,28 @@ class FavoritesPage extends StatelessWidget { final contactState = ContactState.of(context); if (contactState.loading) { - return const Center(child: CircularProgressIndicator()); + return const Scaffold( + backgroundColor: Colors.black, + body: Center(child: CircularProgressIndicator()), + ); } final favorites = contactState.favoriteContacts; - if (favorites.isEmpty) { - return const Center( - child: Text( - 'No favorites yet.\nStar your contacts to add them here.', - style: TextStyle(color: Colors.white60), - textAlign: TextAlign.center, - ), - ); - } - return Scaffold( backgroundColor: Colors.black, - body: AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: favorites, - ), + body: favorites.isEmpty + ? const Center( + child: Text( + 'No favorites yet.\nStar your contacts to add them here.', + style: TextStyle(color: Colors.white60), + textAlign: TextAlign.center, + ), + ) + : AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: favorites, + ), ); } } diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index 317f0e2..e193119 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -79,14 +79,19 @@ class _HistoryPageState extends State fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } - await _refreshContacts(); + // Check if still mounted before accessing context + if (mounted) { + await _refreshContacts(); + } } else { - print("Could not fetch contact details"); + debugPrint("Could not fetch contact details"); } } catch (e) { - print("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update favorite status'))); + debugPrint("Error updating favorite status: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update favorite status'))); + } } } diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index fb43fc6..77babf9 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -107,14 +107,19 @@ class _MyHomePageState extends State if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); - _fetchContacts(); + // Check if widget is still mounted before updating state + if (mounted) { + _fetchContacts(); + } } } } catch (e) { debugPrint("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update contact favorite status')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } } } -- 2.45.2 From d7ed797b80fd9d7eed18c5aa6b1bfc8eec6c651a Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 16 May 2025 22:14:42 +0200 Subject: [PATCH 15/17] refactor: update imports and restructure widget files for better organization --- dialer/lib/core/navigation/app_router.dart | 2 +- .../domain/services/obfuscate_service.dart | 2 +- .../common}/widgets/color_darkener.dart | 0 .../common}/widgets/qr_scanner.dart | 0 .../widgets/username_color_generator.dart | 0 .../features/composition/composition.dart | 1 - .../contacts/widgets/contact_modal.dart | 13 +- .../features/dialer/composition_page.dart | 311 ------------------ .../features/history/history_page.dart | 4 +- .../presentation/features/home/home_page.dart | 2 +- dialer/lib/widgets/loading_indicator.dart | 10 - 11 files changed, 7 insertions(+), 338 deletions(-) rename dialer/lib/{ => presentation/common}/widgets/color_darkener.dart (100%) rename dialer/lib/{ => presentation/common}/widgets/qr_scanner.dart (100%) rename dialer/lib/{ => presentation/common}/widgets/username_color_generator.dart (100%) delete mode 100644 dialer/lib/presentation/features/dialer/composition_page.dart delete mode 100644 dialer/lib/widgets/loading_indicator.dart diff --git a/dialer/lib/core/navigation/app_router.dart b/dialer/lib/core/navigation/app_router.dart index ec72fab..f63aff2 100644 --- a/dialer/lib/core/navigation/app_router.dart +++ b/dialer/lib/core/navigation/app_router.dart @@ -4,7 +4,7 @@ import '../../presentation/features/call/incoming_call_page.dart'; import '../../presentation/features/home/home_page.dart'; import '../../presentation/features/settings/settings.dart'; // Updated import import '../../presentation/features/contacts/contact_page.dart'; -import '../../presentation/features/dialer/composition_page.dart'; +import '../../presentation/features/composition/composition.dart'; import 'dart:typed_data'; class AppRouter { diff --git a/dialer/lib/domain/services/obfuscate_service.dart b/dialer/lib/domain/services/obfuscate_service.dart index 4d5bba1..82f0bbf 100644 --- a/dialer/lib/domain/services/obfuscate_service.dart +++ b/dialer/lib/domain/services/obfuscate_service.dart @@ -1,5 +1,5 @@ // lib/services/obfuscate_service.dart -import 'package:dialer/widgets/color_darkener.dart'; +import 'package:dialer/presentation/common/widgets/color_darkener.dart'; import '../../core/config/app_config.dart'; import 'dart:ui'; diff --git a/dialer/lib/widgets/color_darkener.dart b/dialer/lib/presentation/common/widgets/color_darkener.dart similarity index 100% rename from dialer/lib/widgets/color_darkener.dart rename to dialer/lib/presentation/common/widgets/color_darkener.dart diff --git a/dialer/lib/widgets/qr_scanner.dart b/dialer/lib/presentation/common/widgets/qr_scanner.dart similarity index 100% rename from dialer/lib/widgets/qr_scanner.dart rename to dialer/lib/presentation/common/widgets/qr_scanner.dart diff --git a/dialer/lib/widgets/username_color_generator.dart b/dialer/lib/presentation/common/widgets/username_color_generator.dart similarity index 100% rename from dialer/lib/widgets/username_color_generator.dart rename to dialer/lib/presentation/common/widgets/username_color_generator.dart diff --git a/dialer/lib/presentation/features/composition/composition.dart b/dialer/lib/presentation/features/composition/composition.dart index fe4d525..14cd776 100644 --- a/dialer/lib/presentation/features/composition/composition.dart +++ b/dialer/lib/presentation/features/composition/composition.dart @@ -4,7 +4,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../domain/services/contact_service.dart'; import '../../../domain/services/obfuscate_service.dart'; import '../../../domain/services/call_service.dart'; -import '../contacts/widgets/add_contact_button.dart'; class CompositionPage extends StatefulWidget { const CompositionPage({super.key}); diff --git a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index a0a1dce..a1a8cf2 100644 --- a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../widgets/username_color_generator.dart'; -import '../../../../widgets/color_darkener.dart'; +import '../../../common/widgets/username_color_generator.dart'; +import '../../../common/widgets/color_darkener.dart'; import '../../../../domain/services/obfuscate_service.dart'; import '../../../../domain/services/block_service.dart'; import '../../../../domain/services/contact_service.dart'; @@ -86,15 +86,6 @@ class _ContactModalState extends State { } } - void _launchPhoneDialer(String phoneNumber) async { - final uri = Uri(scheme: 'tel', path: phoneNumber); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - debugPrint('Could not launch $phoneNumber'); - } - } - void _launchSms(String phoneNumber) async { final uri = Uri(scheme: 'sms', path: phoneNumber); if (await canLaunchUrl(uri)) { diff --git a/dialer/lib/presentation/features/dialer/composition_page.dart b/dialer/lib/presentation/features/dialer/composition_page.dart deleted file mode 100644 index 5f186c7..0000000 --- a/dialer/lib/presentation/features/dialer/composition_page.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../../../domain/services/contact_service.dart'; -import '../../../domain/services/obfuscate_service.dart'; -import '../../../domain/services/call_service.dart'; -import '../contacts/widgets/add_contact_button.dart'; - -class CompositionPage extends StatefulWidget { - const CompositionPage({super.key}); - - @override - _CompositionPageState createState() => _CompositionPageState(); -} - -class _CompositionPageState extends State { - String dialedNumber = ""; - List _allContacts = []; - List _filteredContacts = []; - final ContactService _contactService = ContactService(); - final ObfuscateService _obfuscateService = ObfuscateService(); - final CallService _callService = CallService(); - - // Cache for normalized phone numbers to avoid repeated processing - final Map _normalizedPhoneCache = {}; - - @override - void initState() { - super.initState(); - _fetchContacts(); - } - - Future _fetchContacts() async { - _allContacts = await _contactService.fetchContacts(); - _filteredContacts = _allContacts; - setState(() {}); - } - - String _getNormalizedPhone(String phone) { - return _normalizedPhoneCache.putIfAbsent( - phone, - () => phone.replaceAll(RegExp(r'\D'), '') - ); - } - - void _filterContacts() { - if (dialedNumber.isEmpty) { - setState(() { - _filteredContacts = _allContacts; - }); - return; - } - - final String normalizedDialed = dialedNumber.replaceAll(RegExp(r'\D'), ''); - final String lowerDialed = dialedNumber.toLowerCase(); - - setState(() { - _filteredContacts = _allContacts.where((contact) { - // Check phone numbers - final phoneMatch = contact.phones.any((phone) => - _getNormalizedPhone(phone.number).contains(normalizedDialed)); - - // Only check name if phone doesn't match (optimization) - if (phoneMatch) return true; - - // Check display name - return contact.displayName.toLowerCase().contains(lowerDialed); - }).toList(); - }); - } - - void _onNumberPress(String number) { - setState(() { - dialedNumber += number; - _filterContacts(); - }); - } - - void _onDeletePress() { - setState(() { - if (dialedNumber.isNotEmpty) { - dialedNumber = dialedNumber.substring(0, dialedNumber.length - 1); - _filterContacts(); - } - }); - } - - void _onClearPress() { - setState(() { - dialedNumber = ""; - _filteredContacts = _allContacts; - }); - } - - void _makeCall(String phoneNumber) async { - try { - await _callService.makeGsmCall(context, phoneNumber: phoneNumber); - setState(() { - dialedNumber = phoneNumber; - }); - } catch (e) { - debugPrint("Error making call: $e"); - } - } - - void _launchSms(String phoneNumber) async { - final uri = Uri(scheme: 'sms', path: phoneNumber); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - debugPrint('Could not send SMS to $phoneNumber'); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - Column( - children: [ - // Top half: Display contacts matching dialed number - Expanded( - flex: 2, - child: Container( - padding: const EdgeInsets.only( - top: 42.0, left: 16.0, right: 16.0, bottom: 16.0), - color: Colors.black, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ListView( - children: _filteredContacts.isNotEmpty - ? _filteredContacts.map((contact) { - final phoneNumber = contact.phones.isNotEmpty - ? contact.phones.first.number - : 'No phone number'; - return ListTile( - title: Text( - _obfuscateService.obfuscateData(contact.displayName), - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - _obfuscateService.obfuscateData(phoneNumber), - style: const TextStyle(color: Colors.grey), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.phone, - color: Colors.green[300], - size: 20), - onPressed: () { - _makeCall(phoneNumber); - }, - ), - IconButton( - icon: Icon(Icons.message, - color: Colors.blue[300], - size: 20), - onPressed: () { - _launchSms(phoneNumber); - }, - ), - ], - ), - onTap: () { - // Handle contact selection if needed - }, - ); - }).toList() - : [], - ), - ), - ], - ), - ), - ), - - // Bottom half: Dialpad and Dialed number display with erase button - Expanded( - flex: 2, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // Display dialed number with erase button - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Align( - alignment: Alignment.center, - child: Text( - dialedNumber, - style: const TextStyle( - fontSize: 24, color: Colors.white), - overflow: TextOverflow.ellipsis, - ), - ), - ), - IconButton( - onPressed: _onClearPress, - icon: const Icon(Icons.backspace, - color: Colors.white), - ), - ], - ), - const SizedBox(height: 10), - - // Dialpad - Expanded( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - _buildDialButton('1'), - _buildDialButton('2'), - _buildDialButton('3'), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - _buildDialButton('4'), - _buildDialButton('5'), - _buildDialButton('6'), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - _buildDialButton('7'), - _buildDialButton('8'), - _buildDialButton('9'), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - _buildDialButton('*'), - _buildDialButton('0'), - _buildDialButton('#'), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - - // Add Contact Button - Positioned( - bottom: 20.0, - left: 0, - right: 0, - child: Center( - child: AddContactButton(), - ), - ), - - // Top Row with Back Arrow - Positioned( - top: 40.0, - left: 16.0, - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () { - Navigator.pop(context); - }, - ), - ), - ], - ), - ); - } - - Widget _buildDialButton(String number) { - return ElevatedButton( - onPressed: () => _onNumberPress(number), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - shape: const CircleBorder(), - padding: const EdgeInsets.all(16), - ), - child: Text( - number, - style: const TextStyle( - fontSize: 24, - color: Colors.white, - ), - ), - ); - } -} diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index 1c940e1..efd9afd 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -6,8 +6,8 @@ import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../domain/services/obfuscate_service.dart'; -import '../../../widgets/color_darkener.dart'; -import '../../../widgets/username_color_generator.dart'; +import '../../common/widgets/color_darkener.dart'; +import '../../common/widgets/username_color_generator.dart'; import '../../../domain/services/block_service.dart'; import '../../../domain/services/call_service.dart'; import '../contacts/contact_state.dart'; diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index 77babf9..a2ad446 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -3,7 +3,7 @@ import '../../../domain/services/obfuscate_service.dart'; import '../contacts/contact_page.dart'; import '../favorites/favorites_page.dart'; import '../history/history_page.dart'; -import '../dialer/composition_page.dart'; +import '../composition/composition.dart'; import '../settings/settings.dart'; import '../voicemail/voicemail_page.dart'; import '../contacts/contact_state.dart'; diff --git a/dialer/lib/widgets/loading_indicator.dart b/dialer/lib/widgets/loading_indicator.dart deleted file mode 100644 index ecb22c1..0000000 --- a/dialer/lib/widgets/loading_indicator.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIndicatorWidget extends StatelessWidget { - const LoadingIndicatorWidget({super.key}); - - @override - Widget build(BuildContext context) { - return const Center(child: CircularProgressIndicator()); - } -} -- 2.45.2 From 6fc2991d1e7957106fe39ef079c5dc50e0b6d8c7 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 16 May 2025 22:27:58 +0200 Subject: [PATCH 16/17] refactor: clean up whitespace in block_service and contact_modal files --- dialer/lib/domain/services/block_service.dart | 8 ++++---- .../features/contacts/widgets/contact_modal.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dialer/lib/domain/services/block_service.dart b/dialer/lib/domain/services/block_service.dart index 5ec0cb7..5a4a0b0 100644 --- a/dialer/lib/domain/services/block_service.dart +++ b/dialer/lib/domain/services/block_service.dart @@ -21,12 +21,12 @@ class BlockService { try { final prefs = await SharedPreferences.getInstance(); final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; - + // Don't add if already blocked if (blockedNumbers.contains(phoneNumber)) { return true; } - + blockedNumbers.add(phoneNumber); return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); } catch (e) { @@ -40,11 +40,11 @@ class BlockService { try { final prefs = await SharedPreferences.getInstance(); final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; - + if (!blockedNumbers.contains(phoneNumber)) { return true; } - + blockedNumbers.remove(phoneNumber); return await prefs.setStringList(_blockedNumbersKey, blockedNumbers); } catch (e) { diff --git a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index a1a8cf2..ddf8692 100644 --- a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -60,7 +60,7 @@ class _ContactModalState extends State { ); return; } - + if (isBlocked) { await BlockService().unblockNumber(phoneNumber); if (mounted) { @@ -80,7 +80,7 @@ class _ContactModalState extends State { if (phoneNumber != 'No phone number' && mounted) { _checkIfBlocked(); } - + if (mounted) { Navigator.of(context).pop(); } -- 2.45.2 From 960ddd8e8f75e5051faf03a2b0b14044561289eb Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 16 May 2025 22:33:48 +0200 Subject: [PATCH 17/17] refactor: remove unused color_utils import and replace with username_color_generator in call and contact pages --- dialer/lib/core/utils/color_utils.dart | 37 ------------------- .../presentation/features/call/call_page.dart | 2 +- .../features/call/incoming_call_page.dart | 2 +- .../widgets/alphabet_scroll_page.dart | 2 +- 4 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 dialer/lib/core/utils/color_utils.dart diff --git a/dialer/lib/core/utils/color_utils.dart b/dialer/lib/core/utils/color_utils.dart deleted file mode 100644 index 6677b1f..0000000 --- a/dialer/lib/core/utils/color_utils.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Generates a color based on a string input (typically a name) -Color generateColorFromName(String name) { - if (name.isEmpty) return Colors.grey; - - // Use the hashCode of the name to generate a consistent color - int hash = name.hashCode; - - // Use the hash to generate RGB values - final r = (hash & 0xFF0000) >> 16; - final g = (hash & 0x00FF00) >> 8; - final b = hash & 0x0000FF; - - // Create a color with these RGB values - return Color.fromARGB(255, r, g, b); -} - -/// Darkens a color by a percentage (0.0 to 1.0) -Color darken(Color color, [double amount = 0.3]) { - assert(amount >= 0 && amount <= 1); - - final hsl = HSLColor.fromColor(color); - final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); - - return darkened.toColor(); -} - -/// Lightens a color by a percentage (0.0 to 1.0) -Color lighten(Color color, [double amount = 0.3]) { - assert(amount >= 0 && amount <= 1); - - final hsl = HSLColor.fromColor(color); - final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); - - return lightened.toColor(); -} diff --git a/dialer/lib/presentation/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart index 137e7cf..46f04b5 100644 --- a/dialer/lib/presentation/features/call/call_page.dart +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'dart:typed_data'; import '../../../domain/services/call_service.dart'; import '../../../domain/services/obfuscate_service.dart'; -import '../../../core/utils/color_utils.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; // import '../../../presentation/common/widgets/obfuscated_avatar.dart'; class CallPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/call/incoming_call_page.dart b/dialer/lib/presentation/features/call/incoming_call_page.dart index 1abe72d..e1eae95 100644 --- a/dialer/lib/presentation/features/call/incoming_call_page.dart +++ b/dialer/lib/presentation/features/call/incoming_call_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'dart:typed_data'; import '../../../domain/services/call_service.dart'; import '../../../domain/services/obfuscate_service.dart'; -import '../../../core/utils/color_utils.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; import 'call_page.dart'; class IncomingCallPage extends StatefulWidget { diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index c0af877..ff723fe 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import '../../../../domain/services/obfuscate_service.dart'; -import '../../../../core/utils/color_utils.dart'; +import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; import '../contact_state.dart'; import 'add_contact_button.dart'; import 'contact_modal.dart'; -- 2.45.2