Barbados por la Pista

Screen Shot 2014-10-24 at 6.43.59 PM

A una semana de afrontar mi mayor reto en aguas abiertas, quería repasar todo lo que ha pasado en estos dos años de entrenamiento y por que es que me voy a mandar a esta competencia en específico.

Como ya había comentado en otro post, empecé a nadar para mejorar mi salud pero sin las lesiones del atletismo. Si bien mi idea era volver a correr, no me esperaba “enamorarme” de la natación.

Desde entonces me he propuesto retos, desde los más fantasiosos hasta los mas triviales. Primero empezando a alcanzar distancias en el entrenamiento hasta participar en mi primera competencia. Otros retos tan alocados como salir de Puntarenas y llegar a Paquera o inclusive, por que no, pensar en algún día hacer el Canal de la Mancha (De Inglaterra a Francia).

Me suscribí a H2Open (http://www.h2openmagazine.com/), una revista especializada en aguas abiertas. Ellos son patrocinadores de una competencia muy reciente en Barbados. En realidad es un festival, que tiene como plato fuerte las competencias de aguas abiertas, pero que es un evento de todo el día con diferentes actividades.

Una de mis metas antes de llegar a los 40 era participar a una competencia fuera de Costa Rica, en aguas abiertas y atletismo. Aunque siempre tenía en mente ir a Estados Unidos, al ver el anuncio en la revista, fue como amor a primera vista. Primero por que es una competencia en el Caribe, lo que hace que la temperatura del agua sea similar a la de aquí (en comparacion a EUA, donde es más frío) y segundo por que al hacer números, es una competencia que se puede hacer con un presupuesto ajustado (la inscripción cuesta $15 por ejemplo).

Llevo entrenando ya casi 2 años natación, con bastante progreso en condición y en técnica. He recibido el apoyo de mi familia y amigos, pero de manera muy en especial, del Equipo de Natación Guppys (G-Swim) y de la academia, pues me permitieron hacer doble sesión los Jueves, donde hemos trabajado la distancia y la estrategia. Por ejemplo ayer vimos la parte de la hidratación, ya que no va a haber hidratación durante el trayecto, por lo que cada nadador tiene que llevarlo.

Faltando pocos día, aumenta la emoción y el nerviosismo, pero igualmente las muestras de apoyo de los que me rodean.

Les prometo muchas fotos y dar todo de mí en la competencia.

Problemas Guava + Proguard

Para los que vieron los post sobre usar Google App Engine Endpoints para un App en Android (sí, yo sé que debo la parte final) a la hora de generar un APK firmado, puede que se topen con una excepción con Proguard. Esto pasa por culpa de una de las librerías, en este caso Guava.

Los warnings que tira Proguard son respecto a dos clases: sun.misc.Unsafe y com.google.common.collect.MinMaxPriorityQueue

Esto se arregla agregando las siguientes directivas en su archivo de proguard:

-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue

Parte 2: Android + AppEngine + Cloud Datastore

En la parte 1 se preparó una aplicación Android básica. Además se generó una aplicación web como backend con un endpoint para usuario. En esta segunda parte se terminará el endpoint de salvar notas (de manera muy breve) y se pasará a integra la aplicación Android con los dos endpoints.

La Parte 1 esta aqui.

Endpoint de Notas

Siguiendo los pasos de la creación del endpoint User, se creará el endpoint para salvar notas. Así, se creara la clase Note

public class Note {

    private Long mId;
    private String mContent;
    private Long mUserId;
    private Date mDate;

    public Long getId() {
        return mId;
    }

    public void setId(Long id) {
        mId = id;
    }

    public String getContent() {
        return mContent;
    }

    public void setContent(String content) {
        mContent = content;
    }

    public Long getUserId() {
        return mUserId;
    }

    public void setUserId(Long userId) {
        mUserId = userId;
    }

    public Date getDate() {
        return mDate;
    }

    public void setDate(Date date) {
        mDate = date;
    }
}

Además se creará la clase NoteEndpoint que recibirá las solicitudes de manipulación de notas, similar a usuario, obtener una nota, insertar nota y listar notas.

/** An endpoint class we are exposing */
@Api(name = "noteEndpoint", version = "v1", namespace = @ApiNamespace(ownerDomain = "savenotes.backend.fr4gus.com", ownerName = "savenotes.backend.fr4gus.com", packagePath=""))
public class NoteEndpoint {

    // TODO in memory implementation to test endpoints
    static Map<Long, Note> mLocalNotes = new HashMap<Long, Note>();
    static long mLastId = 0;

    private static final Logger LOG = Logger.getLogger(NoteEndpoint.class.getName());

    /**
     * This method gets the <code>Note</code> object associated with the specified <code>id</code>.
     * @param id The id of the object to be returned.
     * @return The <code>Note</code> associated with <code>id</code>.
     */
    @ApiMethod(name = "getNote")
    public Note getNote(@Named("id") Long id) {
        LOG.info("Calling getNote method");
        return mLocalNotes.get(id);
    }

    /**
     * This inserts a new <code>Note</code> object.
     * @param note The object to be added.
     * @return The object to be added.
     */
    @ApiMethod(name = "insertNote")
    public Note insertNote(Note note) {
        LOG.info("Calling insertNote method");

        synchronized (mLocalNotes) {
            mLastId++;
            mLocalNotes.put(mLastId,note);
            note.setId(mLastId);
        }

        return note;
    }

    /**
     * This returns all stored <code>Note</code> objects stored in the service
     * @return a list of all notes
     */
    @ApiMethod(name = "listNotes", path = "note/list")
    public List<Note> listNotes() {
        ArrayList<Note> notes = new ArrayList<Note>();
        notes.addAll(mLocalNotes.values());
        return notes;
    }
}

Se actualiza web.xml para agregar la clase de NoteEnpoint

   <servlet>
        <servlet-name>SystemServiceServlet</servlet-name>
        <servlet-class>com.google.api.server.spi.SystemServiceServlet</servlet-class>
        <init-param>
            <param-name>services</param-name>
            <param-value>com.fr4gus.backend.savenotes.UserEndpoint, com.fr4gus.backend.savenotes.NoteEndpoint</param-value>
        </init-param>
    </servlet>

Integración Android + GAE

Antes de poder utilizar las clases de la librería cliente para los endpoints. Es necesario primero agregar dependencias a nuestro proyecto. En este link, se muestra los pasos necesarios para agregar las dependencias.

También es posible obtener los pasos, al crear las librerías cliente, ya que vienen con una documentación. Aquí tienen un resumen de los cambios:

En build.gradle

repositories {
    mavenCentral()
    mavenLocal() // Agregar el repo local de maven
}

dependencies {
    compile ([group: 'com.google.api-client', name: 'google-api-client-android', version: '1.18.0-rc'])
    // Si quieren usar Gson pongan este
    compile ([group: 'com.google.api-client', name: 'google-http-client-gson', version: '1.18.0-rc'])

    // Si quieren usar Jackson pongan este
    compile ([group: 'com.google.api-client', name: 'google-http-client-jackson2', version: '1.18.0-rc'])

}

Si no quieren usar ni Gson ni Jackson, pueden importar AndroidJsonFactory si su proyecto tienen un API level mínimo de 11, importando: import com.google.api.client.extensions.android.json.AndroidJsonFactory;

Se recomienda ver la documentación, ya que esta cuenta con información de como agregar esas dependencias de manera más óptima, eliminando ciertos paquetes que no se ocupan.

Se creó una clase Application para poder guardar el usuario actual (usando el patrón Singleton)

public class SaveNotesApplication extends Application {
    static SaveNotesApplication sInstance;

    User mCurrentUser;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
    }

    public static SaveNotesApplication getInstance() {
        return sInstance;
    }

    public User getCurrentUser() {
        return mCurrentUser;
    }

    public void setCurrentUser(User currentUser) {
        mCurrentUser = currentUser;
    }
}

