jueves, 11 de octubre de 2012

Una oferta de 500 euros al mes para un ingeniero con experiencia

En Infojobs han publicado esta oferta de trabajo

Parece ser que posteriormente se ha montado un follón de los que se suelen montar en internet, y les ha dado tanta vergüenza (o no) que han retirado el salario.
Se habla mucho de Responsabilidad Corporativa, Código Ético en el mundo empresarial, etc., etc. pero ¿Una empresa que es un portal de oferta y demanda de trabajo, no debería tener un código ético por el cual debiese rechazar este tipo de esperpentos? En fin, parace que ya solo nos queda internet para sacar los colores a estos pájaros.

miércoles, 3 de octubre de 2012

Just Eat compra Sindelantal

Leo en El Confidencial que Just Eat compra Sindelantal La venta del año: Just Eat compra la 'startup' española Sindelantal.com. Iñaki Arrola en su blog se felicita de la venta, como inversor no me extraña y lo llama un éxito Un gran exit. Just Eat compra Sindelantal. ¿Es un éxito que te absorba la competencia? No se, no lo creo.
Desde luego, es una realidad que los inversores y los emprendedores del proyecto se han puesto las botas. Si el bussines plan era ese, enhorabuena, pero me parece muy triste. Se que es muy injusto llamarlo pelotazo, porque hay una gran cantidad de trabajo detrás, pero es que se parece tanto...

martes, 2 de octubre de 2012

XML Parser en Android

A la hora de analizar un fichero XML y obtener información de él, Android ofrece su propia clase parser basada en SAX org.xmlpull.v1.XmlPullParser.
Para analizar un fichero XML este primero debe colocarse en la carperta de recursos del proyecto Android. En este caso, en la carpeta raw. En caso de no existir esta carpeta debe crease e incluir el fichero XML en esta ruta.
El flujo de entrada del fichero se obtiene del contexto de la actividad, de la siguiente manera,

...
InputStream inputStream = context.getResources().openRawResource(R.raw.fichero);
  
...
Este InputStream es pasado posteriormente al parser para iniciar el análisis del fichero. La manera de obtener el parser y asignar el flujo del fichero a este se realiza como sigue:
...
  XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
  factory.setNamespaceAware(true);
  XmlPullParser xpp = factory.newPullParser();
  InputStream inputStream =    context.getResources().openRawResource(R.raw.fichero);
  xpp.setInput(inputStream, null);
...

En este trozo de código se obtine un objeto XmlPullParser mediante un pattern Factory. Mediante el método setInput se asigna el fichero XML a analizar. Por último, se debe saber como recoger la información almacenada en el fichero XML. Al igualque SAX, que se maneja por eventos, aquí se debe recorrer el fichero llamando al siguiente evento de análisis mediante el método next().
...


  while (eventType != XmlPullParser.END_DOCUMENT) {
   if (eventType == XmlPullParser.START_DOCUMENT) {
    System.out.println("Start document");
   } else if (eventType == XmlPullParser.END_DOCUMENT) {
    System.out.println("End document");
    
   } else if (eventType == XmlPullParser.START_TAG) {
    
    System.out.println("Start tag " + xpp.getName() + " Text " + xpp.getText());
    
   } else if (eventType == XmlPullParser.END_TAG) {
    System.out.println("End tag " + xpp.getName() + " Text " + xpp.getText());
   } else if (eventType == XmlPullParser.TEXT) {
    System.out.println("--- Tag " + xpp.getName() + " Text " + xpp.getText());
   }
   eventType = xpp.next();
  }

...


En este ejemplo, se atiende al tipo de evento siguiente, pero también se puede acceder a distintos tipos de información como son el nombre del nodo, los atributos, etc..

viernes, 31 de agosto de 2012

OpenGL ES

En el blog de androideity hay dos post muy buenos sobre como dibujar una pirámide en un proyecto Android, usando para ello OpenGL ES 1.0.

