jueves, 24 de mayo de 2012

Lista negra de llamadas entrantes

Me estan friendo a llamadas de un número de teléfono publicitario. He mirado en Android Market y lo que he visto no me ha gustado así que he decidido hacerme la aplicación. Toma ya.
He sufrido un poco, porque aunque parezca increible, el API de telefonía no permite colgar llamadas entrantes. He tenido que investigar un montón, y en inglés, así que dejo aquí lo aprendido.
Para ello, voy a utilizar una base de datos sqlite para persistir la información referida a números de teléfono en lista negra. Para insertar y borrar los números de serie se crea una actividad que dará la funcionalidad, tanto de insertar, como de borrar y consultar números de serie en lista negra.
Para controlar las llamadas entrantes se crea una clase de tipo BroadcastReceiver que notificará las llamadas entrantes, consultará en base de datos si ese número de teléfono esta dado de alta en base de datos, y en caso de que haya una coincidencia, colgar la llamada.

Configuración del manifest


En el manifest de la aplicación se definen los permisos que debe tener esta, que en este caso será tener acceso al estado del teléfono android.permission.READ_PHONE_STATE, modificar el estado del teléfono android.permission.MODIFY_PHONE_STATE, y procesar llamadas entrantes android.permission.PROCESS_INCOMING_CALLS. En este caso además la aplicación consta de un receiver y de una actividad. La actividad principal, BlacklistPhoneNumberActivity se encarga de la parte gráfica de la aplicación, en la que se da de alta o de baja números de teléfono y consulta los números que están en lista negra en el preciso momento. El receiver BlackListPhoneNumberBroadcastReceiver se encarga de recibir intents de llamada entrante y colgar esta llamada en caso de que ese nùmero de teléfono este dado de alta en lista negra.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.m607"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" />

    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" >
        
        <uses-permission android:name="android.permission.READ_PHONE_STATE" />
        <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
        <uses-permission android:name="android.permission.CALL_PHONE" />
        <uses-permission android:name="android.permission.PROCESS_INCOMING_CALLS" />
        
        <receiver android:name=".BlackListPhoneNumberBroadcastReceiver" >
            <intent-filter android:priority="999" >
                <action android:name="android.intent.action.PHONE_STATE" />
            </intent-filter>
        </receiver>

        <activity
            android:name=".BlackListPhoneActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        


    </application>

</manifest>




Persistencia de la información


La persistencia de la información se realiza mediante una base de datos SQLite. Como en ejemplos anteriores, se usa una clase BlacklistPhoneNumberDBAdapter que maneja el modelo de datos de la aplicación, con una clase interna DatabaseHelper que extiende la clase SQLiteOpenHelper y que sobreescribe los método onCreate y onUpgrade del Helper para la creación y actualización de la base de datos y que dará acceso a la apertura de conexión y cierre de la base de datos, así como acceso a la instancia de SQLiteDatabase que maneja la persistencia de información en la base de datos.
La clase BlacklistPhoneNumberDBAdapter crea los métodos insertPhoneNumber(String) para persistir nuevos números en base de datos, deletePhoneNumber(String) para borrar números en base de datos, isPhoneNumberInBlacklist(String) para preguntar por un número en especial, getBlacklistPhoneNumbersAsString para la consulta de todos los números dados de alta en base de datos.

package com.m607.database;

import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;



public class BlacklistPhoneNumberDBAdapter {
 private static final String TAG = "BlacklistPhoneNumberDBAdapter";
  
    private static final String DATABASE_NAME = "blacklistdb";
    private static final String DATABASE_TABLE = "blacklistphonenumbers";
    
    private static final int DATABASE_VERSION = 1;
 
    private static final String TXPHONENUMBER="tx_phonenumber";
    
    
 private static final String DATABASE_CREATE = 
  "create table "+DATABASE_TABLE+
   "(_id integer primary key autoincrement, "+
   TXPHONENUMBER+" text not null);";
    
 private final String sqlSelectByNumber = 
  "select "  + TXPHONENUMBER+ " FROM " + DATABASE_TABLE +
   " where "  + TXPHONENUMBER +"=?";

  
 private final String sqlSelectNumbers = 
  "select "  + TXPHONENUMBER+ " FROM " + DATABASE_TABLE;
   

 
 private final String sqlDeleteNumber = 
  "delete from " + DATABASE_TABLE + " where "  + TXPHONENUMBER +"=?";
 
