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.Bundle
import android.provider.CallLog
import android.provider.BlockedNumberContract
import android.telecom.TelecomManager
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionInfo
import android.util.Log
import androidx.core.content.ContextCompat
import com.icing.dialer.KeystoreHelper
@ -23,6 +26,7 @@ class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore"
private val CALLLOG_CHANNEL = "com.example.calllog"
private val CALL_CHANNEL = "call_service"
private val BLOCK_CHANNEL = "com.example.block"
private val TAG = "MainActivity"
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002
@ -240,6 +244,44 @@ class MainActivity : FlutterActivity() {
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 {
@ -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/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing blocked phone numbers
class BlockService {
static const String _blockedNumbersKey = 'blocked_numbers';
static const MethodChannel _channel = MethodChannel('com.example.block');
// Private constructor
BlockService._privateConstructor();
@ -16,46 +18,71 @@ class BlockService {
return _instance;
}
/// Block a phone number
/// Block a phone number (syncs with system and app)
Future<bool> blockNumber(String phoneNumber) async {
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 blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
// Don't add if already blocked
if (blockedNumbers.contains(phoneNumber)) {
return true;
if (!blockedNumbers.contains(phoneNumber)) {
blockedNumbers.add(phoneNumber);
await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
}
blockedNumbers.add(phoneNumber);
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
return true; // Return true if either system or app blocking succeeded
} catch (e) {
debugPrint('Error blocking number: $e');
return false;
}
}
/// Unblock a phone number
/// Unblock a phone number (syncs with system and app)
Future<bool> unblockNumber(String phoneNumber) async {
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 blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
if (!blockedNumbers.contains(phoneNumber)) {
return true;
if (blockedNumbers.contains(phoneNumber)) {
blockedNumbers.remove(phoneNumber);
await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
}
blockedNumbers.remove(phoneNumber);
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
return true;
} catch (e) {
debugPrint('Error unblocking number: $e');
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 {
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 blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
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 {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList(_blockedNumbersKey) ?? [];
Set<String> allBlockedNumbers = {};
// 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) {
debugPrint('Error getting blocked numbers: $e');
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:shared_preferences/shared_preferences.dart';
import '../../../../domain/services/block_service.dart';
class BlockedNumbersPage extends StatefulWidget {
const BlockedNumbersPage({super.key});
@ -12,6 +13,8 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers
List<String> _blockedNumbers = []; // List of blocked numbers
final TextEditingController _numberController = TextEditingController();
final BlockService _blockService = BlockService();
bool _isLoading = true;
@override
void initState() {
@ -19,13 +22,33 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
_loadPreferences(); // Load data on initialization
}
// Load preferences from local storage
// Load preferences from local storage and sync with system
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_blockUnknownNumbers = prefs.getBool('blockUnknownNumbers') ?? false;
_blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
_isLoading = true;
});
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
@ -41,84 +64,121 @@ class _BlockedNumbersPageState extends State<BlockedNumbersPage> {
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Blocked Numbers'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text(
'Block Unknown Numbers',
style: TextStyle(color: Colors.white),
),
value: _blockUnknownNumbers,
onChanged: (bool value) {
setState(() {
_blockUnknownNumbers = value;
_savePreferences(); // Save the state to local storage
});
actions: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: _isLoading ? null : () async {
await _loadPreferences();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Synced 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(),
tooltip: 'Sync with system blocklist',
),
],
),
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
void _blockNumber(String number) {
void _blockNumber(String number) async {
if (number.isNotEmpty && !_blockedNumbers.contains(number)) {
setState(() {
_blockedNumbers.add(number);
_savePreferences(); // Save the updated list
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been blocked')),
);
try {
final success = await _blockService.blockNumber(number);
if (success) {
await _loadPreferences(); // Refresh the list
ScaffoldMessenger.of(context).showSnackBar(
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
void _unblockNumber(String number) {
setState(() {
_blockedNumbers.remove(number);
_savePreferences(); // Save the updated list
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$number has been unblocked')),
);
void _unblockNumber(String number) async {
try {
final success = await _blockService.unblockNumber(number);
if (success) {
await _loadPreferences(); // Refresh the list
ScaffoldMessenger.of(context).showSnackBar(
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