Aquí está los enlaces:

La aplicación funciona correctamente si se realiza un cambio en la definición de las caras;

 // La forma como vamos a conectarlos

 private short caras[] = {

 0, 1, 2,

 0, 2, 3,

 0, 1, 3,

 1, 2, 3

 //2, 3, 4,

 //3, 0, 4

 };

Código fuente del proyecto:OpenGLProject

jueves, 9 de agosto de 2012

Error de visualización de código con SyntaxHighlighter

Acabo de darme cuenta que los número de línea de código fuente en Chrome hacen cosas feas, feas, feas. Googleando he visto que es un problema del css de SyntaxHighlighter, en el que hay que cambiar una linea para que todo se solucione.
.syntaxhighlighter table td.gutter .line {
  text-align: right !important;
  padding: 0 0.5em 0 1em !important;
}

por
.syntaxhighlighter table td.gutter .line {
  text-align: right !important;
  padding: 0 5px  !important;
}

Se cambia esto en la plantilla de Blogger y listo. En la consola de administración del blog se va uno a Plantilla > Edición de HTML y en el se comenta la línea


Y se añade el bloque:
 

viernes, 29 de junio de 2012

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

 

lunes, 30 de abril de 2012

Realidad Aumentada en Android. Deshabilitar auto rotación

He visto que cuando se rota el móvil, la actividad de la cámara se cae. Por lo que he estado investigando, cuando se rota el móvil se destruye la actividad y se vuelve a crear, por lo que el acceso a la cámara también se cae. La única solución que he encontrado es deshabilitar la autorotación desde el manifest de la aplicación

...
        <activity
            android:name=".RealidadAumentadaActivity"
            android:label="@string/app_name" 
            android:screenOrientation="landscape"
            android:configChanges="orientation|keyboardHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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


La propiedad dentro del manifest que configChanges, que indica para que eventos de la actividad, esta no se destruye. Además, se indica que la aplicación se lance en modo landscape, mediante la propiedad screenOrientation

Actualización: Para que no aparezca la barra del título de la aplicación, se puede modificar el manifest de la aplicación de nuevo, añadiendo la propiedad android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" a la actividad principal RealidadAumentadaActivity

...
      
        <activity
            android:name=".RealidadAumentadaActivity"
            android:label="@string/app_name" 
            android:screenOrientation="landscape"
            android:configChanges="orientation|keyboardHidden"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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


jueves, 26 de abril de 2012

Realidad Aumentada en Android. Acceso a la cámara.

Realidad Aumentada es la habilidad de superponer puntos de información sobre la vista ofrecida por la cámara del dispositivo móvil. La Realidad Aumentada permite aumentar la información ofrecida por la cámara del dispositivo móvil, e interacturar con sensores y dispositivos como el magnetómetro, acelerómetro o geolocalización y ofrecer esta información añadida junto con la imagen ofrecida por la cámara. A la hora de desarrollar alguna aplicación de realidad aumentada, la base es acceder a los dispositivos y sensores del teléfono móvil; cámara, mágnetometro y acelerómetro y GPS.


Acceso a la vista de la cámara


El primer paso para implementar un entorno de realidad aumentada es acceder a la cámara para tener una vista del entorno real en el que se ofrece la funcionalidad deseada. Se debe dar permiso a la aplicación para acceder a la cámara del dispositivo, para lo que se modifica el Manifest de la aplicación


<application android:icon="@drawable/icon" 
                android:label="@string/app_name"
                android:debuggable="true">
...
<uses-permission android:name="android.permission.CAMERA" />
...
</application>



