viernes, 25 de mayo de 2012

debug certificate expired android packaging problem


Me acabo de encontrar este problema al intentar probar una aplicación en modo "run". Investigando se soluciona en dos pasos:


1. borrando el fichero /.android/debug.keystore
2. haciendo un clean del proyecto en Eclipse.

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.

miércoles, 9 de mayo de 2012

Realidad Aumentada en Android. Acelerómetro y magnetómetro

Con este último ejemplo, termino con la serie de posts dedicados a un framework de realidad virtual en Android. Para ver un ejemplo básico de uso del acelerómetro en Android, voy a publicar el típico ejemplo de una Brújula.

Para desarrollar una aplicación que haga las veces de brújula debo implementar una vista especifica que pinte la flecha de indicación al Norte actualizándose cada vez que se obtenga una actualización del acelerómetro y magnetómetro.

La implementación de la clase View sobrescribe los métodos onDraw y onMeasure, y crea un método update que refresca el valor de la orientación (dirección) que se usa para dibujar la linea roja que apunta al norte. Cuando se extiende la clase View y no se sobreescribe el método onMesure, la aplicación falla en el momento de usar la vista. El método onDraw, dibuja un círculo con el radio devuelto por el menor valor entre el ancho y el alto de la pantalla y una linea roja en forma de radio del círculo, que indica el Norte y que toma como valor de cálculo la dirección calculada en el SensorListener de la clase BrujulaActivity. En el método update se refresca el valor de la dirección y se repinta la vista.

package com.m607;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class BrujulaView extends View {

 private float direction;
 private Paint paint;
 public BrujulaView(Context context) {
  super(context);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 public BrujulaView(Context context, AttributeSet attrs) {
  super(context, attrs);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 public BrujulaView(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    MeasureSpec.getSize(heightMeasureSpec));
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  paint.reset();
  int w = getMeasuredWidth();
  int h = getMeasuredHeight();
  int r;
  if (w > h) {
   r = h / 2;
  } else {
   r = w / 2;
  }

  
  paint.setStyle(Paint.Style.STROKE);
  paint.setStrokeWidth(5);
  paint.setColor(Color.WHITE);

  canvas.drawCircle(w / 2, h / 2, r, paint);

  paint.setColor(Color.RED);
  canvas.drawLine(w / 2, h / 2,
    (float) (w / 2 + r * Math.sin(-direction)), 
    (float) (h / 2 - r * Math.cos(-direction)), paint);

 }

 public void update(float dir) {
  direction = dir;
  invalidate();
 }
}


Por último queda la actividad que controla las actualizaciones en el acelerómetro y en el magnetómetro. En el método onCreate se recuperan los TextView donde se va mostrar información sobre los valores de los tres ejes de inclinación, azimut, pitch y roll. Además se recupera del main.xml el objeto BrujulaView sobre el que se actuará para notificar la dirección actual. Además se recuperan las instancias a los sensores, acelerómetro y magnetómetro mediante Sensor.TYPE_ACCELEROMETER y Sensor.TYPE_MAGNETIC_FIELD.
Se implementa el listener de sensores SensorEventListener implemtando la interfaz en el cuerpo de la actividad. Se sobrescribe el método onSensorChanged para actualizar la orientación del teléfono en relación al evento SensorEvent. En caso de ser un evento de acelerómetro se actualiza la martriz de valores de acelétometro valoresAcelerometro y equivalentemente para un evento de cambio de campo magnético.
Mediante el método getRotationMatrix al que se le pasan los valores almacenados en los arrays de valores de acelerómetro y mangnetómetro, se recuperan los valores de rotación en relación a la tierra, en vez de los valores en relación al móvil, que nunca cambian. Posteriormente se obtienen los valores de orientación mediante la llamada al método getOrientation y se asigna el primer valor del array, matrixValues a la dirección que sirve para repintar la linea roja que indica el Norte.

package com.m607;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;

public class BrujulaActivity extends Activity implements SensorEventListener {

 private SensorManager sensorManager;
 private Sensor sensorAcelerometro;
 private Sensor sensorCampoMagnetico;

 private float[] valoresAcelerometro;
 private float[] valoresCamposMagnetico;

 private float[] matrizRotacion;
 private float[] matrizInclinacion;
 private float[] matrixValues;

 private TextView readingAzimuth, readingPitch, readingRoll;
 
 private BrujulaView brujulaView;

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  readingAzimuth = (TextView) findViewById(R.id.azimuth);
  readingPitch = (TextView) findViewById(R.id.pitch);
  readingRoll = (TextView) findViewById(R.id.roll);

  brujulaView = (BrujulaView) findViewById(R.id.brujula);

  sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  sensorAcelerometro = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  sensorCampoMagnetico = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

  valoresAcelerometro = new float[3];
  valoresCamposMagnetico = new float[3];

  matrizRotacion = new float[9];
  matrizInclinacion = new float[9];
  matrixValues = new float[3];
 }

 @Override
 protected void onResume() {

  sensorManager.registerListener(this, sensorAcelerometro,
    SensorManager.SENSOR_DELAY_NORMAL);
  sensorManager.registerListener(this, sensorCampoMagnetico,
    SensorManager.SENSOR_DELAY_NORMAL);
  super.onResume();
 }

 @Override
 protected void onPause() {

  sensorManager.unregisterListener(this, sensorAcelerometro);
  sensorManager.unregisterListener(this, sensorCampoMagnetico);
  super.onPause();
 }

 @Override
 public void onAccuracyChanged(Sensor arg0, int arg1) {
  // TODO Auto-generated method stub

 }

 @Override
 public void onSensorChanged(SensorEvent event) {
  

  switch (event.sensor.getType()) {
  case Sensor.TYPE_ACCELEROMETER:
   
   System.arraycopy(event.values, 0, valoresAcelerometro, 0, event.values.length);
   
   break;
  case Sensor.TYPE_MAGNETIC_FIELD:
   System.arraycopy(event.values, 0, valoresCamposMagnetico, 0, event.values.length);
   break;
  }

  boolean exito = SensorManager.getRotationMatrix(matrizRotacion, matrizInclinacion,
    valoresAcelerometro, valoresCamposMagnetico);

  if (exito) {
   SensorManager.getOrientation(matrizRotacion, matrixValues);

   double azimuth = Math.toDegrees(matrixValues[0]);
   double pitch = Math.toDegrees(matrixValues[1]);
   double roll = Math.toDegrees(matrixValues[2]);

   readingAzimuth.setText("Azimuth: " + String.valueOf(azimuth));
   readingPitch.setText("Pitch: " + String.valueOf(pitch));
   readingRoll.setText("Roll: " + String.valueOf(roll));

   brujulaView.update(matrixValues[0]);
  }

 }
}

