Refactor obfuscate service and avatar widget; move to domain layer and remove unused files
- Moved ObfuscateService and ObfuscatedAvatar from services to domain/services. - Updated references to use globals for stealth mode instead of AppConfig. - Removed old obfuscated_avatar.dart file and updated imports in call and incoming call pages. - Cleaned up imports in composition, contact, and history pages to reflect new structure. - Deleted unused BlockService, CallService, ContactService, AsymmetricCryptoService, and QRScanner classes.
This commit is contained in:
parent
f89c5440fc
commit
612cccc381
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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 {
|
class CallService {
|
||||||
static const MethodChannel _channel = MethodChannel('call_service');
|
static const MethodChannel _channel = MethodChannel('call_service');
|
||||||
@ -9,20 +10,7 @@ class CallService {
|
|||||||
|
|
||||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
// Private constructor
|
CallService() {
|
||||||
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 {
|
_channel.setMethodCallHandler((call) async {
|
||||||
final context = navigatorKey.currentContext;
|
final context = navigatorKey.currentContext;
|
||||||
print('CallService: Received method ${call.method} with args ${call.arguments}');
|
print('CallService: Received method ${call.method} with args ${call.arguments}');
|
||||||
@ -33,36 +21,19 @@ class CallService {
|
|||||||
|
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "callAdded":
|
case "callAdded":
|
||||||
_handleCallAdded(context, call.arguments);
|
final phoneNumber = call.arguments["callId"] as String;
|
||||||
break;
|
final state = call.arguments["state"] as String;
|
||||||
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:', '');
|
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
|
||||||
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
||||||
|
|
||||||
if (state == "ringing") {
|
if (state == "ringing") {
|
||||||
_navigateToIncomingCallPage(context);
|
_navigateToIncomingCallPage(context);
|
||||||
} else {
|
} else {
|
||||||
_navigateToCallPage(context);
|
_navigateToCallPage(context);
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
case "callStateChanged":
|
||||||
void _handleCallStateChanged(BuildContext context, dynamic arguments) {
|
final state = call.arguments["state"] as String;
|
||||||
final state = arguments["state"] as String;
|
|
||||||
print('CallService: State changed to $state');
|
print('CallService: State changed to $state');
|
||||||
|
|
||||||
if (state == "disconnected" || state == "disconnecting") {
|
if (state == "disconnected" || state == "disconnecting") {
|
||||||
_closeCallPage(context);
|
_closeCallPage(context);
|
||||||
} else if (state == "active" || state == "dialing") {
|
} else if (state == "active" || state == "dialing") {
|
||||||
@ -70,12 +41,15 @@ class CallService {
|
|||||||
} else if (state == "ringing") {
|
} else if (state == "ringing") {
|
||||||
_navigateToIncomingCallPage(context);
|
_navigateToIncomingCallPage(context);
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
case "callEnded":
|
||||||
void _handleCallEnded(BuildContext context) {
|
case "callRemoved":
|
||||||
print('CallService: Call ended/removed');
|
print('CallService: Call ended/removed');
|
||||||
_closeCallPage(context);
|
_closeCallPage(context);
|
||||||
currentPhoneNumber = null;
|
currentPhoneNumber = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToCallPage(BuildContext context) {
|
void _navigateToCallPage(BuildContext context) {
|
||||||
@ -83,20 +57,20 @@ class CallService {
|
|||||||
print('CallService: CallPage already visible, skipping navigation');
|
print('CallService: CallPage already visible, skipping navigation');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('CallService: Navigating to CallPage');
|
print('CallService: Navigating to CallPage');
|
||||||
Navigator.pushReplacementNamed(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
'/call',
|
MaterialPageRoute(
|
||||||
arguments: {
|
settings: const RouteSettings(name: '/call'),
|
||||||
'displayName': currentPhoneNumber!,
|
builder: (context) => CallPage(
|
||||||
'phoneNumber': currentPhoneNumber!,
|
displayName: currentPhoneNumber!,
|
||||||
'thumbnail': null,
|
phoneNumber: currentPhoneNumber!,
|
||||||
}
|
thumbnail: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_isCallPageVisible = false;
|
_isCallPageVisible = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
_isCallPageVisible = true;
|
_isCallPageVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,20 +79,20 @@ class CallService {
|
|||||||
print('CallService: IncomingCallPage already visible, skipping navigation');
|
print('CallService: IncomingCallPage already visible, skipping navigation');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('CallService: Navigating to IncomingCallPage');
|
print('CallService: Navigating to IncomingCallPage');
|
||||||
Navigator.pushNamed(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
'/incoming_call',
|
MaterialPageRoute(
|
||||||
arguments: {
|
settings: const RouteSettings(name: '/incoming_call'),
|
||||||
'displayName': currentPhoneNumber!,
|
builder: (context) => IncomingCallPage(
|
||||||
'phoneNumber': currentPhoneNumber!,
|
displayName: currentPhoneNumber!,
|
||||||
'thumbnail': null,
|
phoneNumber: currentPhoneNumber!,
|
||||||
}
|
thumbnail: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_isCallPageVisible = false;
|
_isCallPageVisible = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
_isCallPageVisible = true;
|
_isCallPageVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +101,6 @@ class CallService {
|
|||||||
print('CallService: CallPage not visible, skipping pop');
|
print('CallService: CallPage not visible, skipping pop');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
print('CallService: Popping CallPage');
|
print('CallService: Popping CallPage');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@ -146,7 +119,6 @@ class CallService {
|
|||||||
print('CallService: Making GSM call to $phoneNumber');
|
print('CallService: Making GSM call to $phoneNumber');
|
||||||
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||||
print('CallService: makeGsmCall result: $result');
|
print('CallService: makeGsmCall result: $result');
|
||||||
|
|
||||||
if (result["status"] != "calling") {
|
if (result["status"] != "calling") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Failed to initiate call")),
|
SnackBar(content: Text("Failed to initiate call")),
|
||||||
@ -166,7 +138,6 @@ class CallService {
|
|||||||
print('CallService: Hanging up call');
|
print('CallService: Hanging up call');
|
||||||
final result = await _channel.invokeMethod('hangUpCall');
|
final result = await _channel.invokeMethod('hangUpCall');
|
||||||
print('CallService: hangUpCall result: $result');
|
print('CallService: hangUpCall result: $result');
|
||||||
|
|
||||||
if (result["status"] != "ended") {
|
if (result["status"] != "ended") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Failed to end call")),
|
SnackBar(content: Text("Failed to end call")),
|
||||||
|
@ -1,296 +1,170 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'package:flutter/services.dart';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:pointycastle/export.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:pointycastle/asymmetric/api.dart';
|
|
||||||
import 'package:asn1lib/asn1lib.dart';
|
|
||||||
|
|
||||||
/// Service for handling asymmetric cryptography operations
|
|
||||||
class AsymmetricCryptoService {
|
class AsymmetricCryptoService {
|
||||||
static const String _privateKeyTag = 'private_key';
|
static const MethodChannel _channel = MethodChannel('com.example.keystore');
|
||||||
static const String _publicKeyTag = 'public_key';
|
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
|
||||||
|
final String _aliasPrefix = 'icing_';
|
||||||
|
final Uuid _uuid = Uuid();
|
||||||
|
|
||||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
/// 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';
|
||||||
|
|
||||||
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>? _currentKeyPair;
|
// Invoke native method to generate the key pair
|
||||||
|
await _channel.invokeMethod('generateKeyPair', {'alias': alias});
|
||||||
|
|
||||||
/// Initialize with the default key pair, creating one if it doesn't exist
|
// 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 {
|
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 {
|
try {
|
||||||
// Try to load existing keys
|
// Retrieve existing keys
|
||||||
final privateKeyStr = await _secureStorage.read(key: _privateKeyTag);
|
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||||
final publicKeyStr = await _secureStorage.read(key: _publicKeyTag);
|
if (existingKeys == null) {
|
||||||
|
throw Exception("No keys found to update.");
|
||||||
|
}
|
||||||
|
|
||||||
if (privateKeyStr != null && publicKeyStr != null) {
|
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||||
try {
|
|
||||||
// Parse existing keys
|
|
||||||
final privateKey = _parsePrivateKeyFromPem(privateKeyStr);
|
|
||||||
final publicKey = _parsePublicKeyFromPem(publicKeyStr);
|
|
||||||
|
|
||||||
_currentKeyPair = AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(publicKey, privateKey);
|
// Find the key with the specified alias
|
||||||
debugPrint('Loaded existing key pair successfully');
|
bool keyFound = false;
|
||||||
return;
|
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) {
|
} catch (e) {
|
||||||
debugPrint('Error parsing stored keys: $e');
|
throw Exception("Failed to update key label: $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<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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
class ObfuscateService {
|
||||||
// Private constructor
|
// Private constructor
|
||||||
@ -14,7 +20,7 @@ class ObfuscateService {
|
|||||||
|
|
||||||
// Public method to obfuscate data
|
// Public method to obfuscate data
|
||||||
String obfuscateData(String data) {
|
String obfuscateData(String data) {
|
||||||
if (AppConfig.isStealthMode) {
|
if (globals.isStealthMode) {
|
||||||
return _obfuscateData(data);
|
return _obfuscateData(data);
|
||||||
} else {
|
} else {
|
||||||
return data;
|
return data;
|
||||||
@ -34,3 +40,52 @@ class ObfuscateService {
|
|||||||
return '';
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import 'dart:typed_data';
|
|||||||
import '../../../domain/services/call_service.dart';
|
import '../../../domain/services/call_service.dart';
|
||||||
import '../../../domain/services/obfuscate_service.dart';
|
import '../../../domain/services/obfuscate_service.dart';
|
||||||
import '../../../core/utils/color_utils.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 {
|
class CallPage extends StatefulWidget {
|
||||||
final String displayName;
|
final String displayName;
|
||||||
|
@ -4,7 +4,6 @@ import 'dart:typed_data';
|
|||||||
import '../../../domain/services/call_service.dart';
|
import '../../../domain/services/call_service.dart';
|
||||||
import '../../../domain/services/obfuscate_service.dart';
|
import '../../../domain/services/obfuscate_service.dart';
|
||||||
import '../../../core/utils/color_utils.dart';
|
import '../../../core/utils/color_utils.dart';
|
||||||
import '../../../presentation/common/widgets/obfuscated_avatar.dart';
|
|
||||||
import 'call_page.dart';
|
import 'call_page.dart';
|
||||||
|
|
||||||
class IncomingCallPage extends StatefulWidget {
|
class IncomingCallPage extends StatefulWidget {
|
||||||
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../../services/contact_service.dart';
|
import '../../../services/contact_service.dart';
|
||||||
import '../../../services/obfuscate_service.dart';
|
import '../../../domain/services/obfuscate_service.dart';
|
||||||
import '../../../services/call_service.dart';
|
import '../../../domain/services/call_service.dart';
|
||||||
import '../contacts/widgets/add_contact_button.dart';
|
import '../contacts/widgets/add_contact_button.dart';
|
||||||
|
|
||||||
class CompositionPage extends StatefulWidget {
|
class CompositionPage extends StatefulWidget {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../contacts/contact_state.dart';
|
import '../contacts/contact_state.dart';
|
||||||
import '../contacts/widgets/alphabet_scroll_page.dart';
|
import '../contacts/widgets/alphabet_scroll_page.dart';
|
||||||
import '../../common/widgets/loading_indicator.dart';
|
|
||||||
import '../../../domain/services/obfuscate_service.dart';
|
|
||||||
|
|
||||||
class ContactPage extends StatefulWidget {
|
class ContactPage extends StatefulWidget {
|
||||||
const ContactPage({super.key});
|
const ContactPage({super.key});
|
||||||
@ -12,8 +10,6 @@ class ContactPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ContactPageState extends State<ContactPage> {
|
class _ContactPageState extends State<ContactPage> {
|
||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final contactState = ContactState.of(context);
|
final contactState = ContactState.of(context);
|
||||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import '../../../../domain/services/obfuscate_service.dart';
|
import '../../../../domain/services/obfuscate_service.dart';
|
||||||
import '../../../../core/utils/color_utils.dart';
|
import '../../../../core/utils/color_utils.dart';
|
||||||
import '../../../common/widgets/obfuscated_avatar.dart';
|
|
||||||
import '../contact_state.dart';
|
import '../contact_state.dart';
|
||||||
import 'add_contact_button.dart';
|
import 'add_contact_button.dart';
|
||||||
import 'contact_modal.dart';
|
import 'contact_modal.dart';
|
||||||
|
@ -3,10 +3,10 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../../../widgets/username_color_generator.dart';
|
import '../../../../widgets/username_color_generator.dart';
|
||||||
import '../../../../widgets/color_darkener.dart';
|
import '../../../../widgets/color_darkener.dart';
|
||||||
import '../../../../services/obfuscate_service.dart';
|
import '../../../../domain/services/obfuscate_service.dart';
|
||||||
import '../../../../services/block_service.dart';
|
import '../../../../domain/services/block_service.dart';
|
||||||
import '../../../../services/contact_service.dart';
|
import '../../../../domain/services/contact_service.dart';
|
||||||
import '../../../../services/call_service.dart';
|
import '../../../../domain/services/call_service.dart';
|
||||||
|
|
||||||
class ContactModal extends StatefulWidget {
|
class ContactModal extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_contacts/contact.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 {
|
class QRCodeButton extends StatelessWidget {
|
||||||
final List<Contact> contacts;
|
final List<Contact> contacts;
|
||||||
|
@ -5,11 +5,11 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:url_launcher/url_launcher.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/color_darkener.dart';
|
||||||
import '../../../widgets/username_color_generator.dart';
|
import '../../../widgets/username_color_generator.dart';
|
||||||
import '../../../services/block_service.dart';
|
import '../../../domain/services/block_service.dart';
|
||||||
import '../../../services/call_service.dart';
|
import '../../../domain/services/call_service.dart';
|
||||||
import '../contacts/contact_state.dart';
|
import '../contacts/contact_state.dart';
|
||||||
import '../contacts/widgets/contact_modal.dart';
|
import '../contacts/widgets/contact_modal.dart';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class ManageKeysPage extends StatefulWidget {
|
||||||
const ManageKeysPage({Key? key}) : super(key: key);
|
const ManageKeysPage({Key? key}) : super(key: key);
|
||||||
|
@ -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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user