Lo siguiente es tener acceso a la vista de superficie de la camara sobre la que se va a pintar las imágenes capturadas. por la cámara. Para esto se extiende la clase SurfaceView. Se debe acceder a la clase SurfaceHolder para acceder y controlar la superficie subyacente, asi mismo, se debe implementar la clase interna Callback de la clase SurfaceHolder para recibir información acerca de cambios en la superficie. La superficie manejada es solo accesible entre llamadas a los métodos surfaceCreated y surfaceDestroyed. La implementación de la clase Callback se añade a la clase SurfaceHolder mediante el método addCallback.
En el método surfaceCreated recupero una instancia a la cámara mediante Camera.open y posteriormente se añade el acceso a la superficie subyacente mediante el método setPreviewDisplay. En el método surfaceChanged se actualizan los parámetros de la cámara y se inicia la previsualización de la imagen en la cámara. El método surfaceDestroyed cierra la cámara y libera recursos de la cámara.


package com.m607.camera;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.view.SurfaceHolder;

public class CameraCallback implements SurfaceHolder.Callback{
    Camera camera;
 Context ctx;
 SurfaceHolder surfaceHolder;

 public CameraCallback(Context ctx, SurfaceHolder surfaceHolder){
  this.ctx = ctx;
  this.surfaceHolder= surfaceHolder;
 }

 public void surfaceCreated(SurfaceHolder holder) {
  camera = Camera.open();
  try {
   camera.setPreviewDisplay(surfaceHolder);
  } catch (Throwable t) {
  }
 }

 public void surfaceChanged(SurfaceHolder holder, int format,
   int width, int height) {
  Parameters parameters = camera.getParameters();
  parameters.setPreviewSize(width, height);
  parameters .setPictureFormat(PixelFormat.JPEG);
  
  if (ctx.getResources().getConfiguration().orientation !=
   Configuration.ORIENTATION_LANDSCAPE) {
   // This is an undocumented although widely known feature
   parameters.set("orientation", "portrait");
   // For Android 2.2 and above
   //camera.setDisplayOrientation(90);
   // Uncomment for Android 2.0 and above
   //parameters.setRotation(90);
   } else {
   // This is an undocumented although widely known feature
   parameters.set("orientation", "landscape");
   // For Android 2.2 and above
   //camera.setDisplayOrientation(0);
   // Uncomment for Android 2.0 and above
   //parameters.setRotation(0);
   }
  

  
  camera.setParameters(parameters);
  camera.startPreview();
 }

 public void surfaceDestroyed(SurfaceHolder arg0) {
  stopCamera();
 }
 public void stopCamera(){
  camera.stopPreview();
  camera.release();
  camera = null;
 }

}








package com.m607.camera;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class CameraView extends SurfaceView {

 SurfaceHolder surfaceHolder;

 CameraCallback surfaceHolderCallback;

 public CameraView(Context ctx) {
  super(ctx);

  surfaceHolder = this.getHolder();
  surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
 
  surfaceHolderCallback = new CameraCallback(ctx, surfaceHolder);
  surfaceHolder.addCallback(surfaceHolderCallback);

 }
 
 public void stopCamera(){
  surfaceHolderCallback.stopCamera();
 }
 
}


Solo falta acceder a la manejador de la cámara desde la actividad principal



 private CameraView cameraView;
        /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
    try{
       super.onCreate(savedInstanceState);
       cameraView = new CameraView(
          this.getApplicationContext());
                 this.setContentView(cameraView);
    } catch(Exception e){}
    }

miércoles, 4 de abril de 2012

Primera Aplicación. Parte 5. Final

Antes que nada, en la entrada Primera Aplicación. Parte 4 se me ha olvidado indicar que para que la aplicación tenga acceso al sensor GPS del dispositivo, debe incluirse en el manifest de la aplicación la siguiente línea
...

    <uses-sdk android:minsdkversion="10">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION">
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION">
    <uses-permission android:name="android.permission.INTERNET">
...

    </uses-sdk>

Para finalizar la serie de entradas para la primera aplicación falta indicar como se lanzará la aplicación. He decidido lanzarla en background como servicio. Para ello, he creado la clase com.m607.MainService que extiende la clase android.app.Service

