feat: add block management features and sync with system blocklist
This commit is contained in:
parent
ed07022916
commit
2e8f457889
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user