diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df5660 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..886eae8 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e0afbb --- /dev/null +++ b/README.md @@ -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. diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100755 index 0000000..b9a6491 --- /dev/null +++ b/app/build.gradle.kts @@ -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") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 0000000..dd2c6f2 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..c6fae7e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/callblocker/CallBlockerService.kt b/app/src/main/java/com/callblocker/CallBlockerService.kt new file mode 100755 index 0000000..b5a79b3 --- /dev/null +++ b/app/src/main/java/com/callblocker/CallBlockerService.kt @@ -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) + } +} diff --git a/app/src/main/java/com/callblocker/MainActivity.kt b/app/src/main/java/com/callblocker/MainActivity.kt new file mode 100755 index 0000000..c1abbda --- /dev/null +++ b/app/src/main/java/com/callblocker/MainActivity.kt @@ -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) + } +} diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100755 index 0000000..db922f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml new file mode 100755 index 0000000..7fc7e85 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..5bb635e --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100755 index 0000000..c6b6072 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,12 @@ + + + #4CAF50 + #388E3C + #8BC34A + #F5F5F5 + #FFFFFF + #212121 + #757575 + #FFFFFF + #F44336 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100755 index 0000000..5e42264 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Bloqueador de Chamadas + Bloqueia automaticamente chamadas de números que não estão na sua lista de contatos + Bloqueio Ativo + Protegendo suas chamadas + Desativado + Toque no switch para ativar e conceder as permissões necessárias + Permissões necessárias não concedidas + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100755 index 0000000..1d5dce2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100755 index 0000000..3a6ec8d --- /dev/null +++ b/build.gradle.kts @@ -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 +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..f0a2e55 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000..15de902 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1518cb8 --- /dev/null +++ b/gradlew @@ -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 "$@" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100755 index 0000000..f77eb5e --- /dev/null +++ b/settings.gradle.kts @@ -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")