package com.m607;

import com.m607.database.DBManager;
import com.m607.sound.SoundManager;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.IBinder;
import android.util.Log;

public class MainService extends Service {
 
 private DBManager db;
 private SoundManager sm;
 @Override
 public void onCreate() {
  Log.v("MainService", "Service created");
  db = new DBManager(this); 
         db.open();
     
         sm = new SoundManager();
         sm.initSounds(getBaseContext());
         sm.addSound(1, R.raw.sound);
         sm.addSound(2, R.raw.beep4);
         sm.addSound(3, R.raw.beep9);
     
     
  LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  LocationListener ll = new GpsLocationListener(this,db, sm);
  lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 50, ll);
  
     
     if(db.getRadarCount()==0){
         db.deleteAllRadars();
         db.readCSV("es-allcam.txt");
     }
 }
 
 
 @Override
 public IBinder onBind(Intent arg0) {
  // TODO Auto-generated method stub 
  return null;
 }
 
 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  return Service.START_STICKY;
 }
}

Esta clase instancia e inicializa el manejador de sonidos, el manejador de base de datos y el localizador GPS indicando que realice llamadas de refresco al dispositivo GPS cada segundo. En caso de que la tabla de radares de la base de datos este vacía, lee el fichero de radares y los inserta en base de datos.

Esta clase es instanciada desde el método onCreate de la actividad principal, com.m607.MainActivity, mediante una llamada al método startService de la clase Activity

<
package com.m607;

import android.app.Application;
import android.content.Intent;

public class MainActivity extends Application {
 @Override
 public final void onCreate() {
  super.onCreate();
  // se crea un servicio para localizar por GPS
  startService(new Intent(this, MainService.class));
  
 }
}
Por último, se añaden la actividad principal y el servicio en el manifest de la aplicación
...
   <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
       <service android:enabled="true" android:name=".MainService" />
       <activity android:label="@string/app_name" android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN">
                <category android:name="android.intent.category.LAUNCHER">
            </category></action></intent-filter>
        </activity>
    </application>

...

Resumen de artículos para la aplicación GPS:

Primera aplicación. Parte 1
Primera aplicación. Parte 2
Primera Aplicación. Parte 3
Primera Aplicación. Parte 4
Primera Aplicación. Parte 5


La aplicación completa se puede descargar aquí:

Primera Aplicacion. Avisador Radar para Vehículos

Magento

Estoy en estos momentos creando una tienda on-line, y para ello he seguido el consejo de un amigo y he decido usar Magento como software para la tienda. Magento es la típica aplicación LAMP (Linux, Apache, Mysql, PHP), que aunque al principio parece un follón cuando se empieza a conocer a fondo es muy interesante. Estaba acostumbrado a osCommerce, que también está muy bien para crear tiendas virtuales en Internet, pero es qué Magento, tiene hasta la posibilidad de crear aplicaciones para móvil (Android, iPhone, etc.) desde el panel de control de la tienda. Según vaya avanzando con el cacharreo de la aplicación, iré posteando mis avances.

jueves, 23 de febrero de 2012

Geeksphone. Smartphone ibérico.

Escribo esta entrada para hacer un poquito de patria.

La empresa Geekphone es una empresa española que se dedica a hacer smarphones que usan sistema operativo Android y que no desmerecen a los que hay en el mercado y que está muy bien de precio.

 Yoigo me ha "regalado" un Android Samsung mini que está muy bien, pero me hubiera hecho más ilusión que me regalaran un Geekphone ZERO. En fin, por ahora por lo que veo en la web de Geekphone, solo Pepephone ofrece el dispositivo.



A ver si cunde el ejemplo en otros operadores que operan en España.

En Xataka hacen un análisis del dispositivo.

  GeeksPhone Zero, análisis (I)
  GeeksPhone Zero, análisis (II)

