←

CallScreeningService vs BroadcastReceiver: dos APIs para bloquear llamadas

Contexto

Bloquear llamadas en Android no es una API: son dos. Android 10 (API 29) introdujo CallScreeningService con respondToCall(), que es la forma moderna y soportada. Pero Android 9 (API 28) sigue siendo ~5% del parque y depende de BroadcastReceiver con TelecomManager.endCall(), ya marcado como deprecated en API 29 pero funcional en 28.

Si quieres soportar Android 9+, tienes que tener las dos implementaciones.

Lo que aprendi

El CallScreeningService se registra en el manifest y recibe Call.Details con un objeto CallResponse.Builder para responder:

@AndroidEntryPoint
class CallBlockerScreeningService : CallScreeningService() {
    override fun onScreenCall(callDetails: Call.Details) {
        val phoneNumber = callDetails.handle?.schemeSpecificPart ?: ""
        serviceScope.launch {
            if (shouldBlock(phoneNumber)) {
                val response = CallResponse.Builder()
                    .setDisallowCall(true)
                    .setRejectCall(true)
                    .setSkipCallLog(false)
                    .build()
                respondToCall(callDetails, response)
            } else {
                allowCall(callDetails)
            }
        }
    }
}

El BroadcastReceiver para API 28 escucha ACTION_PHONE_STATE_CHANGED y termina la llamada con telecomManager.endCall():

class PhoneStateReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Early return en API 29+ donde CallScreeningService toma el rol
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return
        if (intent.action != TelephonyManager.ACTION_PHONE_STATE_CHANGED) return

        val state = intent.getStringExtra(TelephonyManager.EXTRA_STATE)
        if (state != TelephonyManager.EXTRA_STATE_RINGING) return

        @Suppress("DEPRECATION")
        val phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)
        // ... endCall() en TelecomManager
    }
}

El gate if (Build.VERSION.SDK_INT >= Q) return es crucial: sin el, en API 29+ se ejecutan AMBOS receptores y compiten por bloquear, generando race conditions.

Por que importa

La trampa es pensar "tengo Android 10+ como minSdk porque es 95% del parque, ignoro 9". Pero ese 5% es ~50M dispositivos. Si tu app es de utilidad pura (bloquear, antivirus, accesibilidad), es exactamente el publico que mas la necesita: gente con telefonos viejos a quien no puedes pedir actualizar.

El costo de soportar ambas APIs es duplicar determineBlockReason en dos clases. Dolor real, pero menor que excluir usuarios. La logica comun se puede extraer a un use case del domain layer.