Con esto podemos acceder al usuario actual desde cualquier parte de la aplicación. Importante recordar que se ocupa actualizar el archivo AndroidManifest.xml en el tag <application> y agregar el atributo android:name con el valor .SaveNotesApplication.

Antes de poder enviar la nota se ocupa obtener el usuario primero. Para ello se creará una actividad padre, para que todas las actividades que hereden de ella puedan acceder al usuario o en su defecto, solicitarlo al servicio getUser si ya está registrado o insertUser si no ha sido registrado aún.

public class BaseActivity extends ActionBarActivity {
    public static final String TAG = "SaveNotes";

    public static final String APP_NAME = SaveNotesApplication.getInstance().getString(R.string.app_name);
    public static final String TEST_ROOT_URL = "http://192.168.1.140:8080/_ah/api";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // DeviceUtil.getDeviceID simplemente llama a Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID) 
        final String deviceId = DeviceUtil.getDeviceId(this);

        User currentUser = SaveNotesApplication.getInstance().getCurrentUser();
        if (currentUser == null) {
            new AsyncTask<String,Void,Boolean>(){
                @Override
                protected Boolean doInBackground(String... params) {
                    UserEndpoint.Builder userBuilder = new UserEndpoint.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(),null);
                    userBuilder.setApplicationName(APP_NAME);
                    userBuilder.setRootUrl(TEST_ROOT_URL);
                    userBuilder..setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
                        @Override
                        public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest) throws IOException {
                            abstractGoogleClientRequest.setDisableGZipContent(true);
                        }
                    });
                    UserEndpoint userService = userBuilder.build();

                    try {
                        User user = userService.findUser(deviceId).execute();
                        SaveNotesApplication.getInstance().setCurrentUser(user);
                    } catch (IOException e) {
                        Log.e(TAG,e.getMessage(),e);
                        return false;
                    }

                    return true;
                }
            }.execute(deviceId);
        }
    }
}