Por último, el main.xml se ve así:

<?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/azimuth"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <TextView
      android:id="@+id/pitch"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <TextView
      android:id="@+id/roll"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
  <View
      class="com.m607.BrujulaView"
      android:id="@+id/brujula"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
 
</LinearLayout>

Y el manifest de la aplicación:

<?xml version="1.0" encoding="utf-8"?>
<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" >
        <activity
            android:name=".BrujulaActivity"
            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>

viernes, 4 de mayo de 2012

Corrección de ejemplos en XML

Me he dado cuenta que los ejemplos que había colgado en formato XML estaban mal formateados y los he corregido ya. Disculpas a los que hayan tenido problemas con este error.

jueves, 3 de mayo de 2012

Realidad Aumentada en Android. Geolocalización

Acceso a la geolocalización
En los post anteriores sobre realidad aumentada se ha explicado como acceder a la cámera y a la brújula (sensor de orientación espacial) del dispositivo. En este post, se explica como acceder a la geolocalización mediante el dispositivo GPS del teléfono móvil.
Lo primero que hay que hacer para conversar con los satélites y dar permisos a la aplicación para que el objeto LocationManager pueda acceder al hardware del dispositivo. Para ello, como siempre, se edita el fichero AndroidManifest.xml y se añaden estas dos lineas

<?xml version="1.0" encoding="utf-8" ?> 
<manifest android:versioncode="1" android:versionname="1.0" package="com.m607" xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-sdk android:minsdkversion="10">
    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
       
        <activity android:configchanges="orientation|keyboardHidden" 
               android:label="@string/app_name" 
      android:name=".RealidadAumentadaActivity" 
      android:screenorientation="landscape" 
      android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN">

                <category android:name="android.intent.category.LAUNCHER">
            </category></action></intent-filter>
        </activity>
        
    </application>
 <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

</uses-sdk>



Sin estas dos líneas cada vez que la aplicación intente registrar cambios de localización, Android lanzará una excepción de seguridad y parará la aplicación.
Una vez se han incluido estas dos líneas en el manifest es el momento de implementar el listener de localización GPS;
package com.m607.orientation;

import android.location.Location;
import android.location.LocationListener;
import android.os.Bundle;

public class GPSListener implements LocationListener {
 Location localizacionActual;
 boolean cambioLocalizacion = false;