Actualización:

Veo que además Javier Agüera, que parece ser es el creador de esta empresa, de 19 años!!!!!, ha sido galardonado con el Premio TR35 que el Massachussets Institute Technology (MIT) concede a jóvenes emprendedores menores de 35 años. Simplemente impresionante. Aquí dejo el enlace a la noticia

 Un emprendedor de 19 años gana el TR35 del MIT


Además están a punto de sacar al mercado el Geeksphone TWO.

Primera Aplicación. Parte 4. Localización GPS

Queda para terminar la aplicación, actuar con el dispositivo GPS para obtener la localización del dispositivo móvil. Para ello, se implementa el interfaz android.location.LocationListener, que escucha cambios de localización del dispositivo. Para ello se crea la clase GpsLocationListener, que en un cambio de localización, consulta a la base de datos para saber si existen radares en las inmediaciones del dispositivo, y en caso de encontrar entradas para esas coordenadas, se avisa al conductor mediante un sonido y con un mensaje de aviso, mediante la clase android.widget.Toast
public class GpsLocationListener implements LocationListener {
Se sobreescribe el método onLocationChanged;
 public void onLocationChanged(Location location) {
  
  if (location != null) {
   Log.d("LOCATION CHANGED LATITUDE", location.getLatitude() + "");
   Log.d("LOCATION CHANGED LONGITUDE", location.getLongitude() + "");
   
   dLatitude = location.getLatitude();
   dLongitude = location.getLongitude();

   cursorRadarsByLocation = db.selectRadarsByLocation(location);
   cursorRadarsByLocation.moveToFirst();
      
            while (!cursorRadarsByLocation.isAfterLast()){          
             Location radarLocation = new Location(location);
             radarLocation.setLatitude(cursorRadarsByLocation.getDouble(0));
             radarLocation.setLongitude(cursorRadarsByLocation.getDouble(1));
             
Log.v("GpsLocationListener", "RadarDB: " + radarLocation.getLatitude() + " lon " +radarLocation.getLongitude() + " GPS: " + location.getLatitude() +":" + location.getLongitude() + " : Distance: " + radarLocation.distanceTo(location));

    if(radarLocation.distanceTo(location)<500){
Log.v("GpsLocationListener", "RADAR! la:" + location.getLatitude() + " lo:" + location.getLongitude());    
        
     View layout = inflater.inflate(R.layout.toast_layout, null);
     
     ImageView image = (ImageView) layout.findViewById(R.id.image);
     image.setImageResource(R.drawable.radar);
     TextView text = (TextView) layout.findViewById(R.id.text);
     text.setText("RADAR! la:" + location.getLatitude() + " lo:" + location.getLongitude());
     
     Toast toast = new Toast(context.getApplicationContext());
     toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
     toast.setDuration(Toast.LENGTH_LONG);
     toast.setView(layout);
     toast.show();


        sm.playSound(2);
             }
    cursorRadarsByLocation.moveToNext();
            }
  }
 }

En este método, se almacenan las coordenadas de la nueva localización y se realiza una consulta a la base de datos mediante una llamada al método selectRadarsByLocation. En caso de encontrar radares en las inmediaciones, se verifica que el radar se encuentre en las inmendiaciones mediante el método distanceTo de la clase Location, y en caso de cumplir la condición, se avisa al usuario mediante una imagen y un sonido. Para crear un componente que Toast con una imagen y un texto, se crea el layout toast_layout.xml y se almacena en "res/layout"
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/toast_layout_root"
              android:orientation="horizontal"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:padding="10dp"
              android:background="#DAAA"
              >
    <ImageView android:id="@+id/image"
               android:layout_width="wrap_content"
               android:layout_height="fill_parent"
               android:layout_marginRight="10dp"
               />
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="fill_parent"
              android:textColor="#FFF"
              />
</LinearLayout>

Este layout esta compuesto por una imagen ImageView identificado por "@+id/image" y un texto TextView identificado por "@+id/text". Posteriormente, en el método onLocationChanged se recuperan estas vistas mediante sus identificadores R.id.image y R.id.text. Este layout se asigna a la vista del objeto toast y se muestra llamando a su método show.
View layout = inflater.inflate(R.layout.toast_layout, null);
     