 private final String sqlInsertBlacklistedPhoneNumber = 
  "insert into " + DATABASE_TABLE + "("+
  TXPHONENUMBER +") values (?)";
 
    private final Context context; 
 
    private DatabaseHelper DBHelper;
    
    private SQLiteDatabase db;

    
    public BlacklistPhoneNumberDBAdapter(Context ctx) 
    {
        this.context = ctx;
        DBHelper = new DatabaseHelper(context);
    }
    
    public boolean isOpen(){
     boolean isOpen = false;
     if(db.isOpen()){
      isOpen=true;
     }
     return isOpen;
    }

    
    public Cursor getBlacklistPhoneNumber(String sPhoneNumber){
Log.d(TAG, sqlSelectByNumber);
  String[] argumentos = new String[1];
  argumentos[0] = sPhoneNumber;   
Log.d(TAG, "argumentos: " + argumentos[0]);
  Cursor mCursor = db.rawQuery(sqlSelectByNumber, argumentos);
  
  if(mCursor!=null){
Log.d(TAG, "mCursor: " + mCursor); 
   mCursor.moveToFirst();
  }
  return mCursor;
    }
    
    public Cursor getBlacklistPhoneNumbers(){
     Log.d(TAG, sqlSelectNumbers);
       
       Cursor mCursor = db.rawQuery(sqlSelectNumbers, null);
       
       if(mCursor!=null){
     Log.d(TAG, "mCursor: " + mCursor); 
        mCursor.moveToFirst();
       }
       return mCursor;
         }
    
    public String getBlacklistPhoneNumbersAsString(){
     Log.d(TAG, sqlSelectNumbers);
     String sConsulta="";
     Cursor blacklistCursor =getBlacklistPhoneNumbers();
  while (!blacklistCursor.isAfterLast()) {
   
   sConsulta += blacklistCursor.getString(0)+ "\n";
   blacklistCursor.moveToNext();
  }
  return sConsulta;
    }
    
    
    
    public void deletePhoneNumber(String sPhoneNumber){
     Log.d(TAG, sqlDeleteNumber);
  String[] argumentos = new String[1];
  argumentos[0] = sPhoneNumber;   
Log.d(TAG, "argumentos: " + argumentos[0]);
  db.execSQL(sqlDeleteNumber, argumentos);
    }
    
    public boolean isPhoneNumberInBlacklist(String sPhoneNumber){
     Cursor blacklistCursor = getBlacklistPhoneNumber(sPhoneNumber);
     String sBlacklistedPhoneNumber;
  while (!blacklistCursor.isAfterLast()) {
   sBlacklistedPhoneNumber = blacklistCursor.getString(0);
Log.d(TAG, "mCursor: " + "sBlacklistedPhoneNumber: "+ sBlacklistedPhoneNumber + " | sPhoneNumber: " + sPhoneNumber); 
   if(sBlacklistedPhoneNumber!=null && sBlacklistedPhoneNumber.equalsIgnoreCase(sPhoneNumber)){
    return true;
   }
   blacklistCursor.moveToNext();
  }
  return false;
    }
    
    
    public void insertPhoneNumber(String sPhoneNumber){
     String[] rowSet = new String[1];
     rowSet[0]=sPhoneNumber;
     db.execSQL(sqlInsertBlacklistedPhoneNumber, rowSet);
    }
    

    //---opens the database---
    public BlacklistPhoneNumberDBAdapter open() throws SQLException 
    {
        db = DBHelper.getWritableDatabase();
        return this;
    }
 
    //---closes the database---    
    public void close() 
    {
       DBHelper.close();
    }
    
    private static class DatabaseHelper extends SQLiteOpenHelper 
    {
        DatabaseHelper(Context context) 
        {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
 
        @Override
        public void onCreate(SQLiteDatabase db) 
        {
            db.execSQL(DATABASE_CREATE);
        }
 
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                              int newVersion) 
        {
            Log.w(TAG, "Upgrading database from version " + oldVersion 
                  + " to "
                  + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS "+DATABASE_NAME);
            onCreate(db);
        }
    }
}


Vista de la aplicación


La vista de la aplicación contiene un campo de texto android.widget.EditText para introducir números de teléfono, tres botones android.widget.Button para añadir, borrar y consultar números de lista negra y un android.widget.EditText multilínea no editable para mostrar los datos de consulta de la lista negra.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/insertar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/insertar" />

