first commit

This commit is contained in:
Andre Silva 2025-12-05 18:47:28 -03:00
parent 5ece01c17c
commit 9a08f1dec9
20 changed files with 725 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
# Android
.android/
local.properties
*.apk
*.aab
# IDE
.idea/
*.iml
.vscode/
# Build outputs
apk-output/
output/
# OS files
.DS_Store
Thumbs.db

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
FROM eclipse-temurin:17-jdk-jammy
# Install required dependencies
RUN apt-get update && apt-get install -y \
wget \
unzip \
git \
&& rm -rf /var/lib/apt/lists/*
# Set environment variables
ENV ANDROID_HOME=/opt/android-sdk
ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools
# Download and install Android command line tools
RUN mkdir -p $ANDROID_HOME/cmdline-tools && \
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip && \
unzip -q /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools && \
mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest && \
rm /tmp/cmdline-tools.zip
# Accept licenses and install required SDK components
RUN yes | sdkmanager --licenses && \
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0"
# Set working directory
WORKDIR /app
# Copy project files
COPY . .
# Make gradlew executable
RUN chmod +x gradlew
# Build the APK
CMD ["./gradlew", "assembleDebug", "--no-daemon"]

93
README.md Normal file
View File

@ -0,0 +1,93 @@
# Call Blocker
Aplicativo Android para bloqueio automático de chamadas de números desconhecidos.
## Sobre
O **Call Blocker** bloqueia automaticamente chamadas recebidas de números que não estão salvos na sua lista de contatos. Utiliza a API CallScreeningService do Android (disponível a partir do Android 10) para interceptar e filtrar chamadas de forma transparente.
## Funcionalidades
- Bloqueio automático de chamadas de números desconhecidos
- Permite chamadas de números salvos nos contatos
- Interface simples com botão liga/desliga
- Integração nativa com o sistema de chamadas do Android
- Bloqueia chamadas com número oculto/indisponível
## Requisitos
- Android 7.0 (API 24) ou superior
- Permissões necessárias: telefone, contatos e triagem de chamadas
## Estrutura do Projeto
```
call_blocker/
├── app/
│ └── src/main/
│ ├── java/com/callblocker/
│ │ ├── MainActivity.kt # Atividade principal com UI
│ │ └── CallBlockerService.kt # Serviço de triagem de chamadas
│ ├── res/ # Recursos (layouts, strings, cores)
│ └── AndroidManifest.xml
├── gradle/wrapper/ # Gradle wrapper
├── build.gradle.kts # Configuração do Gradle
├── settings.gradle.kts # Configuração de módulos
└── Dockerfile # Build via Docker
```
## Build via Docker
### Pré-requisitos
- Docker instalado na máquina
### Construir a imagem
```bash
docker build -t call-blocker-builder .
```
### Gerar o APK
```bash
docker run --rm -v $(pwd)/output:/app/app/build/outputs call-blocker-builder
```
O APK será gerado em `./output/apk/debug/app-debug.apk`.
### Comando único (build + extração)
```bash
docker build -t call-blocker-builder . && \
docker run --rm -v $(pwd)/output:/app/app/build/outputs call-blocker-builder && \
echo "APK gerado em: ./output/apk/debug/app-debug.apk"
```
## Build Local (sem Docker)
### Pré-requisitos
- JDK 17
- Android SDK com platform-tools e build-tools 34.0.0
### Gerar o APK
```bash
chmod +x gradlew
./gradlew assembleDebug
```
O APK será gerado em `app/build/outputs/apk/debug/app-debug.apk`.
## Instalação
```bash
adb install app-debug.apk
```
Ou transfira o APK para o dispositivo e instale manualmente.
## Licença
Este projeto é de código aberto.

47
app/build.gradle.kts Executable file
View File

@ -0,0 +1,47 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.callblocker"
compileSdk = 34
defaultConfig {
applicationId = "com.callblocker"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}

6
app/proguard-rules.pro vendored Executable file
View File

@ -0,0 +1,6 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in android SDK/tools/proguard/proguard-android.txt
# Keep the CallBlockerService
-keep class com.callblocker.CallBlockerService { *; }

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissões necessárias -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.CallBlocker"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.CallBlocker">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Serviço de triagem de chamadas (Android 10+) -->
<service
android:name=".CallBlockerService"
android:permission="android.permission.BIND_SCREENING_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.CallScreeningService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,94 @@
package com.callblocker
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.ContactsContract
import android.telecom.Call
import android.telecom.CallScreeningService
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.N)
class CallBlockerService : CallScreeningService() {
override fun onScreenCall(callDetails: Call.Details) {
val prefs = getSharedPreferences(MainActivity.PREFS_NAME, Context.MODE_PRIVATE)
val isBlockerEnabled = prefs.getBoolean(MainActivity.KEY_BLOCKER_ENABLED, false)
if (!isBlockerEnabled) {
// Bloqueio desativado - permitir todas as chamadas
respondToCall(callDetails, CallResponse.Builder().build())
return
}
val phoneNumber = getPhoneNumber(callDetails)
if (phoneNumber.isNullOrEmpty()) {
// Número desconhecido/oculto - bloquear
blockCall(callDetails)
return
}
if (isNumberInContacts(phoneNumber)) {
// Número está nos contatos - permitir
allowCall(callDetails)
} else {
// Número não está nos contatos - bloquear
blockCall(callDetails)
}
}
private fun getPhoneNumber(callDetails: Call.Details): String? {
val handle = callDetails.handle ?: return null
return handle.schemeSpecificPart
}
private fun isNumberInContacts(phoneNumber: String): Boolean {
val uri = Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(phoneNumber)
)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(
uri,
arrayOf(ContactsContract.PhoneLookup._ID),
null,
null,
null
)
return cursor != null && cursor.count > 0
} catch (e: Exception) {
e.printStackTrace()
return false
} finally {
cursor?.close()
}
}
private fun allowCall(callDetails: Call.Details) {
val response = CallResponse.Builder()
.setDisallowCall(false)
.setRejectCall(false)
.setSilenceCall(false)
.setSkipCallLog(false)
.setSkipNotification(false)
.build()
respondToCall(callDetails, response)
}
private fun blockCall(callDetails: Call.Details) {
val response = CallResponse.Builder()
.setDisallowCall(true)
.setRejectCall(true)
.setSilenceCall(true)
.setSkipCallLog(false)
.setSkipNotification(true)
.build()
respondToCall(callDetails, response)
}
}

View File

@ -0,0 +1,153 @@
package com.callblocker
import android.Manifest
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.callblocker.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: SharedPreferences
companion object {
const val PREFS_NAME = "CallBlockerPrefs"
const val KEY_BLOCKER_ENABLED = "blocker_enabled"
}
private val requiredPermissions = arrayOf(
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_CALL_LOG,
Manifest.permission.READ_CONTACTS,
Manifest.permission.ANSWER_PHONE_CALLS
)
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
requestCallScreeningRole()
} else {
binding.switchBlocker.isChecked = false
updateStatus(false)
Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_LONG).show()
}
}
private val roleRequestLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
enableBlocker()
} else {
binding.switchBlocker.isChecked = false
updateStatus(false)
Toast.makeText(this, "Permissão de triagem de chamadas negada", Toast.LENGTH_LONG).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
setupSwitch()
loadSavedState()
}
private fun setupSwitch() {
binding.switchBlocker.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
checkAndRequestPermissions()
} else {
disableBlocker()
}
}
}
private fun loadSavedState() {
val isEnabled = prefs.getBoolean(KEY_BLOCKER_ENABLED, false)
if (isEnabled && hasAllPermissions()) {
binding.switchBlocker.isChecked = true
updateStatus(true)
} else {
binding.switchBlocker.isChecked = false
updateStatus(false)
}
}
private fun hasAllPermissions(): Boolean {
return requiredPermissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
}
private fun checkAndRequestPermissions() {
val permissionsToRequest = requiredPermissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
if (permissionsToRequest.isNotEmpty()) {
permissionLauncher.launch(permissionsToRequest)
} else {
requestCallScreeningRole()
}
}
private fun requestCallScreeningRole() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (roleManager.isRoleAvailable(RoleManager.ROLE_CALL_SCREENING)) {
if (!roleManager.isRoleHeld(RoleManager.ROLE_CALL_SCREENING)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_CALL_SCREENING)
roleRequestLauncher.launch(intent)
} else {
enableBlocker()
}
} else {
enableBlocker()
}
} else {
enableBlocker()
}
}
private fun enableBlocker() {
prefs.edit().putBoolean(KEY_BLOCKER_ENABLED, true).apply()
updateStatus(true)
Toast.makeText(this, "Bloqueio de chamadas ativado!", Toast.LENGTH_SHORT).show()
}
private fun disableBlocker() {
prefs.edit().putBoolean(KEY_BLOCKER_ENABLED, false).apply()
updateStatus(false)
Toast.makeText(this, "Bloqueio de chamadas desativado", Toast.LENGTH_SHORT).show()
}
private fun updateStatus(enabled: Boolean) {
binding.switchStatus.text = if (enabled) {
getString(R.string.status_enabled)
} else {
getString(R.string.status_disabled)
}
val color = if (enabled) {
ContextCompat.getColor(this, R.color.primary)
} else {
ContextCompat.getColor(this, R.color.text_secondary)
}
binding.iconShield.setColorFilter(color)
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#4CAF50"
android:pathData="M54,20L27,34v18c0,16.65 11.52,32.22 27,36 15.48,-3.78 27,-19.35 27,-36V34l-27,-14zM48,70l-12,-12 4.23,-4.23L48,61.51l19.77,-19.77L72,46l-24,24z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#4CAF50"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zM10,17l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z"/>
</vector>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
tools:context=".MainActivity">
<!-- Ícone do escudo -->
<ImageView
android:id="@+id/iconShield"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="80dp"
android:src="@drawable/ic_shield"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/primary" />
<!-- Título -->
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/app_name"
android:textColor="@color/text_primary"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iconShield" />
<!-- Descrição -->
<TextView
android:id="@+id/descriptionText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="@string/description"
android:textColor="@color/text_secondary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText" />
<!-- Card com Switch -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardSwitch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="48dp"
app:cardBackgroundColor="@color/card_background"
app:cardCornerRadius="16dp"
app:cardElevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/descriptionText">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/switchTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blocker_title"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/switchStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/status_disabled"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchBlocker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Status das permissões -->
<TextView
android:id="@+id/permissionStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/permission_info"
android:textColor="@color/text_secondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cardSwitch" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#4CAF50</color>
<color name="primary_dark">#388E3C</color>
<color name="accent">#8BC34A</color>
<color name="background">#F5F5F5</color>
<color name="card_background">#FFFFFF</color>
<color name="text_primary">#212121</color>
<color name="text_secondary">#757575</color>
<color name="white">#FFFFFF</color>
<color name="error">#F44336</color>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Bloqueador de Chamadas</string>
<string name="description">Bloqueia automaticamente chamadas de números que não estão na sua lista de contatos</string>
<string name="blocker_title">Bloqueio Ativo</string>
<string name="status_enabled">Protegendo suas chamadas</string>
<string name="status_disabled">Desativado</string>
<string name="permission_info">Toque no switch para ativar e conceder as permissões necessárias</string>
<string name="permission_denied">Permissões necessárias não concedidas</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.CallBlocker" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:statusBarColor">@color/primary_dark</item>
</style>
</resources>

4
build.gradle.kts Executable file
View File

@ -0,0 +1,4 @@
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}

4
gradle.properties Executable file
View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

5
gradle/wrapper/gradle-wrapper.properties vendored Executable file
View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

27
gradlew vendored Executable file
View File

@ -0,0 +1,27 @@
#!/bin/sh
#
# Gradle start up script for POSIX generated by Gradle.
#
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="java"
fi
# Add default JVM options here
DEFAULT_JVM_OPTS='-Xmx64m -Xms64m'
APP_HOME=$( cd "$( dirname "$0" )" && pwd -P ) || exit
# Setup classpath
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Execute Gradle
exec "$JAVACMD" $DEFAULT_JVM_OPTS -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

18
settings.gradle.kts Executable file
View File

@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "CallBlocker"
include(":app")