     ImageView image = (ImageView) layout.findViewById(R.id.image);
     image.setImageResource(R.drawable.radar);
     TextView text = (TextView) layout.findViewById(R.id.text);
     text.setText("RADAR! la:" + location.getLatitude() + " lo:" + location.getLongitude());
     
     Toast toast = new Toast(context.getApplicationContext());
     toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
     toast.setDuration(Toast.LENGTH_LONG);
     toast.setView(layout);
     toast.show();
Posteriormente se llama al manager de sonido mediante una clase SoundManager sm, que se ha implementado previamente
package com.m607.sound;

import java.util.HashMap;

import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.util.Log;



public class SoundManager {
 
 private  SoundPool spSoundPool; 
 private  HashMap<Integer, Integer> hmSoundPoolMap; 
 private  AudioManager  amAudioManager;
 private  Context context;
 
 
 public SoundManager()
 {
  
 }
  
 public void initSounds(Context theContext) { 
   context = theContext;
      spSoundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0); 
      hmSoundPoolMap = new HashMap<Integer, Integer>(); 
      amAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);       
 } 
 
 public void addSound(int Index,int SoundID)
 {
  hmSoundPoolMap.put(Index, spSoundPool.load(context, SoundID, 1));
 }
 
 public void playSound(int index) { 
      int streamVolume = amAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 
      spSoundPool.play(hmSoundPoolMap.get(index), streamVolume, streamVolume, 1, 0, 1f); 
 }
 
 public void playLoopedSound(int index) { 
  
      int streamVolume = amAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 
      spSoundPool.play(hmSoundPoolMap.get(index), streamVolume, streamVolume, 1, -1, 1f); 
 }
}
Esta clase se instancia en la clase en el Service principal, de la siguiente manera
     sm = new SoundManager();
     sm.initSounds(getBaseContext());
     sm.addSound(1, R.raw.sound);
     sm.addSound(2, R.raw.beep4);
     sm.addSound(3, R.raw.beep9);
     
Los sonidos, como las imágenes, se han almacenado en res/raw y se recuperan como se muestra en el trozo de código anterior.

martes, 24 de enero de 2012

Primera Aplicación. Parte 3

Cargando los datos en base de datos.

Para cargar los datos del fichero CSV en base de datos, es necesario añadir este fichero en el proyecto de Eclipse. Para ello es necesario importar este archivo en la carpeta assets del proyecto.

Para ello, hay que seleccionar "File>Import..." en el proyecto actual.

Posteriormente hay que seleccionar "File System":
Selecciono el directorio donde esta almacenado el fichero es-allcam.txt y se importa este fichero a la carpeta assets del proyecto. Una vez incluido este fichero en el proyecto, accedo a él para parsearlo e insertar la información en base de datos. El siguiente código accede al fichero es-allcam.txt, abre un flujo de lectura y parsea los datos fila por fila del fichero insertando los datos como una fila en la tabla de base de datos mediante el método insertRadar
 
 public boolean readCSV(String radarCSVFile) {
  try {
   InputStream is = context.getAssets().open("es-allcam.txt");
   InputStreamReader isr = new InputStreamReader(is);
   BufferedReader in = new BufferedReader(isr);
   in.readLine();
   String reader = "";
   String[] RowData;
   db.beginTransaction();
   while ((reader = in.readLine()) != null) {
    RowData = reader.split(",");
    Log.w(TAG, "Inserting... " + RowData[1] + " " + RowData[0]
      + " " + RowData[2] + " " + RowData[3] + " "
      + RowData[4] + " " + RowData[5] + " " + "...");
    
    insertRadar(RowData[1], RowData[0], RowData[2], RowData[3],
      RowData[4], RowData[5], "");

   }
   db.setTransactionSuccessful();

   db.releaseMemory();
   in.close();
  } catch (Exception e) {
   db.releaseMemory();
   e.printStackTrace();
  } finally {
   db.endTransaction();
  }
  return true;
 }

