Refactor obfuscate service and avatar widget; move to domain layer and remove unused files
Some checks failed
/ mirror (push) Successful in 5s
/ build (push) Has been cancelled
/ build-stealth (push) Has been cancelled

- Moved ObfuscateService and ObfuscatedAvatar from services to domain/services.
- Updated references to use globals for stealth mode instead of AppConfig.
- Removed old obfuscated_avatar.dart file and updated imports in call and incoming call pages.
- Cleaned up imports in composition, contact, and history pages to reflect new structure.
- Deleted unused BlockService, CallService, ContactService, AsymmetricCryptoService, and QRScanner classes.
This commit is contained in:
AlexisDanlos 2025-04-05 01:48:34 +02:00
parent f89c5440fc
commit 612cccc381
19 changed files with 271 additions and 1066 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:typed_data';
import '../../presentation/features/call/call_page.dart';
import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
@ -9,20 +10,7 @@ class CallService {
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Private constructor
CallService._privateConstructor() {
_initializeMethodCallHandler();
}
// Singleton instance
static final CallService _instance = CallService._privateConstructor();
// Factory constructor to return the same instance
factory CallService() {
return _instance;
}
void _initializeMethodCallHandler() {
CallService() {
_channel.setMethodCallHandler((call) async {
final context = navigatorKey.currentContext;
print('CallService: Received method ${call.method} with args ${call.arguments}');
@ -33,70 +21,56 @@ class CallService {
switch (call.method) {
case "callAdded":
_handleCallAdded(context, call.arguments);
final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
break;
case "callStateChanged":
_handleCallStateChanged(context, call.arguments);
final state = call.arguments["state"] as String;
print('CallService: State changed to $state');
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
break;
case "callEnded":
case "callRemoved":
_handleCallEnded(context);
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
break;
}
});
}
void _handleCallAdded(BuildContext context, dynamic arguments) {
final phoneNumber = arguments["callId"] as String;
final state = arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
}
void _handleCallStateChanged(BuildContext context, dynamic arguments) {
final state = arguments["state"] as String;
print('CallService: State changed to $state');
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
}
void _handleCallEnded(BuildContext context) {
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
}
void _navigateToCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
print('CallService: CallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to CallPage');
Navigator.pushReplacementNamed(
context,
'/call',
arguments: {
'displayName': currentPhoneNumber!,
'phoneNumber': currentPhoneNumber!,
'thumbnail': null,
}
Navigator.pushReplacement(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
@ -105,20 +79,20 @@ class CallService {
print('CallService: IncomingCallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to IncomingCallPage');
Navigator.pushNamed(
Navigator.push(
context,
'/incoming_call',
arguments: {
'displayName': currentPhoneNumber!,
'phoneNumber': currentPhoneNumber!,
'thumbnail': null,
}
MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
@ -127,7 +101,6 @@ class CallService {
print('CallService: CallPage not visible, skipping pop');
return;
}
if (Navigator.canPop(context)) {
print('CallService: Popping CallPage');
Navigator.pop(context);
@ -146,7 +119,6 @@ class CallService {
print('CallService: Making GSM call to $phoneNumber');
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
print('CallService: makeGsmCall result: $result');
if (result["status"] != "calling") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to initiate call")),
@ -166,7 +138,6 @@ class CallService {
print('CallService: Hanging up call');
final result = await _channel.invokeMethod('hangUpCall');
print('CallService: hangUpCall result: $result');
if (result["status"] != "ended") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to end call")),
@ -180,4 +151,4 @@ class CallService {
rethrow;
}
}
}
}

View File

@ -1,296 +1,170 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:pointycastle/export.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:asn1lib/asn1lib.dart';
import 'package:uuid/uuid.dart';
/// Service for handling asymmetric cryptography operations
class AsymmetricCryptoService {
static const String _privateKeyTag = 'private_key';
static const String _publicKeyTag = 'public_key';
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>? _currentKeyPair;
/// Initialize with the default key pair, creating one if it doesn't exist
Future<void> initializeDefaultKeyPair() async {
static const MethodChannel _channel = MethodChannel('com.example.keystore');
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
final String _aliasPrefix = 'icing_';
final Uuid _uuid = Uuid();
/// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
Future<String> generateKeyPair({String? label}) async {
try {
// Try to load existing keys
final privateKeyStr = await _secureStorage.read(key: _privateKeyTag);
final publicKeyStr = await _secureStorage.read(key: _publicKeyTag);
// Generate a unique identifier for the key
final String uuid = _uuid.v4();
final String alias = '$_aliasPrefix$uuid';
// Invoke native method to generate the key pair
await _channel.invokeMethod('generateKeyPair', {'alias': alias});
// Store key metadata securely
final Map<String, dynamic> keyMetadata = {
'alias': alias,
'label': label ?? 'Key $uuid',
'created_at': DateTime.now().toIso8601String(),
};
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
List<dynamic> keysList = existingKeys != null ? jsonDecode(existingKeys) : [];
// Add the new key
keysList.add(keyMetadata);
// Save updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
return alias;
} on PlatformException catch (e) {
throw Exception("Failed to generate key pair: ${e.message}");
}
}
/// Signs data using the specified key alias.
Future<String> signData(String alias, String data) async {
try {
final String signature = await _channel.invokeMethod('signData', {
'alias': alias,
'data': data,
});
return signature;
} on PlatformException catch (e) {
throw Exception("Failed to sign data with alias '$alias': ${e.message}");
}
}
/// Retrieves the public key for the specified alias.
Future<String> getPublicKey(String alias) async {
try {
final String publicKey = await _channel.invokeMethod('getPublicKey', {
'alias': alias,
});
return publicKey;
} on PlatformException catch (e) {
throw Exception("Failed to retrieve public key: ${e.message}");
}
}
/// Deletes the key pair associated with the specified alias and removes its metadata.
Future<void> deleteKeyPair(String alias) async {
try {
await _channel.invokeMethod('deleteKeyPair', {'alias': alias});
if (privateKeyStr != null && publicKeyStr != null) {
try {
// Parse existing keys
final privateKey = _parsePrivateKeyFromPem(privateKeyStr);
final publicKey = _parsePublicKeyFromPem(publicKeyStr);
_currentKeyPair = AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(publicKey, privateKey);
debugPrint('Loaded existing key pair successfully');
return;
} catch (e) {
debugPrint('Error parsing stored keys: $e');
// Continue to generate new keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys != null) {
List<dynamic> keysList = jsonDecode(existingKeys);
keysList.removeWhere((key) => key['alias'] == alias);
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
}
} on PlatformException catch (e) {
throw Exception("Failed to delete key pair: ${e.message}");
}
}
/// Retrieves all stored key metadata.
Future<List<Map<String, dynamic>>> getAllKeys() async {
try {
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
print("No keys found");
return [];
}
List<dynamic> keysList = jsonDecode(existingKeys);
return keysList.cast<Map<String, dynamic>>();
} catch (e) {
throw Exception("Failed to retrieve keys: $e");
}
}
/// Checks if a key pair exists for the given alias.
Future<bool> keyPairExists(String alias) async {
try {
final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias});
return exists;
} on PlatformException catch (e) {
throw Exception("Failed to check key pair existence: ${e.message}");
}
}
/// Initializes the default key pair if it doesn't exist.
Future<void> initializeDefaultKeyPair() async {
const String defaultAlias = 'icing_default';
final List<Map<String, dynamic>> keys = await getAllKeys();
// Check if the key exists in metadata
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
if (!defaultKeyExists) {
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
final Map<String, dynamic> keyMetadata = {
'alias': defaultAlias,
'label': 'Default Key',
'created_at': DateTime.now().toIso8601String(),
};
keys.add(keyMetadata);
await _secureStorage.write(key: 'keys', value: jsonEncode(keys));
}
}
/// Updates the label of a key with the specified alias.
///
/// [alias]: The unique alias of the key to update.
/// [newLabel]: The new label to assign to the key.
///
/// Throws an exception if the key is not found or the update fails.
Future<void> updateKeyLabel(String alias, String newLabel) async {
try {
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
throw Exception("No keys found to update.");
}
List<dynamic> keysList = jsonDecode(existingKeys);
// Find the key with the specified alias
bool keyFound = false;
for (var key in keysList) {
if (key['alias'] == alias) {
key['label'] = newLabel;
keyFound = true;
break;
}
}
// Generate new key pair
await generateAndStoreKeyPair();
if (!keyFound) {
throw Exception("Key with alias \"$alias\" not found.");
}
// Save the updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
} catch (e) {
debugPrint('Error initializing key pair: $e');
// Instead of rethrowing, we'll continue without encryption capability
// This ensures the app doesn't crash during startup
throw Exception("Failed to update key label: $e");
}
}
/// Generate a new key pair and store it securely
Future<void> 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<RSAPublicKey>(_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<RSAPrivateKey>(_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<String?> getPublicKeyPem() async {
return await _secureStorage.read(key: _publicKeyTag);
}
// Simpler RSA key pair generation that doesn't use FortunaRandom
Future<AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>> _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<RSAPublicKey, RSAPrivateKey>;
}
// Original method (but no longer used directly)
static AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> _generateRSAKeyPair(int bitLength) {
try {
final secureRandom = _SecureRandom();
final keyGen = RSAKeyGenerator()
..init(ParametersWithRandom(
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
secureRandom,
));
return keyGen.generateKeyPair() as AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>;
} 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;
}
}

View File

@ -1,4 +1,10 @@
import '../../core/config/app_config.dart';
// lib/services/obfuscate_service.dart
import 'package:dialer/widgets/color_darkener.dart';
import '../../globals.dart' as globals;
import 'dart:ui';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class ObfuscateService {
// Private constructor
@ -14,7 +20,7 @@ class ObfuscateService {
// Public method to obfuscate data
String obfuscateData(String data) {
if (AppConfig.isStealthMode) {
if (globals.isStealthMode) {
return _obfuscateData(data);
} else {
return data;
@ -34,3 +40,52 @@ class ObfuscateService {
return '';
}
}
class ObfuscatedAvatar extends StatelessWidget {
final Uint8List? imageBytes;
final double radius;
final Color backgroundColor;
final String? fallbackInitial;
const ObfuscatedAvatar({
Key? key,
required this.imageBytes,
this.radius = 25,
this.backgroundColor = Colors.grey,
this.fallbackInitial,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval(
child: ImageFiltered(
imageFilter: globals.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
width: radius * 2,
height: radius * 2,
),
),
);
} else {
return CircleAvatar(
radius: radius,
backgroundColor: backgroundColor,
child: Text(
fallbackInitial != null && fallbackInitial!.isNotEmpty
? fallbackInitial![0].toUpperCase()
: '?',
style: TextStyle(
color: darken(backgroundColor),
fontSize: radius,
),
),
);
}
}
}

View File

@ -1,53 +0,0 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import '../../../core/config/app_config.dart';
import '../../../core/utils/color_utils.dart';
class ObfuscatedAvatar extends StatelessWidget {
final Uint8List? imageBytes;
final double radius;
final Color backgroundColor;
final String? fallbackInitial;
const ObfuscatedAvatar({
Key? key,
required this.imageBytes,
this.radius = 25,
this.backgroundColor = Colors.grey,
this.fallbackInitial,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval(
child: ImageFiltered(
imageFilter: AppConfig.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
width: radius * 2,
height: radius * 2,
),
),
);
} else {
return CircleAvatar(
radius: radius,
backgroundColor: backgroundColor,
child: Text(
fallbackInitial != null && fallbackInitial!.isNotEmpty
? fallbackInitial![0].toUpperCase()
: '?',
style: TextStyle(
color: darken(backgroundColor),
fontSize: radius,
),
),
);
}
}
}

View File

@ -4,7 +4,7 @@ import 'dart:typed_data';
import '../../../domain/services/call_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../core/utils/color_utils.dart';
import '../../../presentation/common/widgets/obfuscated_avatar.dart';
// import '../../../presentation/common/widgets/obfuscated_avatar.dart';
class CallPage extends StatefulWidget {
final String displayName;

View File

@ -4,7 +4,6 @@ import 'dart:typed_data';
import '../../../domain/services/call_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../core/utils/color_utils.dart';
import '../../../presentation/common/widgets/obfuscated_avatar.dart';
import 'call_page.dart';
class IncomingCallPage extends StatefulWidget {

View File

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/contact_service.dart';
import '../../../services/obfuscate_service.dart';
import '../../../services/call_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../domain/services/call_service.dart';
import '../contacts/widgets/add_contact_button.dart';
class CompositionPage extends StatefulWidget {

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/alphabet_scroll_page.dart';
import '../../common/widgets/loading_indicator.dart';
import '../../../domain/services/obfuscate_service.dart';
class ContactPage extends StatefulWidget {
const ContactPage({super.key});
@ -12,8 +10,6 @@ class ContactPage extends StatefulWidget {
}
class _ContactPageState extends State<ContactPage> {
final ObfuscateService _obfuscateService = ObfuscateService();
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../../domain/services/obfuscate_service.dart';
import '../../../../core/utils/color_utils.dart';
import '../../../common/widgets/obfuscated_avatar.dart';
import '../contact_state.dart';
import 'add_contact_button.dart';
import 'contact_modal.dart';

View File

@ -3,10 +3,10 @@ import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../widgets/username_color_generator.dart';
import '../../../../widgets/color_darkener.dart';
import '../../../../services/obfuscate_service.dart';
import '../../../../services/block_service.dart';
import '../../../../services/contact_service.dart';
import '../../../../services/call_service.dart';
import '../../../../domain/services/obfuscate_service.dart';
import '../../../../domain/services/block_service.dart';
import '../../../../domain/services/contact_service.dart';
import '../../../../domain/services/call_service.dart';
class ContactModal extends StatefulWidget {
final Contact contact;

View File

@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_contacts/contact.dart';
import 'package:dialer/services/contact_service.dart';
import 'package:dialer/domain/services/contact_service.dart';
class QRCodeButton extends StatelessWidget {
final List<Contact> contacts;

View File

@ -5,11 +5,11 @@ import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/obfuscate_service.dart';
import '../../../domain/services/obfuscate_service.dart';
import '../../../widgets/color_darkener.dart';
import '../../../widgets/username_color_generator.dart';
import '../../../services/block_service.dart';
import '../../../services/call_service.dart';
import '../../../domain/services/block_service.dart';
import '../../../domain/services/call_service.dart';
import '../contacts/contact_state.dart';
import '../contacts/widgets/contact_modal.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart';
class ManageKeysPage extends StatefulWidget {
const ManageKeysPage({Key? key}) : super(key: key);

View File

@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing blocked phone numbers
class BlockService {
static const String _blockedNumbersKey = 'blocked_numbers';
// Private constructor
BlockService._privateConstructor();
// Singleton instance
static final BlockService _instance = BlockService._privateConstructor();
// Factory constructor to return the same instance
factory BlockService() {
return _instance;
}
/// Block a phone number
Future<bool> 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<bool> 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<bool> 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<List<String>> getBlockedNumbers() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList(_blockedNumbersKey) ?? [];
} catch (e) {
debugPrint('Error getting blocked numbers: $e');
return [];
}
}
}

View File

@ -1,154 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../presentation/features/call/call_page.dart';
import '../presentation/features/call/incoming_call_page.dart'; // Import the new page
class CallService {
static const MethodChannel _channel = MethodChannel('call_service');
static String? currentPhoneNumber;
static bool _isCallPageVisible = false;
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
CallService() {
_channel.setMethodCallHandler((call) async {
final context = navigatorKey.currentContext;
print('CallService: Received method ${call.method} with args ${call.arguments}');
if (context == null) {
print('CallService: Navigator context is null, cannot navigate');
return;
}
switch (call.method) {
case "callAdded":
final phoneNumber = call.arguments["callId"] as String;
final state = call.arguments["state"] as String;
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
if (state == "ringing") {
_navigateToIncomingCallPage(context);
} else {
_navigateToCallPage(context);
}
break;
case "callStateChanged":
final state = call.arguments["state"] as String;
print('CallService: State changed to $state');
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(context);
} else if (state == "active" || state == "dialing") {
_navigateToCallPage(context);
} else if (state == "ringing") {
_navigateToIncomingCallPage(context);
}
break;
case "callEnded":
case "callRemoved":
print('CallService: Call ended/removed');
_closeCallPage(context);
currentPhoneNumber = null;
break;
}
});
}
void _navigateToCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
print('CallService: CallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to CallPage');
Navigator.pushReplacement(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/call'),
builder: (context) => CallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _navigateToIncomingCallPage(BuildContext context) {
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
print('CallService: IncomingCallPage already visible, skipping navigation');
return;
}
print('CallService: Navigating to IncomingCallPage');
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/incoming_call'),
builder: (context) => IncomingCallPage(
displayName: currentPhoneNumber!,
phoneNumber: currentPhoneNumber!,
thumbnail: null,
),
),
).then((_) {
_isCallPageVisible = false;
});
_isCallPageVisible = true;
}
void _closeCallPage(BuildContext context) {
if (!_isCallPageVisible) {
print('CallService: CallPage not visible, skipping pop');
return;
}
if (Navigator.canPop(context)) {
print('CallService: Popping CallPage');
Navigator.pop(context);
_isCallPageVisible = false;
}
}
Future<void> 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<void> 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;
}
}
}

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:qr_flutter/qr_flutter.dart';
class ContactService {
Future<List<Contact>> fetchContacts() async {
if (await FlutterContacts.requestPermission()) {
List<Contact> contacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
return contacts;
} else {
// Permission denied
return [];
}
}
Future<List<Contact>> fetchFavoriteContacts() async {
if (await FlutterContacts.requestPermission()) {
// Get all contacts and filter for favorites
List<Contact> allContacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
return allContacts.where((c) => c.isStarred).toList();
} else {
// Permission denied
return [];
}
}
Future<Contact?> 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'),
),
],
);
},
);
}
}

View File

@ -1,170 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
class AsymmetricCryptoService {
static const MethodChannel _channel = MethodChannel('com.example.keystore');
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
final String _aliasPrefix = 'icing_';
final Uuid _uuid = Uuid();
/// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
Future<String> generateKeyPair({String? label}) async {
try {
// Generate a unique identifier for the key
final String uuid = _uuid.v4();
final String alias = '$_aliasPrefix$uuid';
// Invoke native method to generate the key pair
await _channel.invokeMethod('generateKeyPair', {'alias': alias});
// Store key metadata securely
final Map<String, dynamic> keyMetadata = {
'alias': alias,
'label': label ?? 'Key $uuid',
'created_at': DateTime.now().toIso8601String(),
};
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
List<dynamic> keysList = existingKeys != null ? jsonDecode(existingKeys) : [];
// Add the new key
keysList.add(keyMetadata);
// Save updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
return alias;
} on PlatformException catch (e) {
throw Exception("Failed to generate key pair: ${e.message}");
}
}
/// Signs data using the specified key alias.
Future<String> signData(String alias, String data) async {
try {
final String signature = await _channel.invokeMethod('signData', {
'alias': alias,
'data': data,
});
return signature;
} on PlatformException catch (e) {
throw Exception("Failed to sign data with alias '$alias': ${e.message}");
}
}
/// Retrieves the public key for the specified alias.
Future<String> getPublicKey(String alias) async {
try {
final String publicKey = await _channel.invokeMethod('getPublicKey', {
'alias': alias,
});
return publicKey;
} on PlatformException catch (e) {
throw Exception("Failed to retrieve public key: ${e.message}");
}
}
/// Deletes the key pair associated with the specified alias and removes its metadata.
Future<void> deleteKeyPair(String alias) async {
try {
await _channel.invokeMethod('deleteKeyPair', {'alias': alias});
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys != null) {
List<dynamic> keysList = jsonDecode(existingKeys);
keysList.removeWhere((key) => key['alias'] == alias);
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
}
} on PlatformException catch (e) {
throw Exception("Failed to delete key pair: ${e.message}");
}
}
/// Retrieves all stored key metadata.
Future<List<Map<String, dynamic>>> getAllKeys() async {
try {
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
print("No keys found");
return [];
}
List<dynamic> keysList = jsonDecode(existingKeys);
return keysList.cast<Map<String, dynamic>>();
} catch (e) {
throw Exception("Failed to retrieve keys: $e");
}
}
/// Checks if a key pair exists for the given alias.
Future<bool> keyPairExists(String alias) async {
try {
final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias});
return exists;
} on PlatformException catch (e) {
throw Exception("Failed to check key pair existence: ${e.message}");
}
}
/// Initializes the default key pair if it doesn't exist.
Future<void> initializeDefaultKeyPair() async {
const String defaultAlias = 'icing_default';
final List<Map<String, dynamic>> keys = await getAllKeys();
// Check if the key exists in metadata
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
if (!defaultKeyExists) {
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
final Map<String, dynamic> keyMetadata = {
'alias': defaultAlias,
'label': 'Default Key',
'created_at': DateTime.now().toIso8601String(),
};
keys.add(keyMetadata);
await _secureStorage.write(key: 'keys', value: jsonEncode(keys));
}
}
/// Updates the label of a key with the specified alias.
///
/// [alias]: The unique alias of the key to update.
/// [newLabel]: The new label to assign to the key.
///
/// Throws an exception if the key is not found or the update fails.
Future<void> updateKeyLabel(String alias, String newLabel) async {
try {
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
throw Exception("No keys found to update.");
}
List<dynamic> keysList = jsonDecode(existingKeys);
// Find the key with the specified alias
bool keyFound = false;
for (var key in keysList) {
if (key['alias'] == alias) {
key['label'] = newLabel;
keyFound = true;
break;
}
}
if (!keyFound) {
throw Exception("Key with alias \"$alias\" not found.");
}
// Save the updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
} catch (e) {
throw Exception("Failed to update key label: $e");
}
}
}

