Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocking Calls on Android #464

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
43 changes: 36 additions & 7 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.apps.blt">
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_SCREENING" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.BIND_SCREENING_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-feature
android:name="android.hardware.telephony"
android:required="true" />
<uses-feature
android:name="android.hardware.microphone"
android:required="true" />
<application
android:label="blt"
android:name="${applicationName}"
Expand Down Expand Up @@ -39,21 +54,19 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--TODO: Add this filter, if you want support opening urls into your app-->

<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/invite"/>
</intent-filter>

<!--TODO: Add this filter, if you want to support sharing text into your app-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>

<!--TODO: Add this filter, if you want to support sharing images into your app-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
Expand All @@ -66,7 +79,6 @@
<data android:mimeType="image/*" />
</intent-filter>

<!--TODO: Add this filter, if you want to support sharing videos into your app-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
Expand All @@ -78,7 +90,6 @@
<data android:mimeType="video/*" />
</intent-filter>

<!--TODO: Add this filter, if you want to support sharing any type of files-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
Expand All @@ -89,12 +100,30 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="tel" />
</intent-filter>

</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name=".SpamCallBlockerService"
android:permission="android.permission.BIND_SCREENING_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.CallScreeningService" />
</intent-filter>
</service>

</application>


</manifest>
121 changes: 92 additions & 29 deletions android/app/src/main/kotlin/com/apps/blt/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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<out String>,
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<List<String>>("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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
68 changes: 68 additions & 0 deletions android/app/src/main/kotlin/com/apps/blt/SpamCallBlockerService.kt
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 16 additions & 0 deletions android/app/src/main/kotlin/com/apps/blt/SpamNumberManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.apps.blt

import kotlin.collections.mutableSetOf

object SpamNumberManager {
private val spamNumbers = mutableSetOf<String>()

fun updateSpamList(numbers: List<String>) {
spamNumbers.clear()
spamNumbers.addAll(numbers)
}

fun isSpamNumber(number: String): Boolean {
return spamNumbers.contains(number)
}
}
3 changes: 2 additions & 1 deletion ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Flutter
import UIKit
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
Expand All @@ -9,7 +10,7 @@ import UIKit
) -> Bool {
// Register the method channel
let controller = window?.rootViewController as! FlutterViewController
let clipboardImageChannel = FlutterMethodChannel(name: "clipboard_image_channel",
let clipboardImageChannel = FlutterMethodChannel(name: "com.apps.blt/channel",
binaryMessenger: controller.binaryMessenger)

clipboardImageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
Expand Down
Loading
Loading