El método insertRadar luce como se muestra a continuación
 public void insertRadar(String latitude, String longitude,
   String radar_type, String speed, String direction_type,
   String direction, String postal_code) {
  rowSet[0] = latitude;
  rowSet[1] = longitude;
  rowSet[2] = radar_type;
  rowSet[3] = speed;
  rowSet[4] = direction_type;
  rowSet[5] = direction;
  rowSet[6] = postal_code;

  // return db.insert(DATABASE_TABLE, null, initialValues);
  db.execSQL(sqlInsertRadar, rowSet);

 
 }

Implemento unos cuantos métodos para tratar los datos de radar en tablas. El siguiente método actualiza los radars por id de radar en base de datos,

 // ---updates a Radar---
 public boolean updateRadar(long rowId, String latitude, String longitude,
   String radar_type, String speed, String direction_type,
   String direction, String postal_code) {
  Log.v(TAG, "update radar " + rowId);
  ContentValues args = new ContentValues();
  args.put(KEY_LATITUDE, latitude);
  args.put(KEY_LONGITUDE, longitude);
  args.put(KEY_RADARTYPE, radar_type);
  args.put(KEY_SPEED, speed);
  args.put(KEY_DIRECTION_TYPE, direction_type);
  args.put(KEY_DIRECTION, direction);
  args.put(KEY_POSTALCODE, postal_code);
  return db.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0;
 }

Este método sirve para recuperar los radares que se encuentren cerca de cierta localización dada por un objeto Location. Este método se utilizará posteriormente para buscar radares en el momento de que se actualice la localización del móvil mediante su localizador GPS.

 public Cursor getRadarsByLocation(Location location) {

  String sConditional = KEY_LATITUDE + " BETWEEN "
    + String.valueOf(location.getLatitude() + 0.001) + " AND "
    + String.valueOf(location.getLatitude() - 0.001) + " AND "
    + KEY_LONGITUDE + " BETWEEN "
    + String.valueOf(location.getLongitude() + 0.001) + " AND "
    + String.valueOf(location.getLongitude() - 0.001);

  Log.v(TAG, sConditional);

  if (location != null) {

   Log.v(TAG, "get radar by location: " + location.getLatitude() + " "
     + location.getLongitude());

   mCursor = db.query(true, DATABASE_TABLE, new String[] { KEY_ROWID,
     KEY_LATITUDE, KEY_LONGITUDE, KEY_RADARTYPE, KEY_SPEED,
     KEY_DIRECTION_TYPE, KEY_DIRECTION, KEY_POSTALCODE },
     sConditional, null, null, null, null, null);
  }
  if (mCursor != null) {
   mCursor.moveToFirst();
  }
  return mCursor;

 }

domingo, 22 de enero de 2012

Primera aplicación. Parte 2

Obvio lo obvio, como bajarse el IDE Eclipse, instalar Android SDK y el ADT de eclipse.
Para una guia muy bien explicada

http://developer.android.com/sdk/index.html

Para los que además tienen un equipo con arquitectura de 64 bits.

http://developer.android.com/sdk/installing.html#troubleshooting

Comienzo con la base de datos.
Los datos de localización de los radares móviles los he sacado de la página SpeedCameraPOI.com de la que se puede bajar un fichero txt en formato csv con los datos de localización de los radares.


