feat: add block management features and sync with system blocklist
All checks were successful
/ build (push) Successful in 10m4s
/ build-stealth (push) Successful in 10m9s
/ mirror (push) Successful in 4s

This commit is contained in:
AlexisDanlos 2025-06-27 17:15:29 +02:00
parent ed07022916
commit 2e8f457889
3 changed files with 366 additions and 83 deletions

View File

@ -9,7 +9,10 @@ import android.database.Cursor
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.CallLog import android.provider.CallLog
import android.provider.BlockedNumberContract
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionInfo
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.icing.dialer.KeystoreHelper import com.icing.dialer.KeystoreHelper
@ -23,6 +26,7 @@ class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore" private val KEYSTORE_CHANNEL = "com.example.keystore"
private val CALLLOG_CHANNEL = "com.example.calllog" private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service" private val CALL_CHANNEL = "call_service"
private val BLOCK_CHANNEL = "com.example.block"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001 private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002 private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002
@ -240,6 +244,44 @@ class MainActivity : FlutterActivity() {
result.notImplemented() result.notImplemented()
} }
} }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BLOCK_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getBlockedNumbers" -> {
val blockedNumbers = getSystemBlockedNumbers()
result.success(blockedNumbers)
}
"blockNumber" -> {
val number = call.argument<String>("number")
if (number != null) {
val success = blockNumberInSystem(number)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "Number is required", null)
}
}
"unblockNumber" -> {
val number = call.argument<String>("number")
if (number != null) {
val success = unblockNumberFromSystem(number)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "Number is required", null)
}
}
"isNumberBlocked" -> {
val number = call.argument<String>("number")
if (number != null) {
val isBlocked = isNumberBlockedInSystem(number)
result.success(isBlocked)
} else {
result.error("INVALID_ARGUMENT", "Number is required", null)
}
}
else -> result.notImplemented()
}
}
} }
private fun isDefaultDialer(): Boolean { private fun isDefaultDialer(): Boolean {
@ -354,4 +396,111 @@ class MainActivity : FlutterActivity() {
} }
} }
} }
// Block management methods
private fun getSystemBlockedNumbers(): List<String> {
val blockedNumbers = mutableListOf<String>()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
val cursor = contentResolver.query(
BlockedNumberContract.BlockedNumbers.CONTENT_URI,
arrayOf(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER),
null,
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val number = it.getString(0)
if (number != null) {
blockedNumbers.add(number)
}
}
}
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to get system blocked numbers: ${e.message}")
}
return blockedNumbers
}
private fun blockNumberInSystem(number: String): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
val values = android.content.ContentValues().apply {
put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number)
}
val uri = contentResolver.insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, values)
uri != null
} else {
Log.w(TAG, "Current user cannot block numbers")
false
}
} else {
Log.w(TAG, "Block numbers API not available on this Android version")
false
}
} catch (e: Exception) {
Log.w(TAG, "Failed to block number $number: ${e.message}")
false
}
}
private fun unblockNumberFromSystem(number: String): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
val deletedRows = contentResolver.delete(
BlockedNumberContract.BlockedNumbers.CONTENT_URI,
"${BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER} = ?",
arrayOf(number)
)
deletedRows > 0
} else {
Log.w(TAG, "Current user cannot unblock numbers")
false
}
} else {
Log.w(TAG, "Block numbers API not available on this Android version")
false
}
} catch (e: Exception) {
Log.w(TAG, "Failed to unblock number $number: ${e.message}")
false
}
}
private fun isNumberBlockedInSystem(number: String): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
val cursor = contentResolver.query(
BlockedNumberContract.BlockedNumbers.CONTENT_URI,
arrayOf(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER),
"${BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER} = ?",
arrayOf(number),
null
)
cursor?.use {
return it.count > 0
}
false
} else {
false
}
} else {
false
}
} catch (e: Exception) {
Log.w(TAG, "Failed to check if number $number is blocked: ${e.message}")
false
}
}
} }

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing blocked phone numbers /// Service for managing blocked phone numbers
class BlockService { class BlockService {
static const String _blockedNumbersKey = 'blocked_numbers'; static const String _blockedNumbersKey = 'blocked_numbers';
static const MethodChannel _channel = MethodChannel('com.example.block');
// Private constructor // Private constructor
BlockService._privateConstructor(); BlockService._privateConstructor();
@ -16,46 +18,71 @@ class BlockService {
return _instance; return _instance;
} }
/// Block a phone number /// Block a phone number (syncs with system and app)
Future<bool> blockNumber(String phoneNumber) async { Future<bool> blockNumber(String phoneNumber) async {
try { try {
// First try to block in system
try {
await _channel.invokeMethod('blockNumber', {'number': phoneNumber});
} catch (e) {
debugPrint('Failed to block in system: $e');
}
// Always save to app storage as backup
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
// Don't add if already blocked // Don't add if already blocked
if (blockedNumbers.contains(phoneNumber)) { if (!blockedNumbers.contains(phoneNumber)) {
return true; blockedNumbers.add(phoneNumber);
await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} }
blockedNumbers.add(phoneNumber); return true; // Return true if either system or app blocking succeeded
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} catch (e) { } catch (e) {
debugPrint('Error blocking number: $e'); debugPrint('Error blocking number: $e');
return false; return false;
} }
} }
/// Unblock a phone number /// Unblock a phone number (syncs with system and app)
Future<bool> unblockNumber(String phoneNumber) async { Future<bool> unblockNumber(String phoneNumber) async {
try { try {
// First try to unblock from system
try {
await _channel.invokeMethod('unblockNumber', {'number': phoneNumber});
} catch (e) {
debugPrint('Failed to unblock from system: $e');
}
// Always remove from app storage
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
if (!blockedNumbers.contains(phoneNumber)) { if (blockedNumbers.contains(phoneNumber)) {
return true; blockedNumbers.remove(phoneNumber);
await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} }
blockedNumbers.remove(phoneNumber); return true;
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
} catch (e) { } catch (e) {
debugPrint('Error unblocking number: $e'); debugPrint('Error unblocking number: $e');
return false; return false;
} }
} }
/// Check if a number is blocked /// Check if a number is blocked (checks both system and app)
Future<bool> isNumberBlocked(String phoneNumber) async { Future<bool> isNumberBlocked(String phoneNumber) async {
try { try {
// Check system first
try {
bool systemBlocked = await _channel.invokeMethod('isNumberBlocked', {'number': phoneNumber});
if (systemBlocked) return true;
} catch (e) {
debugPrint('Failed to check system blocked status: $e');
}
// Check app storage as fallback
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? []; final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
return blockedNumbers.contains(phoneNumber); return blockedNumbers.contains(phoneNumber);
@ -65,14 +92,61 @@ class BlockService {
} }
} }
/// Get all blocked numbers /// Get all blocked numbers (merges system and app blocked numbers)
Future<List<String>> getBlockedNumbers() async { Future<List<String>> getBlockedNumbers() async {
try { try {
final prefs = await SharedPreferences.getInstance(); Set<String> allBlockedNumbers = {};
return prefs.getStringList(_blockedNumbersKey) ?? [];
// Get system blocked numbers
try {
List<dynamic> systemBlocked = await _channel.invokeMethod('getBlockedNumbers');
allBlockedNumbers.addAll(systemBlocked.cast<String>());
} catch (e) {
debugPrint('Failed to get system blocked numbers: $e');
}
// Get app blocked numbers
try {
final prefs = await SharedPreferences.getInstance();
final appBlocked = prefs.getStringList(_blockedNumbersKey) ?? [];
allBlockedNumbers.addAll(appBlocked);
} catch (e) {
debugPrint('Failed to get app blocked numbers: $e');
}
return allBlockedNumbers.toList()..sort();
} catch (e) { } catch (e) {
debugPrint('Error getting blocked numbers: $e'); debugPrint('Error getting blocked numbers: $e');
return []; return [];
} }
} }
/// Sync app blocked numbers with system blocked numbers
Future<void> syncWithSystem() async {
try {
// Get system blocked numbers
List<String> systemBlocked = [];
try {
List<dynamic> result = await _channel.invokeMethod('getBlockedNumbers');
systemBlocked = result.cast<String>();
} catch (e) {
debugPrint('Failed to get system blocked numbers for sync: $e');
return;
}
// Get app blocked numbers
final prefs = await SharedPreferences.getInstance();
List<String> appBlocked = prefs.getStringList(_blockedNumbersKey) ?? [];
// Merge them (system takes precedence)
Set<String> mergedBlocked = {...systemBlocked, ...appBlocked};
// Update app storage with merged list
await prefs.setStringList(_blockedNumbersKey, mergedBlocked.toList());
debugPrint('Synced ${mergedBlocked.length} blocked numbers with system');
} catch (e) {
debugPrint('Error syncing with system: $e');
}
}
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../../../domain/services/block_service.dart';
class BlockedNumbersPage extends StatefulWidget { class BlockedNumbersPage extends StatefulWidget {
const BlockedNumbersPage({super.key}); const BlockedNumbersPage({super.key});
@ -12,6 +13,8 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers
List<String> _blockedNumbers = []; // List of blocked numbers List<String> _blockedNumbers = []; // List of blocked numbers
final TextEditingController _numberController = TextEditingController(); final TextEditingController _numberController = TextEditingController();
final BlockService _blockService = BlockService();
bool _isLoading = true;
@override @override
void initState() { void initState() {
@ -19,13 +22,33 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
_loadPreferences(); // Load data on initialization _loadPreferences(); // Load data on initialization
} }
// Load preferences from local storage // Load preferences from local storage and sync with system
Future<void> _loadPreferences() async { Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false; _isLoading = true;
_blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
}); });
try {
final prefs = await SharedPreferences.getInstance();
final blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false;
// Sync with system blocked numbers
await _blockService.syncWithSystem();
// Get all blocked numbers (merged from system and app)
final blockedNumbers = await _blockService.getBlockedNumbers();
setState(() {
_blockUnknownNumbers = blockUnknownNumbers;
_blockedNumbers = blockedNumbers;
_isLoading = false;
});
} catch (e) {
debugPrint('Error loading preferences: $e');
setState(() {
_isLoading = false;
});
}
} }
// Save preferences to local storage // Save preferences to local storage
@ -41,84 +64,121 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: AppBar( appBar: AppBar(
title: const Text('Blocked Numbers'), title: const Text('Blocked Numbers'),
), actions: [
body: ListView( IconButton(
padding: const EdgeInsets.all(16), icon: const Icon(Icons.sync),
children: [ onPressed: _isLoading ? null : () async {
SwitchListTile( await _loadPreferences();
title: const Text( ScaffoldMessenger.of(context).showSnackBar(
'Block Unknown Numbers', const SnackBar(content: Text('Synced with system blocklist')),
style: TextStyle(color: Colors.white), );
),
value: _blockUnknownNumbers,
onChanged: (bool value) {
setState(() {
_blockUnknownNumbers = value;
_savePreferences(); // Save the state to local storage
});
}, },
), tooltip: 'Sync with system blocklist',
const SizedBox(height: 16),
ListTile(
title: const Text(
'Blocked Numbers',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
subtitle: _blockedNumbers.isEmpty
? const Text(
'No blocked numbers',
style: TextStyle(color: Colors.grey),
)
: null,
),
..._blockedNumbers.map(
(number) => ListTile(
title: Text(
number,
style: const TextStyle(color: Colors.white),
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _unblockNumber(number),
),
),
),
const Divider(color: Colors.grey),
ListTile(
title: const Text(
'Block a Number',
style: TextStyle(color: Colors.white),
),
trailing: const Icon(Icons.add, color: Colors.white),
onTap: () => _showBlockNumberDialog(),
), ),
], ],
), ),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text(
'Block Unknown Numbers',
style: TextStyle(color: Colors.white),
),
value: _blockUnknownNumbers,
onChanged: (bool value) {
setState(() {
_blockUnknownNumbers = value;
_savePreferences(); // Save the state to local storage
});
},
),
const SizedBox(height: 16),
ListTile(
title: const Text(
'Blocked Numbers',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
subtitle: _blockedNumbers.isEmpty
? const Text(
'No blocked numbers',
style: TextStyle(color: Colors.grey),
)
: Text(
'${_blockedNumbers.length} numbers blocked (includes system blocklist)',
style: const TextStyle(color: Colors.grey),
),
),
..._blockedNumbers.map(
(number) => ListTile(
title: Text(
number,
style: const TextStyle(color: Colors.white),
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _unblockNumber(number),
),
),
),
const Divider(color: Colors.grey),
ListTile(
title: const Text(
'Block a Number',
style: TextStyle(color: Colors.white),
),
trailing: const Icon(Icons.add, color: Colors.white),
onTap: () => _showBlockNumberDialog(),
),
],
),
); );
} }
// Function to block a number // Function to block a number
void _blockNumber(String number) { void _blockNumber(String number) async {
if (number.isNotEmpty && !_blockedNumbers.contains(number)) { if (number.isNotEmpty && !_blockedNumbers.contains(number)) {
setState(() { try {
_blockedNumbers.add(number); final success = await _blockService.blockNumber(number);
_savePreferences(); // Save the updated list if (success) {
}); await _loadPreferences(); // Refresh the list
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been blocked')), SnackBar(content: Text('$number has been blocked')),
); );
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to block $number')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error blocking $number: $e')),
);
}
} }
} }
// Function to unblock a number // Function to unblock a number
void _unblockNumber(String number) { void _unblockNumber(String number) async {
setState(() { try {
_blockedNumbers.remove(number); final success = await _blockService.unblockNumber(number);
_savePreferences(); // Save the updated list if (success) {
}); await _loadPreferences(); // Refresh the list
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been unblocked')), SnackBar(content: Text('$number has been unblocked')),
); );
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to unblock $number')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error unblocking $number: $e')),
);
}
} }
// Dialog for blocking a new number // Dialog for blocking a new number