En la línea 18 se nota la utilización del patrón Builder, con el cual se “configura” la instancia para acceder al endpoint de usuario.

En la línea 19 se establece el nombre de la aplicación. Este valor va como una cabecera en el request http, y permite mantener estadisticas de que tipo de cliente está utilizando los endpoints. Esto es importante cuando se tiene una version web u en otra plataforma como iOS del mismo cliente.

En la línea 20 se configura, para efectos de pruebas locales, la dirección del equipo donde se correrá una version local del servicio de App Engine. Esto es útil para las pruebas locales, pues por defecto intentará contactar a los servicios hospedados en la infraestructura de Google. Es de suma importancia aclarar que por defecto, el servidor se levanta utilizando “localhost” (127.0.0.1) por lo que solo escuchará request del mismo loop. Como vamos a querer acceder al servicio local desde otros equipos desde la misma red, es necesario modificar el build.gradle del módulo de backend para que escuche solicitudes de otros equipos. Esto se hace de manera sencilla agregando un valor nuevo al enclosure “appengine”. Se debe agregar httpAddress = ‘0.0.0.0’ por lo que el enclosure se verá similar a este:

appengine {
  httpAddress = '0.0.0.0'
  downloadSdk = true
  appcfg {
    oauth2 = true
  }
  endpoints {
    getClientLibsOnBuild = true
    getDiscoveryDocsOnBuild = true
  }
}

De la línea 21 a la 26, solo para pruebas locales, se configura el GoogleClientRequest, para deshabilitar la compresión de las solicitudes usando GZip. Esto por que la instancia local no maneja GZip y si ejecutan el cliente sin eso, les va a dar un EOFException en el servidor.

Si notaron, hay un método nuevo llamado UserEndpoint.findUser(String s). Este metodo se agregó para poder buscar el usuario con el deviceId, y así ver si el dispositivo está registrado. Si no está registrado, lo registra inmediatamente, ahorrando un llamado extra al endpoint.

Ya con el usuario disponible, se guarda en la clase Application y se puede continuar con el flujo normal. Si el usuario ya se encontraba asignado en la aplicación, simplemente no se llama al servicio.

A la clase NoteActivity hay que actualizarle el método OnClickListener para que llame al servicio de insertar notas. Nótese que se configura el Builder del endpoint de manera similar al de Usuario.

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.action_save) {
            String noteContent = mNoteContent.getText().toString();

            // Do nothing if content is empty
            if (TextUtils.isEmpty(noteContent))
                return;

            new AsyncTask<String, Void, Boolean>() {
                @Override
                protected Boolean doInBackground(String... params) {
                    String content = params[0];

                    NoteEndpoint.Builder noteBuilder = new NoteEndpoint.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null);

                    noteBuilder.setApplicationName(APP_NAME);

                    //TODO Use only for local test. Remove for production
                    noteBuilder.setRootUrl(TEST_ROOT_URL);
                    noteBuilder.setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
                        @Override
                        public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest) throws IOException {
                            abstractGoogleClientRequest.setDisableGZipContent(true);
                        }
                    });

                    NoteEndpoint noteService = noteBuilder.build();

                    Note note = new Note();
                    note.setContent(content);

                    User currentUser = SaveNotesApplication.getInstance().getCurrentUser();
                    note.setUserId(currentUser.getId());
                    try {
                        noteService.insertNote(note).execute();
                    } catch (IOException e) {
                        Log.e(TAG, e.getMessage(), e);
                        return false;
                    }
                    return true;
                }

                @Override
                protected void onPostExecute(Boolean result) {
                    if (result){
                        mNoteContent.setText("");
                    } else {
                        Toast.makeText(NotesActivity.this, R.string.error_send_note, Toast.LENGTH_SHORT).show();
                    }
                }
            }.execute(noteContent);

        }
    }

Así de simple se llaman a los endpoints de un API implementado con Google App Engine. Simplemente se utiliza el Builder para crear la instancia del cliete del endpoint y luego se llama al método, pasándole los parámetros necesarios y luego llamando al método execute. Esto último se debe hacer en un hilo aparte al del UI por que lo bloquearía, razón por la cual en este ejemplo se utilzó un AsyncTask.

Siguientes pasos

Si bien con esto se completaría la integración de una aplicación Android con un API implementado en Google App Engine, creo conveniente explicar en la tercera y última parte de éste tutorial, como utilizar Objectify para poder guardar los datos en el Datastore. Principalmente por que me parece interesante la librería que facilita enormemente el acceso al Datastore, y así cerrar el ejemplo con algo funcional.

Pueden acceder al código fuente del ejemplo completo en Github: https://github.com/fr4gus/SaveNotes