1:  lon,lat,TYPE,SPEED,DIRTYPE,DIRECTION
2:  -17.851917,28.653617,5,0,0,0
3:  -16.825796,28.198766,5,0,0,0
4:  -16.817533,28.265373,5,0,0,0
5:  -16.797592,28.168118,5,0,0,0
6:  -16.797476,28.241514,5,0,0,0
7:  -16.791524,28.147537,5,0,0,0
8:  -16.765271,28.183670,5,0,0,0
9:  -16.653410,28.394205,5,0,0,0
10:  -16.580407,28.391820,5,0,0,0
11:  -16.575469,28.061445,5,0,0,0
12:  -16.373555,28.355344,5,0,0,0
13:  -16.349598,28.489710,5,0,0,0
14:  -16.273584,28.443848,5,0,0,0
15:  ....
Una vez que tengo los datos de localización de los radares, y viendo la estructura que poseen, decido que la base de datos va a consistir de una sola tabla RADARLOCATION con seis columnas; LONGITUDE, LATITUDE, RADARTYPE, SPEED, DIRECTIONTYPE

De acuerdo. Ahora viene lo divertido, ¿Como accedo a una base de datos sqlite?

Para ello Android ofrece la clase abstracta android.database.sqlite.SQLiteOpenHelper con los métodos onCreate y onUpgrade. El método onCreate se llama la primera vez que se crea la base de datos, en mi caso, RADAR_LOCATION y con el método onUpgrade, se actualiza la base de datos, en caso de que modifiquemos la versión de esta.

 private static class DatabaseHelper extends SQLiteOpenHelper {
  DatabaseHelper(Context context) {
   super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
   Log.w(TAG, "Create database " + DATABASE_NAME);
   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_TABLE);
   onCreate(db);
  }
 }
La creación de la base de datos se realiza en el constructor de la clase DatabaseHelper que hereda SQLiteOpenHelper, llamando al constructor de la clase heredada, pasando como argumento el nombre de la base de datos, en este caso radars y la versión de la base de datos. DATABASE_CREATE es un String con la sentencia SQL para crear la base de datos:
 private static final String DATABASE_CREATE = "create table "
   + DATABASE_TABLE
   + " (_id integer primary key autoincrement, "
   + "latitude real not null, longitude real not null, radar_type integer not null, speed integer not null, direction_type integer not null, direction integer not null, postal_code text)";
En caso de que la versión de la base de datos se modifique, se ejecuta el método onUpgrade, que hace un drop de la tabla radar_location y vuelve a crearla. Queda por último crear la clase que hará uso de esta clase Helper, Para la siguiente entrada...

lunes, 16 de enero de 2012

Primera aplicación. Parte 1



He decidido realizar una aplicación avisadora de radar de carretera. Lo he decidido así porque se pueden tocar muchos palos con una aplicación como esta. Las funcionalidades de las que esta compuesta la aplicación son las siguientes:

Acceso a base de datos
API de localización GPS
Uso de servicios Android
API multimedia para el uso de sonidos.
Personalización de vistas

En esencia, la aplicación cargará en una base de datos sqlite los datos relativos a la posición de los radares que existen en la red de carreteras de España, y usará estos datos para compararlos con la posición GPS actual. En caso de que exista un radar de carretera en una distancia específica respecto a la distancia recogida por el teléfono móvil, notificará este evento al usuario mediante un mensaje en la pantalla del móvil y un sonido de alarma.

La aplicación correrá en background haciendo uso la clase del API de Android Service.



miércoles, 11 de enero de 2012

Comienzo de la aventura


Un nuevo blog ha dado a luz hoy, y espero que tenga recorrido en el poco tiempo libre que tengo. En este pequeño espacio espero plasmar los avances que realizo en el estudio sobre desarrollo de aplicaciones móviles sobre Android y espero que me cunda para iPhone, aunque esto implicaría aprender aprender Objetive-C, cosa que no me tienta mucho.

Espero que los que por aquí recaléis, obtengáis ayuda en este doloroso mundo que es el de la informática.