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