From 2e8f45788953239cd361c4967b4ff84663f1c9c4 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:15:29 +0200 Subject: [PATCH] feat: add block management features and sync with system blocklist --- .../icing/dialer/activities/MainActivity.kt | 149 +++++++++++++ dialer/lib/domain/services/block_service.dart | 102 +++++++-- .../settings/blocked/settings_blocked.dart | 198 ++++++++++++------ 3 files changed, 366 insertions(+), 83 deletions(-) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index f58cdab..796500f 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -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("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("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("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 { + val blockedNumbers = mutableListOf() + + 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 + } + } } \ No newline at end of file diff --git a/dialer/lib/domain/services/block_service.dart b/dialer/lib/domain/services/block_service.dart index 5a4a0b0..6ac306a 100644 --- a/dialer/lib/domain/services/block_service.dart +++ b/dialer/lib/domain/services/block_service.dart @@ -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 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 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 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> getBlockedNumbers() async { try { - final prefs = await SharedPreferences.getInstance(); - return prefs.getStringList(_blockedNumbersKey) ?? []; + Set allBlockedNumbers = {}; + + // Get system blocked numbers + try { + List systemBlocked = await _channel.invokeMethod('getBlockedNumbers'); + allBlockedNumbers.addAll(systemBlocked.cast()); + } 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 syncWithSystem() async { + try { + // Get system blocked numbers + List systemBlocked = []; + try { + List result = await _channel.invokeMethod('getBlockedNumbers'); + systemBlocked = result.cast(); + } catch (e) { + debugPrint('Failed to get system blocked numbers for sync: $e'); + return; + } + + // Get app blocked numbers + final prefs = await SharedPreferences.getInstance(); + List appBlocked = prefs.getStringList(_blockedNumbersKey) ?? []; + + // Merge them (system takes precedence) + Set 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'); + } + } } diff --git a/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart index 77c3a85..fe85861 100644 --- a/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart +++ b/dialer/lib/presentation/features/settings/blocked/settings_blocked.dart @@ -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 { bool _blockUnknownNumbers = false; // Toggle for blocking unknown numbers List _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 { _loadPreferences(); // Load data on initialization } - // Load preferences from local storage + // Load preferences from local storage and sync with system Future _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 { 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