    <EditText
        android:id="@+id/editor_texto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberSigned" >
    </EditText>
    
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:orientation="horizontal" >
  <Button
        android:id="@+id/botonAniadir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/aniadir"
        android:onClick="onClickAddphoneNumber" />


    
     <Button
        android:id="@+id/botonBorrar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickDeletephoneNumber"       
        android:text="@string/borrar" />
     
      <Button
        android:id="@+id/botonConsulta"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickConsulta"       
        android:text="@string/consulta" />
      
      </LinearLayout>

      <EditText
     android:id="@+id/consulta"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:inputType="textMultiLine" 
  android:enabled="false">
  </EditText>
</LinearLayout>

Estos componentes gráficos son recuperados en el método onCreate de la actividad principal para poder actuar sobre la vista. En la actividad principal se crean los métodos onClick asociados a cada uno de los botones, onClickAddPhonenumber, onClickDeletephonenumber, onClickConsultar.
package com.m607;

import com.m607.database.BlacklistPhoneNumberDBAdapter;

import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class BlackListPhoneActivity extends Activity {
    /** Called when the activity is first created. */
 private Button btAceptar;
 private Button btBorrar;
 private EditText etEditorTexto;
 private EditText etConsulta;
 private BlacklistPhoneNumberDBAdapter dbBlacklist;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dbBlacklist = new BlacklistPhoneNumberDBAdapter(getBaseContext());
        dbBlacklist.open();
        setContentView(R.layout.main);
        
        
        btAceptar = (Button)findViewById(R.id.botonAniadir);
        btBorrar = (Button)findViewById(R.id.botonBorrar);
        etEditorTexto = (EditText)findViewById(R.id.editor_texto);
        etConsulta = (EditText)findViewById(R.id.consulta);
        

    }
    public void onClickAddphoneNumber(View view){
     if(!dbBlacklist.isOpen()){
      dbBlacklist.open();
     }
     dbBlacklist.insertPhoneNumber(etEditorTexto.getText().toString());
     
     Toast.makeText(getBaseContext(), "Añadido número " +etEditorTexto.getText().toString() , 1000);
     
     etEditorTexto.setText("");
    }
    public void onClickDeletephoneNumber(View view){
     if(!dbBlacklist.isOpen()){
      dbBlacklist.open();
     }

     dbBlacklist.deletePhoneNumber(etEditorTexto.getText().toString());
     Toast.makeText(getBaseContext(), "Borrado n�mero " +etEditorTexto.getText().toString() , 1000);
     etEditorTexto.setText("");
    }
    
    public void onClickConsulta(View view){
     if(!dbBlacklist.isOpen()){
      dbBlacklist.open();
     }
     etConsulta.setText(dbBlacklist.getBlacklistPhoneNumbersAsString());
    }
    
 @Override
 protected void onDestroy() {
  // TODO Auto-generated method stub
  super.onDestroy();
     if(dbBlacklist.isOpen()){
      dbBlacklist.close();
     }
 }
 @Override
 protected void onNewIntent(Intent intent) {
  // TODO Auto-generated method stub
  super.onNewIntent(intent);
 }
 @Override
 protected void onPause() {
  // TODO Auto-generated method stub
  super.onPause();
 }
 @Override
 protected void onPostCreate(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onPostCreate(savedInstanceState);
 }
 @Override
 protected void onPostResume() {
  // TODO Auto-generated method stub
  super.onPostResume();
 }
 @Override
 protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
  // TODO Auto-generated method stub
  super.onPrepareDialog(id, dialog, args);
 }
 @Override
 protected void onStart() {
  // TODO Auto-generated method stub
  super.onStart();
 }
 @Override
 protected void onStop() {
  // TODO Auto-generated method stub
  super.onStop();
 }
    
    
}


¿Como colgar la llamada?


Ya tenemos todo el entorno de persistencia y el interfaz gráfico para manejar el acceso a la persistencia, y queda controlar la llegada de llamadas entrantes, para verificar que es una llamada deseada y en caso contrario, colgarla. Esto, después de investigar un montón he visto que no puede hacerse de una manera mas o menos fácil, TelephonyManager no da opción a realizar esta acción, así que trás muuuucho investigar, lo único que he visto para colgar pasa por acceder por reflexión al interfaz com.android.internal.telephony.ITelephony que posee los métodos necesarios para manejar una llamada entrante,

package com.android.internal.telephony;

public interface ITelephony {

 boolean endCall();
 void answerRingingCall();
 void silenceRinger();
}


