1 Google Cloud EndPoints (Parte II) 1. Aplicaciones android con Google EndPoints. 1.1. Resumen en la creación de un cliente y un backend con Google endPoints. 1.2. Añadiendo autentificación a nuestra aplicación. 1.3 Modificaciones en el cliente Android para soportar solicitudes REST con autentificación. 1.4 Seleccionando la cuenta. 1.5 Excepciones. 1.6 Testeando. 1.7 Añadiendo Google Cloud Messaging. 1.8 Código fuente 2 Aplicaciones android con Google EndPoints. En la primera parte vimos cómo podíamos crear nuestra API REST y la gestión de la persistencia utilizando anotaciones de los EndPoints y de JDO o JPA. Mediante el plugin de eclipse para GAE desde una clase POJO se creaba su clase con los principales verbos http. Ahora, a través de un ejemplo sencillo, volveremos analizar la parte backend y nos centraremos en cómo crear una aplicación android que haga uso de la librería cliente. En este apartado, vamos a ver ver como crear toda una aplicación (backend y librería cliente) a través de Google EndPoints y GAE y cómo prácticamente no tenemos necesidad de escribir código. Creamos un nuevo proyecto Android. Seleccionamos en “Compile With” alguna versión de Google APIs superior a la 15. Creamos el proyecto backend. Va a generarnos automáticamente el proyecto CloudLibreria­AppEngine y también va a añadir código en nuestro proyecto Android. 3 4 Ahora tenemos dos proyectos, nuestro proyecto Android y el proyecto GAE: nombre_proyecto_android­AppEngine. En el proyecto AppEngine creamos nuestras clases Entity. Para este ejemplo utilizamos anotación JPA. 5 package com.jtristan.librosendpoint.entidades; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable public class Libro { @PrimaryKey @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY) private Long id; @Persistent private String titulo; @Persistent private String autor; @Persistent private int puntuacion; //­­­getters and setters­­// Las dos anotaciones que usamos son muy simples, con @PersistenceCapable indicamos que la clase es una entidad que debe persistirse. Con @PrimaryKey indicamos que se trata del campo clave. 6 En este paso se crea la clase LibroEndPoint.java que es la responsable de gestionar el datastore de nuestra entidad mediante los métodos HTTP. 7 Si modificamos nuestra clase POJO, porque añadimos, modificamos o eliminamos alguna de los atributos tendríamos que volver a realizar el mismo paso para actualizar nuestra clase EndPoint. Una vez que tenemos creada nuestra API REST generamos la estructura de comunicación con la misma en nuestro proyecto cliente. En nuestro proyecto Android se ha creado entre otras clases, Libroendpoint.java. Esta clase se encarga de hacer de forma transparente para nosotros todas las solicitudes a los métodos del Endpoint, getLibro(Long id); listLibro(); insertLibro(Libro libro), updateLibro(Libro libro), removeLibro(Long id), etc Tenemos también la clase Entity Libro que es la que vamos a utilizar para mandar y recuperar los datos a través de los EndPoints. También se crean clases Message para poder gestionar el Google Cloud Messaging. En el proyecto se ha creado también un fichero index.html con la lógica para mostrar todos los dispositivos que se han registrado para recibir mensajes push y un campo para introducir el texto que deseamos mandar. 8 Para probarlo nos creamos en el proyecto Android una clase que extiende de AsyncTask ya que no podemos bloquear la UI y desconocemos cuál va a ser el tiempo de respuesta de los Endpoints. public class EndpointsTask extends AsyncTask<Context, Integer, Long> { protected Long doInBackground(Context... contexts) { Libroendpoint.Builder endpointBuilder = new Libroendpoint.Builder( AndroidHttp.newCompatibleTransport(), new JacksonFactory(), new HttpRequestInitializer() { public void initialize(HttpRequest httpRequest) { } }); Libroendpoint endpoint = CloudEndpointUtils.updateBuilder( endpointBuilder).build(); try { Libro libro = new Libro(); libro.setTitulo("El juego de Ripper"); libro.setAutor("Isabel Allende"); libro.setPuntuacion(8); Libro result = endpoint.insertLibro(libro).execute(); } catch (IOException e) { e.printStackTrace(); } return (long) 0; } } Idénticamente si quisiesemos buscar todos los libros: CollectionResponseLibro collectionLibro = endpoint.listLibro().execute(); List<Libro> libros = collectionLibro.getItems(); Utilizamos el Builder para crear la conexión con nuestro EndPoint. Le pasamos una nueva instancia de transporte HTTP, una factoría para la creación de documentos JSON y un inicializador de la solicitud HTTP (httpRequestInitializer()). 9 A continuación instanciamos la clase endPoint pasando el builder. Asignamos los valores a nuestra Entidad y mandamos una solicitud POST para grabar los datos mediante el método insertLibro(libro).execute();. En el método onCreate del Activity llamamos al AsyncTask: new EndpointsTask().execute(getApplicationContext()); Si queremos ejecutar el proyecto AppEngine en local debemos cambiar la variable LOCAL_ANDROID_RUN a true en la clase CloudEndPointUtils. Ejecutamos el proyecto CloudLibreria­AppEngine mediante Run­>Web Application y también la app Android con el emulador o en un dispositivo. Una vez ejecutado la aplicación Android podemos ir a al explorado de APIs para ver si se guardado bien el registro: http://localhost:8888/_ah/api/explorer. 10 11 Resumen en la creación de un cliente y un backend con Google endPoints. 1.­ Crear el proyecto en el Google cloud 2.­ Crear el proyecto Android. Compilación: Google APIs versión mayor o igual a 15. 3.­ Crear con el GEP el proyecto backend (Generate App Engine Backend). 4.­ En el proyecto GAE crear las clases Entity. 5.­ Generar en el proyecto GAE los endpoints con el GEP (Generate Cloud Endpoint Class) 6.­ Generar la estructura de comunicación del cliente REST con GEP (Generate Cloud Endpoint Client Library) 7.­ Ejemplo de código para comunicarnos en el cliente Android public class EndpointsTask extends AsyncTask<Context, Integer, Long> { protected Long doInBackground(Context... contexts) { Libroendpoint.Builder endpointBuilder = new Libroendpoint.Builder( AndroidHttp.newCompatibleTransport(), new JacksonFactory(), new HttpRequestInitializer() { public void initialize(HttpRequest httpRequest) { } }); Libroendpoint endpoint = CloudEndpointUtils.updateBuilder( endpointBuilder).build(); try { Libro libro = new Libro(); libro.setTitulo("El juego de Ripper"); libro.setAutor("Isabel Allende"); libro.setPuntuacion(8); Libro result = endpoint.insertLibro(libro).execute(); } catch (IOException e) { e.printStackTrace(); } return (long) 0; } } 12 Añadiendo autentificación a nuestra aplicación. Cloud Endpoints nos permite trabajar con autenticación OAuth 2.0 de forma que podamos conocer la identidad de los usuarios que van a usar la API Rest. Requisitos: Necesitamos añadir la librería google­play­services.jar. Para ello, primero tenemos que importar la librería a eclipse y después, desde las propiedades del proyecto Android añadimos la librería. En el proyecto android tenemos que incluir dentro del atributo “<application>” del AndroidManifest.xml el atritubo <meta­data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />. En la consola de Google cloud registramos un cliente Android. Para ello vamos a Apis y autenticación ­> Credenciales ­> Crear Id Nuevo de Cliente ­> Aplicación instalada ­>Android. La 13 clave se genera mediante una huella digital de certificado SHA1 y el nombre del paquete de la aplicación, separados por un punto y coma. La huella digital de certificado la podemos encontrar en eclipse en “Preferencias” ­> “Android” ­> “Build” Vamos a necesitar el web ID de aplicación. Para ello, siguiendo los mismos pasos seleccionamos “Aplicación instalada” e introducimos la ruta de nuestra aplicación. Si la estamos probando en local: http://localhost:8888 y si está subida al App Engine http://proyecto_id.appspot.com. NOTA: Cada aplicación que desplegamos en el App Engine es identificada por un único Id de aplicación y versión. Mediante la versión podemos distinguir entre las distintas releases de una aplicación, ya que podemos tener varias versiones de la misma aplicación en el App Engine. Con esto vamos a la clase LibroEndPoint del proyecto AppEngine y en la anotación @Api o la anotación @ApiMethod si queremos especificar la seguridad a nivel de ciertos métodos, 14 añadimos el atributo clientIds y el atributo audience. El primero lo usamos para registrar el id de Cliente Android y el segundo para el Id de aplicación App Engine. clientIds: se utiliza cuando nuestra API usa autentificación. Se indican una lista con todos los Ids de clientes permitidos para solicitar tokens (pueden ser clientes web, Android o iOS). Por ejemplo: clientIds = {"1­web­apps.apps.googleusercontent.com", "2­android­apps.apps.googleusercontent.com"} Si usamos autentificación y queremos probar la API a través del Google API explorer (http://localhost:8888/_ah/api/explorer) tenemos que añadir el id de cliente: com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID. audiences: es un atributo obligatorio cuando usamos clienteIds para Android (Corresponde con el WEB Id que podemos generar desde la consola de Google). A cada uno de los métodos tenemos que añadir el parámetro com.google.appengine.api.users.User que va a contener información sobre la cuenta del usuario autenticado. App Engine se encarga de llenar este parámetro automáticamente. @Api(name = "libroendpoint", namespace = @ApiNamespace(ownerDomain = "jtristan.com", ownerName = "jtristan.com", packagePath = "cloudlibreria.entities"), clientIds={"128678154032­9vn*************************g0q.apps.googleusercontent.com", com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID}, audiences={"128****************************3hl61q60qu.apps.googleusercontent.com"}) public class LibroEndpoint { @ApiMethod(name = "listLibro") public CollectionResponse<Libro> listLibro( @Nullable @Named("cursor") String cursorString, @Nullable @Named("limit") Integer limit, User user) { Si queremos desplegar la aplicación en GAE para poder probar la autenticación con usuarios reales tenemos que añadir en el xml appengine­web.xml el Id de la aplicación en el atributo <application>. Después desde el plugin de Eclipse hacemos la importación. 15 NOTA: Recuerda que aunque podemos probar la autenticación en local, siempre recibiremos como cuenta example@example.com. Si queremos utilizar nuestras propias cuentas tendremos que desplegar la aplicación en GAE. Modificaciones en el cliente Android para soportar solicitudes REST con autentificación. En nuestro cliente android una vez hayamos solicitado o recuperado las credenciales vamos a pasárselas al builder del endpoint. Para ello, usamos la clase GoogleAccountCredential. GoogleAccountCredential credenciales = GoogleAccountCredential.usingAudience(this, "server:client_id:"+WEB_CLIENT_ID); credenciales.setSelectedAccountName(preferencias.getString(PREF_NOMBRE_CUENTA, "")); Siendo this el contexto de Android y el segundo parámetro la audiencia. La audiencia se compone del literal “server:client_id:” más id de cliente WEB que hemos obtenido en la consola del google cloud. En la credencial indicamos el nombre de cuenta qué va a autentificarse. En este caso, tenemos almacenado el nombre de cuenta en las preferencias. Ver el apartado siguiente para ver cómo podemos gestionar nuestras cuentas online. 16 Con las credenciales completas, ya podemos pasarlas al builder del Endpoint. Libroendpoint.Builder endpointBuilder = new Libroendpoint.Builder( AndroidHttp.newCompatibleTransport(), new JacksonFactory(), credenciales); Ahora, la cuenta de usuario es recibida como parámetro de tipo User en nuestro appEngine endpoint. Seleccionando la cuenta. Si trabajamos con autenticación tendremos que implementar el mecanismo para solicitar la cuenta con la que vamos a autentificarnos. En Android esto es muy sencillo gracias a la clase AccountManager. Mediante esta clase tenemos acceso al registro centralizado de cuentas de usuario online. Para que el usuario seleccione la cuenta que quiere usar podemos usar el método newChooseAccountIntent() del AccountManager. Este método muestra una actividad con todas las cuentas activas del usuario. Una vez que el usuario selecciona la cuenta devuelve a nuestra actividad el resultado. 17 18 private void seleccionarCuenta() { startActivityForResult(credenciales.newChooseAccountIntent(), RECOGER_SOLICITUD_CUENTA); } Recuperamos en el método onActivityResult la cuenta seleccionada. Si se ha seleccionado una cuenta la guardamos en preferencias para usarla en posteriores uso de la aplicación. Esto lo hacemos en el método setNombreCuenta. 19 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RECOGER_SOLICITUD_CUENTA: if (data != null && data.getExtras() != null) { Bundle bundle = data.getExtras(); String nombreCuenta = bundle.getString(AccountManager.KEY_ACCOUNT_NAME); if (nombreCuenta != null) { setNombreCuenta(nombreCuenta); } } break; } } private void setNombreCuenta(String nombreCuenta) { SharedPreferences.Editor editor = preferencias.edit(); editor.putString(PREF_NOMBRE_CUENTA, nombreCuenta); editor.commit(); credenciales.setSelectedAccountName(nombreCuenta); this.nombreCuenta = nombreCuenta; } Este método almacena en preferencias la cuenta seleccionada y a su vez la almacena en la variable credenciales. Credenciales es una instancia de la clase GoogleAccountCredential que es la que necesitamos pasar al Builder de nuestro EndPoint con la información del usuario que va a solicitar el acceso al endPoint. En nuestra clase onCreate de la activity podemos recuperar el nombre de cuenta con el que deseamos logueado. Si nunca lo hemos hecho llamaríamos al picker del AccountManager. credenciales = GoogleAccountCredential.usingAudience(this, "server:cliente_id"+CLIENTE_ID); preferencias = this.getPreferences(Context.MODE_PRIVATE); if (preferencias.getString(PREF_NOMBRE_CUENTA, "")==""){ seleccionarCuenta(); } 20 Naturalmente, deberíamos desarrollar en la configuración de la aplicación o como una acción del actionbar la capacidad de poder cambiar de cuenta. Excepciones. Puede ser adecuado, utilizar los códigos de estatus HTTP para indicar la causa por la que una solicitud REST falle o tenga éxito. Estos códigos son estandar con lo cuál son fácilmente entendibles por cualquier aplicación. Google endpoints nos da acceso a una serie de excepciones relacionadas directamente con códigos de estatus HTTP específicos: com.google.api.server.spi.response.BadRequestException HTTP 400 com.google.api.server.spi.response.UnauthorizedException HTTP 401 com.google.api.server.spi.response.ForbiddenException HTTP 403 com.google.api.server.spi.response.NotFoundException HTTP 404 com.google.api.server.spi.response.ConflictException HTTP 409 com.google.api.server.spi.response.InternalServerErrorException HTTP 500 com.google.api.server.spi.response.ServiceUnavailableException HTTP 503 Cuando se esté realizando una búsqueda de una entidad, si esta no es encontrada podremos lanzar la excepción NotFoundException, la cual nos devuelve un código HTTP 404. El error 404 nos indica que se ha podido establecer la conexión con el servidor pero que la entidad solicitada no existe. Si estamos solicitando un método HTTP que necesita de autentificación y no la hemos provisto podemos utilizar el código HTTP 401 que se lanzaría con la excepción UnauthorizedException. @ApiMethod(name = "getUsuario") public Usuario getUsuario(@Named("id") Long id) throws NotFoundException{ PersistenceManager mgr = getPersistenceManager(); Usuario usuario = null; try { usuario = mgr.getObjectById(Usuario.class, id); }catch(Exception e){ if (usuario==null){ String mensaje = String.format("No existe la entidad con el id %s", id); throw new NotFoundException(mensaje); 21 } } finally { mgr.close(); } return usuario; } En el ejemplo podemos ver como si se ha solicitado un id de Usuario que no existe, se devuelve una excepción NotFoundException con el siguiente mensaje: 404 Not Found { "code" : 404, "errors" : [ { "domain" : "global", "message" : "No existe la entidad con el id 25", "reason" : "notFound" } ], "message" : "No existe la entidad con el id 25" } En este caso, comprobamos en un método que necesita autentificación que se esta se haya realizado, de lo contrario, devolvemos un código HTTP 401. @ApiMethod(name = "listReserva") public CollectionResponse<Reserva> listReserva( @Nullable @Named("cursor") String cursorString, @Nullable @Named("limit") Integer limit, User usuario) throws ServiceException{ PersistenceManager mgr = null; Cursor cursor = null; List<Reserva> execute = null; if (usuario==null){ String mensaje = "Se trata de un método autentificado"; throw new UnauthorizedException(mensaje); } … 22 401 Unauthorized { "error": { "message": "Se trata de un método autentificado", "code": 401, "errors": [ { "domain": "global", "reason": "required", "message": "Se trata de un método autentificado" } ] } } Podremos crear nuestras propias excepciones extendiendo de la clase com.google.api.server.spi.ServiceException Testeando. La autentificación con OAuth2 todavía no funciona desde el emulador de Android. Si queremos testear la autenticación desde el explorador de APIs Google nos provee un usuario “dump” para las pruebas que es: example@example.com. Añadiendo Google Cloud Messaging. Mediante GCM podemos enviar notificaciones a todos los dispositivos registrados. Para añadir esta funcionalidad es necesario activar en el la consola Google APIs la mensajería. El siguiente paso es crear un “server key” Mediante esta clave nos aseguramos una comunicación segura. 23 Para ello, dentro de la consola, vamos a “Credenciales”, “Acceso a API pública” y “Crear clave nueva”, “Clave de servidor”. No introducimos ningún valor, simplemente la generamos. En nuestro proyecto AppEngine tenemos que establecer la clave de servidor. La constante es API_KEY y está en la clase MessageEndPoint.java. private static final String API_KEY = "AIzaS***********************G0d0w"; Vamos a actualizar el proyecto Android para poder trabajar con GCM. En la clase GCMIntentService establecemos el número de proyecto. protected static final String PROJECT_NUMBER = ""; En esta clase tenemos el método onMessage() donde debemos indicar qué acciones vamos a realizar cuando se reciba una notificación. Por defecto manda un intent que abre la actividad RegisterActivity y muestra el mensaje. Ahora ya sólo nos queda pendiente registrar el dispositivo para que pueda recibir notificaciones. El registro se hace en el método register(Context context). Deberíamos llamar a este método desde nuestro onCreate de la actividad principal para asegurarnos que el dispositivo sea siempre registrado. GCMIntentService.register(this); Una vez que se ha registrado el dispositivo podemos ver que el registro ha sido correcto desde la url de la aplicación: https://id_aplicación.appspot.com/. 24 Código fuente. Puedes encontrar en Github y en el zip que se acompaña el documento el código de dos proyectos básicos con la parte backend y una app android que hace uso del API generada. App android : https://github.com/josetristan/LibrosEndPoint Proyecto backend: https://github.com/josetristan/LibrosEndPoint­AppEngine 25