From e991326666d361c8e0b76c4fd30ddeb9a93ac125 Mon Sep 17 00:00:00 2001
From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com>
Date: Thu, 31 Oct 2024 02:18:25 +0530
Subject: [PATCH 1/5] addded paste method channel in native ios folder
---
ios/Runner/AppDelegate.swift | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index b636303481..be8434e74b 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,5 +1,5 @@
-import UIKit
import Flutter
+import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -7,7 +7,36 @@ import Flutter
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
+ // Register the method channel
+ let controller = window?.rootViewController as! FlutterViewController
+ let clipboardImageChannel = FlutterMethodChannel(name: "clipboard_image_channel",
+ binaryMessenger: controller.binaryMessenger)
+
+ clipboardImageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
+ if call.method == "getClipboardImage" {
+ self.getClipboardImage(result: result)
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
-}
+
+ private func getClipboardImage(result: FlutterResult) {
+ // Check if the clipboard contains an image
+ if let image = UIPasteboard.general.image {
+ // Convert the image to PNG data
+ if let imageData = image.pngData() {
+ // Encode the image data to a Base64 string
+ let base64String = imageData.base64EncodedString()
+ result(base64String) // Send the Base64 string back to Flutter
+ } else {
+ result(FlutterError(code: "NO_IMAGE", message: "Could not convert image to data", details: nil))
+ }
+ } else {
+ result(FlutterError(code: "NO_IMAGE", message: "Clipboard does not contain an image", details: nil))
+ }
+ }
+}
\ No newline at end of file
From 3e4a8bb1e3261fff1285d5fc5c795257b574ceac Mon Sep 17 00:00:00 2001
From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com>
Date: Tue, 19 Nov 2024 20:30:46 +0530
Subject: [PATCH 2/5] android call block working
---
android/app/build.gradle | 6 +-
android/app/src/main/AndroidManifest.xml | 43 +++++-
.../main/kotlin/com/apps/blt/MainActivity.kt | 121 +++++++++++----
.../com/apps/blt/NotificationManagerImpl.kt | 23 +++
.../com/apps/blt/SpamCallBlockerService.kt | 68 ++++++++
.../kotlin/com/apps/blt/SpamNumberManager.kt | 14 ++
lib/src/pages/home/home.dart | 37 +++++
lib/src/pages/home/report_bug.dart | 2 +-
.../pages/spam_call_blocker/blocker_home.dart | 145 ++++++++++++++++++
.../spam_call_blocker/database_helper.dart | 60 ++++++++
lib/src/routes/routes_import.dart | 1 +
lib/src/routes/routing.dart | 20 +++
macos/Flutter/GeneratedPluginRegistrant.swift | 2 +-
pubspec.yaml | 1 +
14 files changed, 503 insertions(+), 40 deletions(-)
create mode 100644 android/app/src/main/kotlin/com/apps/blt/NotificationManagerImpl.kt
create mode 100644 android/app/src/main/kotlin/com/apps/blt/SpamCallBlockerService.kt
create mode 100644 android/app/src/main/kotlin/com/apps/blt/SpamNumberManager.kt
create mode 100644 lib/src/pages/spam_call_blocker/blocker_home.dart
create mode 100644 lib/src/pages/spam_call_blocker/database_helper.dart
diff --git a/android/app/build.gradle b/android/app/build.gradle
index a299894e4c..523fd548d0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -36,7 +36,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.apps.blt"
- minSdkVersion 21
+ minSdkVersion 29
targetSdkVersion 34
multiDexEnabled true
versionCode flutterVersionCode.toInteger()
@@ -71,5 +71,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.activity:activity-ktx:1.7.2'
-
+ implementation 'org.greenrobot:eventbus:3.2.0'
+ implementation 'com.jakewharton.timber:timber:4.7.1'
+ implementation 'pub.devrel:easypermissions:3.0.0'
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c3c463a8b4..c7f1ea9248 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
@@ -66,7 +79,6 @@
-
@@ -78,7 +90,6 @@
-
@@ -89,12 +100,30 @@
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/apps/blt/MainActivity.kt b/android/app/src/main/kotlin/com/apps/blt/MainActivity.kt
index ebb9a12f8a..2a9b8b6691 100644
--- a/android/app/src/main/kotlin/com/apps/blt/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/apps/blt/MainActivity.kt
@@ -1,48 +1,111 @@
package com.apps.blt
+import android.util.Log
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
import android.content.ClipData
import android.content.ClipboardManager
import android.graphics.Bitmap
-import android.graphics.drawable.BitmapDrawable
-import android.os.Build
import android.provider.MediaStore
import android.util.Base64
import androidx.annotation.NonNull
-import androidx.annotation.RequiresApi
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
class MainActivity : FlutterActivity() {
- private val CHANNEL = "clipboard_image_channel"
+ private val CHANNEL = "com.apps.blt/channel"
+ private val REQUEST_CODE_PERMISSIONS = 1001
+ private val REQUIRED_PERMISSIONS = arrayOf(
+ Manifest.permission.READ_CALL_LOG,
+ Manifest.permission.ANSWER_PHONE_CALLS,
+ Manifest.permission.READ_PHONE_STATE,
+ "android.permission.CALL_SCREENING"
+ )
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ startSpamCallBlockerService()
+
+ }
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startSpamCallBlockerService()
+ } else {
+ Log.d("PermissionCheck", "Permissions not granted: ${permissions.zip(grantResults.toTypedArray()).joinToString { "${it.first}: ${it.second}" }}")
+ // Handle the case where permissions are not granted
+ }
+ }
+ }
+ private fun allPermissionsGranted(): Boolean {
+ val result = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
+ }
+ Log.d("PermissionCheck", "All permissions granted: $result")
+ return result
+ }
+
+ private fun startSpamCallBlockerService() {
+ val intent = Intent(this, SpamCallBlockerService::class.java)
+ startService(intent)
+
+ }
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
- MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
- if (call.method == "getClipboardImage") {
- val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
- val clipData = clipboard.primaryClip
-
- if (clipData != null && clipData.itemCount > 0) {
- val item = clipData.getItemAt(0)
-
- if (item.uri != null) {
- val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, item.uri)
- val stream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
- val byteArray = stream.toByteArray()
- val base64String = Base64.encodeToString(byteArray, Base64.DEFAULT)
- result.success(base64String)
- } else {
- result.error("NO_IMAGE", "Clipboard does not contain an image", null)
- }
- } else {
- result.error("EMPTY_CLIPBOARD", "Clipboard is empty", null)
- }
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
+ when (call.method) {
+ "getClipboardImage" -> handleClipboardImage(result)
+ "updateSpamList" -> handleSpamList(call, result)
+ else -> result.notImplemented()
+ }
+ }
+ }
+ private fun handleSpamList(call: MethodCall, result: MethodChannel.Result) {
+ val numbers = call.argument>("numbers")
+
+ if (numbers != null) {
+ SpamNumberManager.updateSpamList(numbers)
+ result.success("Spam list updated successfully!")
+ } else {
+ result.error("INVALID_ARGUMENT", "Numbers list is null", null)
+ }
+ }
+
+ private fun handleClipboardImage(result: MethodChannel.Result) {
+ val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ val clipData = clipboard.primaryClip
+
+ if (clipData != null && clipData.itemCount > 0) {
+ val item = clipData.getItemAt(0)
+
+ if (item.uri != null) {
+ val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, item.uri)
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ val byteArray = stream.toByteArray()
+ val base64String = Base64.encodeToString(byteArray, Base64.DEFAULT)
+ result.success(base64String)
} else {
- result.notImplemented()
+ result.error("NO_IMAGE", "Clipboard does not contain an image", null)
}
+ } else {
+ result.error("EMPTY_CLIPBOARD", "Clipboard is empty", null)
}
}
}
diff --git a/android/app/src/main/kotlin/com/apps/blt/NotificationManagerImpl.kt b/android/app/src/main/kotlin/com/apps/blt/NotificationManagerImpl.kt
new file mode 100644
index 0000000000..0eeaa6d301
--- /dev/null
+++ b/android/app/src/main/kotlin/com/apps/blt/NotificationManagerImpl.kt
@@ -0,0 +1,23 @@
+package com.apps.blt
+
+import android.content.Context
+import android.os.Looper
+import android.widget.Toast
+interface NotificationManager {
+ fun showToastNotification(context: Context, message: String)
+}
+
+class NotificationManagerImpl : NotificationManager {
+ override fun showToastNotification(context: Context, message: String) {
+ val t = Thread {
+ try {
+ Looper.prepare()
+ Toast.makeText(context.applicationContext, message, Toast.LENGTH_LONG).show()
+ Looper.loop()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ t.start()
+ }
+}
diff --git a/android/app/src/main/kotlin/com/apps/blt/SpamCallBlockerService.kt b/android/app/src/main/kotlin/com/apps/blt/SpamCallBlockerService.kt
new file mode 100644
index 0000000000..0ace07a94e
--- /dev/null
+++ b/android/app/src/main/kotlin/com/apps/blt/SpamCallBlockerService.kt
@@ -0,0 +1,68 @@
+package com.apps.blt
+
+import android.telecom.CallScreeningService
+import android.telecom.Call
+import android.util.Log
+import org.greenrobot.eventbus.EventBus
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import android.net.Uri
+
+class MessageEvent(val message: String) {}
+
+class SpamCallBlockerService : CallScreeningService() {
+ private val notificationManager = NotificationManagerImpl()
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d("SpamCallBlockerService", "Service started")
+ }
+
+ override fun onScreenCall(callDetails: Call.Details) {
+ Log.d("SpamCallBlockerService", "onScreenCall triggered")
+ val phoneNumber = getPhoneNumber(callDetails)
+ Log.d("SpamCallBlockerService", "Intercepted call from: $phoneNumber")
+ var response = CallResponse.Builder()
+ response = handlePhoneCall(response, phoneNumber)
+
+ respondToCall(callDetails, response.build())
+ logCallInterception(phoneNumber, response.build())
+ }
+
+ private fun handlePhoneCall(
+ response: CallResponse.Builder,
+ phoneNumber: String
+ ): CallResponse.Builder {
+ if (SpamNumberManager.isSpamNumber(phoneNumber)) {
+ response.apply {
+ setRejectCall(true)
+ setDisallowCall(true)
+ setSkipCallLog(false)
+ displayToast(String.format("Rejected call from %s", phoneNumber))
+ }
+ } else {
+ displayToast(String.format("Incoming call from %s", phoneNumber))
+ }
+ return response
+ }
+
+ private fun getPhoneNumber(callDetails: Call.Details): String {
+ return callDetails.handle.toString().removeTelPrefix().parseCountryCode()
+ }
+
+ private fun displayToast(message: String) {
+ notificationManager.showToastNotification(applicationContext, message)
+ EventBus.getDefault().post(MessageEvent(message))
+ }
+
+ private fun logCallInterception(phoneNumber: String, response: CallResponse) {
+ val currentTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
+ val action = "action"
+ val logMessage = "[$currentTime] $action call from $phoneNumber"
+ Log.d("SpamCallBlockerService", logMessage)
+ }
+
+ fun String.removeTelPrefix() = this.replace("tel:", "")
+ fun String.parseCountryCode(): String = Uri.decode(this)
+}
diff --git a/android/app/src/main/kotlin/com/apps/blt/SpamNumberManager.kt b/android/app/src/main/kotlin/com/apps/blt/SpamNumberManager.kt
new file mode 100644
index 0000000000..b125bda352
--- /dev/null
+++ b/android/app/src/main/kotlin/com/apps/blt/SpamNumberManager.kt
@@ -0,0 +1,14 @@
+package com.apps.blt
+
+object SpamNumberManager {
+ private val spamNumbers = mutableSetOf()
+
+ fun updateSpamList(numbers: List) {
+ spamNumbers.clear()
+ spamNumbers.addAll(numbers)
+ }
+
+ fun isSpamNumber(number: String): Boolean {
+ return spamNumbers.contains(number)
+ }
+}
diff --git a/lib/src/pages/home/home.dart b/lib/src/pages/home/home.dart
index a81ad7c1c0..dbd94ec053 100644
--- a/lib/src/pages/home/home.dart
+++ b/lib/src/pages/home/home.dart
@@ -424,6 +424,43 @@ class _HomeState extends ConsumerState {
}),
),
),
+ ListTile(
+ title: Container(
+ width: double.infinity,
+ height: 50,
+ child: Builder(builder: (context) {
+ return TextButton(
+ child: Text(
+ "Spam Call Blocker",
+ style: GoogleFonts.ubuntu(
+ textStyle: TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ ),
+ ),
+ ),
+ style: ButtonStyle(
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ backgroundColor: WidgetStateProperty.all(
+ isDarkMode.isDarkMode
+ ? Color.fromRGBO(126, 33, 58, 1)
+ : Color(0xFFDC4654),
+ ),
+ ),
+ onPressed: () async {
+ Navigator.pushNamed(
+ context,
+ RouteManager.spamCallBlockerPage,
+ );
+ },
+ );
+ }),
+ ),
+ ),
// Sizzle Button
ListTile(
title: Container(
diff --git a/lib/src/pages/home/report_bug.dart b/lib/src/pages/home/report_bug.dart
index f6d96b1724..ed1fd3e356 100644
--- a/lib/src/pages/home/report_bug.dart
+++ b/lib/src/pages/home/report_bug.dart
@@ -76,7 +76,7 @@ class _ReportFormState extends ConsumerState {
bool showLabel = false;
List _labels = [];
late List _labelsState;
- static const platform = MethodChannel('clipboard_image_channel');
+ static const platform = MethodChannel('com.apps.blt/channel');
bool _isSnackBarVisible = false;
void showSnackBar(BuildContext context, String message) {
diff --git a/lib/src/pages/spam_call_blocker/blocker_home.dart b/lib/src/pages/spam_call_blocker/blocker_home.dart
new file mode 100644
index 0000000000..2cf56c03a9
--- /dev/null
+++ b/lib/src/pages/spam_call_blocker/blocker_home.dart
@@ -0,0 +1,145 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'database_helper.dart'; // Your DatabaseHelper class for SQLite operations
+
+class SpamCallBlockerPage extends StatefulWidget {
+ @override
+ _SpamCallBlockerPageState createState() => _SpamCallBlockerPageState();
+}
+
+class _SpamCallBlockerPageState extends State {
+ final List _numbers = [];
+ final TextEditingController _controller = TextEditingController();
+ static const platform = MethodChannel('com.apps.blt/channel'); // MethodChannel for native communication
+
+ @override
+ void initState() {
+ super.initState();
+ _loadNumbers();
+ }
+
+ Future _loadNumbers() async {
+ try {
+ final numbers = await DatabaseHelper.getNumbers();
+ setState(() {
+ _numbers.clear();
+ _numbers.addAll(numbers);
+ });
+ _sendSpamListToNative();
+ } catch (e) {
+ _showError("Failed to load numbers: $e");
+ }
+ }
+
+ Future _addNumber() async {
+ final newNumber = _controller.text.trim();
+ if (newNumber.isEmpty || !_validatePhoneNumber(newNumber)) {
+ _showError("Please enter a valid phone number.");
+ return;
+ }
+
+ if (_numbers.contains(newNumber)) {
+ _showError("This number is already in the list.");
+ return;
+ }
+
+ setState(() {
+ _numbers.add(newNumber);
+ _controller.clear();
+ });
+
+ try {
+ await DatabaseHelper.insertNumber(newNumber);
+ _sendSpamListToNative();
+ } catch (e) {
+ _showError("Failed to add number: $e");
+ }
+ }
+
+ Future _removeNumber(String number) async {
+ setState(() {
+ _numbers.remove(number);
+ });
+
+ try {
+ await DatabaseHelper.deleteNumber(number);
+ _sendSpamListToNative();
+ } catch (e) {
+ _showError("Failed to remove number: $e");
+ }
+ }
+
+ Future _sendSpamListToNative() async {
+ try {
+ var response = await platform.invokeMethod('updateSpamList', {
+ 'numbers': _numbers,
+ });
+ print(response);
+ } catch (e) {
+ _showError("Failed to update spam list on the native side: $e");
+ }
+ }
+
+ bool _validatePhoneNumber(String number) {
+ final regex = RegExp(r'^\+?[0-9]{10,15}$'); // Allows country code (e.g., +91)
+ return regex.hasMatch(number);
+ }
+
+ void _showError(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('Spam Call Blocker'),
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: [
+ // Input Field for Adding Numbers
+ TextField(
+ controller: _controller,
+ decoration: InputDecoration(
+ labelText: 'Enter number',
+ border: OutlineInputBorder(),
+ suffixIcon: IconButton(
+ icon: Icon(Icons.add),
+ onPressed: _addNumber,
+ ),
+ ),
+ keyboardType: TextInputType.phone,
+ ),
+ SizedBox(height: 20),
+ Expanded(
+ child: _numbers.isEmpty
+ ? Center(child: Text("No numbers added."))
+ : ListView.builder(
+ itemCount: _numbers.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ title: Text(
+ _numbers[index],
+ style: TextStyle(fontSize: 16),
+ ),
+ trailing: IconButton(
+ icon: Icon(Icons.remove_circle, color: Colors.red),
+ onPressed: () => _removeNumber(_numbers[index]),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/pages/spam_call_blocker/database_helper.dart b/lib/src/pages/spam_call_blocker/database_helper.dart
new file mode 100644
index 0000000000..3e96e65af7
--- /dev/null
+++ b/lib/src/pages/spam_call_blocker/database_helper.dart
@@ -0,0 +1,60 @@
+import 'package:sqflite/sqflite.dart';
+import 'package:path/path.dart';
+
+class DatabaseHelper {
+ static Database? _database;
+
+ // Create a singleton pattern
+ static Future get database async {
+ if (_database != null) return _database!;
+
+ // If the database doesn't exist, create it
+ _database = await _initDB();
+ return _database!;
+ }
+
+ // Initialize the database
+ static Future _initDB() async {
+ String path = join(await getDatabasesPath(), 'spam_numbers.db');
+
+ return await openDatabase(
+ path,
+ onCreate: (db, version) {
+ return db.execute(
+ 'CREATE TABLE numbers(id INTEGER PRIMARY KEY AUTOINCREMENT, number TEXT)',
+ );
+ },
+ version: 1,
+ );
+ }
+
+ // Insert a number into the database
+ static Future insertNumber(String number) async {
+ final db = await database;
+ await db.insert(
+ 'numbers',
+ {'number': number},
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+ }
+
+ // Get all numbers from the database
+ static Future> getNumbers() async {
+ final db = await database;
+ final List