View File

@ -1,91 +0,0 @@
// lib/services/obfuscate_service.dart
import 'package:dialer/widgets/color_darkener.dart';
import '../../globals.dart' as globals;
import 'dart:ui';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class ObfuscateService {
// Private constructor
ObfuscateService._privateConstructor();
// Singleton instance
static final ObfuscateService _instance = ObfuscateService._privateConstructor();
// Factory constructor to return the same instance
factory ObfuscateService() {
return _instance;
}
// Public method to obfuscate data
String obfuscateData(String data) {
if (globals.isStealthMode) {
return _obfuscateData(data);
} else {
return data;
}
}
// Private helper method for obfuscation logic
String _obfuscateData(String data) {
if (data.isNotEmpty) {
// Ensure the string has at least two characters to obfuscate
if (data.length == 1) {
return '${data[0]}';
} else {
return '${data[0]}...${data[data.length - 1]}';
}
}
return '';
}
}
class ObfuscatedAvatar extends StatelessWidget {
final Uint8List? imageBytes;
final double radius;
final Color backgroundColor;
final String? fallbackInitial;
const ObfuscatedAvatar({
Key? key,
required this.imageBytes,
this.radius = 25,
this.backgroundColor = Colors.grey,
this.fallbackInitial,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval(
child: ImageFiltered(
imageFilter: globals.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
width: radius * 2,
height: radius * 2,
),
),
);
} else {
return CircleAvatar(
radius: radius,
backgroundColor: backgroundColor,
child: Text(
fallbackInitial != null && fallbackInitial!.isNotEmpty
? fallbackInitial![0].toUpperCase()
: '?',
style: TextStyle(
color: darken(backgroundColor),
fontSize: radius,
),
),
);
}
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class QRCodeScannerScreen extends StatefulWidget {
const QRCodeScannerScreen({super.key});
@override
_QRCodeScannerScreenState createState() => _QRCodeScannerScreenState();
}
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
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<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
// Return the first barcode value
final String? code = barcodes.first.rawValue;
if (code != null) {
Navigator.pop(context, code);
}
}
},
),
);
}
}