 public void onLocationChanged(Location location) {
  if (localizacionActual == null) {
   localizacionActual = location;
   cambioLocalizacion = true;
  }

  if (localizacionActual.getLatitude() == location.getLatitude()
    && localizacionActual.getLongitude() == location
      .getLongitude())
   cambioLocalizacion = false;
  else
   cambioLocalizacion = true;

  localizacionActual = location;
 }

 public void onProviderDisabled(String provider) {
 }

 public void onProviderEnabled(String provider) {
 }

 public void onStatusChanged(String provider, int status,
   Bundle extras) {
 }
}




Esta clase implementa el método onLocationChanged que pasa como argumento el objeto Localtion con la nueva localidación del dispositivo. En caso de encontrar un cambio en la localización actual frente a la localización anterior, se guarda la actual en la variable almacén y se actualiza el valor del booleano que indica el cambio de localización.
Escrito el listener queda asignarlo al objeto LocationMaganer que se recupera del contexto de la actividad principal por medio del método getSystemService pasando como argumento el Context.LOCATION_SERVICE. Posteriormente se requiere actualizaciones en la geolocalización llamando al método requestLocationUpdates, asignando un poll de 100 milisegundos, la distancia mínima para disparar un evento de localización (en este caso 1 metro) y el escuchador de evento de localización, LocationListener.
...
public void onCreate(Bundle savedInstanceState)
{
LocationManager locationManager;
locationManager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
100, 1, gpsListener);
}


...

martes, 1 de mayo de 2012

Realidad Aumentada en Android. Sensor de Orientación

El sensor de orientación ofrece la posibilidad de acceder a orientación del móvil en los tres ejes espaciales. Así, en el momento en el que se detecta un cambio en la orientación del dispositivo, el listener del sensor ofrece un evento SensorEvent que da acceso a un array de tres elementos:
  • Heading: la orientación en grados desde el eje X, o norte.
  • Roll: la orientación en grados respecto al eje Z.
  • Pitch: la orientación en grados respescto al eje Y.

Mediante una llamada al método getSystemService del contexto de la actividad se recupera un manejador a los sensores que servirá para acceder al sensor de orientación.
 

...

SensorManager sensorMan = (SensorManager)ctx.getSystemService(Context.SENSOR_SERVICE);

...


Posteriormenete, se recupera el sensor de orientación, indicando el tipo de sensor que se quiere recuperar SensorManager.SENSOR_ORIENTATION, el retardo con el que se recuperaran los cambios en la orientación, y el listener que implementa la acción en caso de que se detecte un cambio en la orientación.
 

...

sensorMan.registerListener( listener,
       sensorMan.getDefaultSensor(SensorManager.SENSOR_ORIENTATION),
       SensorManager.SENSOR_DELAY_NORMAL);

...



Los retardos pueden ser de cuatro tipos, SENSOR_DELAY_FASTEST, SENSOR_DELAY_UI, SENSOR_DELAY_GAME y SENSOR_DELAY_NORMAL.
Queda definir el listener que implementa la clase SensorEventListener y que sobreescribe los métodos onSensorChanged y onAccuracyChanged:



  SensorEventListener listenerOrientacion = new SensorEventListener(){
     
     public void onAccuracyChanged(Sensor arg0, int arg1)
     {}

     public void onSensorChanged(SensorEvent evt)
     {
      if (evt.sensor.getType() == Sensor.TYPE_ORIENTATION) { 
       direccion = evt.values[0];
      }
     }
  };



listenerOrientacion simplemente sobreescribe el método onSensorChanged para almacenar en la variable direccion la dirección sobre el eje x que tiene el dispositivo móvil en el momento de recibir el evento de cambio de dirección.
En el método se discrimina por el tipo de evento ya que existen muchos tipos de evento de sensor, y puede que en otras aplicaciones tengamos mas de un tipo de sensor actuando sobre el dispositivo. En este caso, se discrimina por tipo de evento de orientación Sensor.TYPE_ORIENTATION
La clase completa que reacciona a eventos de cambio de orientación:
package com.m607.orientation;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

public class Orientation { 
 
 public SensorManager sensorManager;
 public float direccion;
 
 public Orientation(Context context){
  SensorEventListener listenerOrientacion = new SensorEventListener(){
     
     public void onAccuracyChanged(Sensor arg0, int arg1)
     {}

     public void onSensorChanged(SensorEvent evt)
     {
      if (evt.sensor.getType() == Sensor.TYPE_ORIENTATION) { 
       direccion = evt.values[0];
      }
     }
  };
  
  sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
  sensorManager.registerListener(
          listenerOrientacion, 
          sensorManager.getDefaultSensor(
             SensorManager.SENSOR_ORIENTATION), 
             SensorManager.SENSOR_DELAY_NORMAL);
 }
}

Dong Nan Xi Bei