En el momento en que el BroadcastReceiver dispara un evento de llamada entrante se accede a este interfaz por Reflection, se compara el número de telefono con los que hay almacenados en la base de datos, y en caso de haber entrada, se cuelga la llamada.


package com.m607;

import java.lang.reflect.Method;

import com.android.internal.telephony.ITelephony;
import com.m607.database.BlacklistPhoneNumberDBAdapter;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

public class BlackListPhoneNumberBroadcastReceiver extends BroadcastReceiver {
 private final String TAG = "BlackListPhoneNumberBroadcastReceiver";
 private String telephonyServiceName = Context.TELEPHONY_SERVICE;
 private TelephonyManager telephonyManager;

 private BlackListPhoneNumerPhoneStateListener phoneStateListener;

 private BlacklistPhoneNumberDBAdapter dbAdapter;

 private ITelephony telephonyService;

 private void setTelephone(TelephonyManager tm) {
  try {
   if (telephonyService == null) {
    Class c = Class.forName(tm.getClass().getName());
    Method m = c.getDeclaredMethod("getITelephony");
    m.setAccessible(true);
    telephonyService = (ITelephony) m.invoke(tm);
   }
  } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

 }

 @Override
 public void onReceive(Context context, Intent intent) {

  // insertar en bbdd 12345678
  dbAdapter = new BlacklistPhoneNumberDBAdapter(context);
  dbAdapter.open();
  dbAdapter.insert("12345678");

  telephonyManager = (TelephonyManager) context
    .getSystemService(telephonyServiceName);

  phoneStateListener = new BlackListPhoneNumerPhoneStateListener();

  telephonyManager.listen(phoneStateListener,
    PhoneStateListener.LISTEN_CALL_STATE);

  if (telephonyManager.getCallState() == TelephonyManager.CALL_STATE_RINGING) {
   // incoming phonecall, get nr
   String phoneNumber = telephonyManager.getLine1Number();
   Log.d(TAG,
     "phone number on BR getLine1Number: callstate is ringing: "
       + phoneNumber);

   Bundle bundle = intent.getExtras();
   String phoneNr = bundle.getString("incoming_number");
   Log.d(TAG, "bundle.getString(incoming_number): " + phoneNr);

   if (dbAdapter.isPhoneNumberInBlacklist(phoneNr)) {

    Log.d(TAG, phoneNumber + " esta en lista negra. resultdata: "
      + getResultData());

    setTelephone(telephonyManager);
    telephonyService.endCall();
   }
  } else {
   Log.d(TAG, "phone number on BR: callstate is not ringing");
   Integer myInt = telephonyManager.getCallState();
   String error = "State NR:" + myInt.toString();
   Log.d(TAG, "error: " + error);
   // if (getResultData()!=null) {
   // setResultData(null);
   // }

  }

  if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
  }

 }

}


El método setTelephone es el que recupera por reflection la instancia al objeto ITelephony, que posteriormente se usa para colgar la llamada, llamando al método getITelephony de la clase TelephonyManager.

8 comentarios:

  1. Exelente aporte compañero.... muy buen ejemplo de como manejar bases de datos y hacer consultas...

    ResponderEliminar
    Respuestas
    1. Gracias por el comentario, sienta bien saber que esto no son horas perdidas y le es útil a la gente.

      Nos vemos por aquí.

      Eliminar
  2. Será posible usar com.android.internal.telephony.ITelephony para colgar llamadas salientes? Mis felicitaciones muy buen tutorial

    ResponderEliminar
  3. Hola,

    Creo que si, de hecho el método es indistinto para llamadas salientes o entrantes, solamente se debe escuchar el intent Intent.ACTION_NEW_OUTGOING_CALL.

    Aquí un pequeño trozo de código que debe incluirse en el onReceive del BroadcastReceiver.


    public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
    // end call
    }
    }


    y añadir al manifest



    Te de que no lo he probado, por falta de tiempo. Cuéntame si te funciona.

    ResponderEliminar
  4. En el manifest hay que añadir el permiso

    android.permission.PROCESS_OUTGOING_CALLS

    ResponderEliminar
  5. Donde puedo bajar el presente ejmplo?, Gracias

    ResponderEliminar
  6. Hola,

    Gracias por visitar el blog. El proyecto eclipse lo puedes encontrar aquí, aunque te aviso de antemano que es muy antiguo y puede que no funcione bien en versión modernas de Eclipse.

    https://dl.dropboxusercontent.com/u/73821490/BlackListPhone.zip


    Saludos

    ResponderEliminar