Thursday, July 11, 2013

Push Notification Services with Android, GCM and Google App Engine (Part 2)

 

Introduction

In this part of the tutorial will implement server side implementation of our 3 main components. For this purpose we’ll use Google App Engine (GAE) to implement simple JSP web application as well GAE Datastore that is delivered with application. The entire project can be downloaded from here.

The GAE web app will have two functionalities:

  • to send message to the GCM which will then dispatch notification to all targeted devices
  • to store device tokens, once they are received from GCM

For messaging sending will have very simple form where we can enter some message and submit for delivery:

GAE

Figure01: Web application in Google App Engine

Setup

Step01: Visit Google App Engine web site and create a new application.

GAECreateApp

Figure02: Create GAE application

Step02: Install GAE plugin within your Eclipse. Visit here for guidelines.

Step03: Once you installed the plugin, in Eclipse go to File > New > Other and choose under Google section Web application project.

GAEEclipse01

Figure03: Select GAE project in Eclipse

Step04: Name your project and namespace and uncheck the Generate project sample code option.

GAEEclipse

Figure04: Create GAE project in Eclipse

Implementation

Our web application consists of two servlets (MainActivityServlet.java, StoreIdServlet.java)  and two JSP pages (main.jsp, storeid.jsp) respectively.

GAEEclipse02

Figure05: GAE project structure in Eclipse

The MainActivityServlet.java servlet handles HTTP requests that are coming from the main.jsp form (see Figure01) and is in charge of sending notification messages to the GCM service. The second functionality for storing device tokens is implemented within StoreIdServlet.java servlet and it also handles requests from storeid.jsp to store device tokens manually if necessary.

Following code is the implementation of the MainActivityServlet.java servlet:

package com.guestapp;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.MulticastResult;
import com.google.android.gcm.server.Sender;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;

public class MainActivityServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(MainActivityServlet.class.getName());
// API_KEY is sender_auth_token (server key previously generated in GCM)
private static final String API_KEY = "";
// Datastore is database where all device tokens get stored
private static DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();


// Handles HTTP GET request from the main.jsp
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("/main.jsp");
}

// Handles HTTP POST request - submit message from the main.jsp
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String txtInput = req.getParameter("txtInput");

// Instantiating sender for dispatching message to GCM
Sender sender = new Sender(API_KEY);
// Creating a message for GCM
Message message = new Message
.Builder()
.addData("message", txtInput)
.build();

ArrayList<String> devices = getAllRegIds();
if(!devices.isEmpty()){
// Sending multicast message to GCM specifying all targeting devices
MulticastResult result = sender.send(message, devices, 5);
log.info("Message posted: " + txtInput);
resp.sendRedirect("/main.jsp?message="+txtInput);
}else{
log.info("No devices registered.");
resp.sendRedirect("/main.jsp?message=warning-no-devices");
}
}

// Reads all previously stored device tokens from the database
private ArrayList<String> getAllRegIds(){
ArrayList<String> regIds = new ArrayList<String>();
Query gaeQuery = new Query("GCMDeviceIds");
PreparedQuery pq = datastore.prepare(gaeQuery);
for (Entity result : pq.asIterable()){
String id = (String) result.getProperty("regid");
regIds.add(id);
}

return regIds;
}
}


The implementation of main.jsp page looks like:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<html>
<body>
Message:
<% if (request.getParameter("message") != null) { %>
<%= request.getParameter("message")%>
<% } %>
<form action="/main" method="post">
<div><textarea name="txtInput" rows="3" cols="60"></textarea></div>
<div><input type="submit" value="Submit" /></div>
</form>
</body>
</html>


The implementation of second StoreIdServlet.java servlet is like following:


package com.guestapp;
import java.io.IOException;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

public class StoreIdServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(MainActivityServlet.class.getName());
// Datastore is database where all device tokens get stored
private static DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

// Handles HTTP GET request from the storeid.jsp
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("/storeid.jsp");
}

// Handles HTTP POST request - submit message from the storeid.jsp
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String txtRegId = req.getParameter("txtRegId");

// Creates device token entity and saves it in the database
Entity regId = new Entity("GCMDeviceIds",txtRegId);
regId.setProperty("regid", txtRegId);
if(!isReqIdExist(txtRegId)){
saveToDB(regId);
log.info("RegId inserted into DB: " + txtRegId);
}
}

// Save device token in the database
private void saveToDB(Entity regId){
datastore.put(regId);
}

// Checks if the device token already exist in the database
private boolean isReqIdExist(String regId){
Key keyRegId = KeyFactory.createKey("GCMDeviceIds", regId);
Entity entity = null;
try {
entity = datastore.get(keyRegId);
} catch (EntityNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(entity!=null){
return true;
}
return false;
}
}


The corresponding JSP page, storeid.jsp looks like:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<html>

<body>
Insert device Registration_id:
<form action="/storeid" method="post">
<div><textarea name="txtRegId" rows="3" cols="60"></textarea></div>
<div><input type="submit" value="Submit" /></div>
</form>
</body>
</html>


Deployment


Right click on the project and go to Google > App Engine Settings… Fill in the application id that goes before “.appspot.com” part.


GAEEclipse03


Figure06: GAE deployment configuration


Now go right click on the project Google > Deploy to App Engine and then click deploy.


Once your deployment is successfully finished you can go back to Google App Engine web site or check your application by visiting appid.appspot.com. Also, on Google App Engine web site you can check Datastore Viewer for preview your database records.


 


The remaining component of notification system is Android application. Check the implementation in the next blog.

15 comments:

  1. When I enter the message and hit the submit button.I get the Error: server Error-the server encountered an error and could not complete your request.

    ReplyDelete
    Replies
    1. Hi,

      Is it storeid.jsp or main.jsp?

      In a case of storeid.jsp I think I forgot a missing step of creating datastore collection. In your Google App Engine website go to Data > DataViewer and create a new datastore collection (table) with name "GCMDeviceIds" and column "regid".

      Sorry for inconvenience I'll add a missing instructions for that.

      Best regards.

      Delete
    2. Hi
      can you explain this step? I can't find the DataViewer option, may because Google is changing the developer console. Thanks for all.

      Delete
  2. When I hit the main.jsp then error is shown.

    ReplyDelete
  3. Log of the above error

    Uncaught exception from servlet
    com.google.android.gcm.server.InvalidRequestException: HTTP Status Code: 401

    at com.google.android.gcm.server.Sender.sendNoRetry(Sender.java:367)
    at com.google.android.gcm.server.Sender.send(Sender.java:261)
    at com.guestapp.MainActivityServlet.doPost(MainActivityServlet.java:58)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
    at com.google.apphosting.utils.servlet.ParseBlobUploadFilter.doFilter(ParseBlobUploadFilter.java:125)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.google.apphosting.runtime.jetty.SaveSessionFilter.doFilter(SaveSessionFilter.java:35)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.handle(AppVersionHandlerMap.java:266)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
    at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:923)
    at com.google.apphosting.runtime.jetty.RpcRequestParser.parseAvailable(RpcRequestParser.java:76)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
    at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:146)
    at com.google.apphosting.runtime.JavaRuntime$RequestRunnable.run(JavaRuntime.java:439)
    at com.google.tracing.TraceContext$TraceContextRunnable.runInContext(TraceContext.java:435)
    at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:442)
    at com.google.tracing.CurrentContext.runInContext(CurrentContext.java:186)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:306)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:298)
    at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:439)
    at com.google.apphosting.runtime.ThreadGroupPool$PoolEntry.run(ThreadGroupPool.java:251)
    at java.lang.Thread.run(Thread.java:722)

    ReplyDelete
    Replies
    1. Did you set your API_KEY in

      private static final String API_KEY = "";

      ?

      BR

      Delete
    2. Hello nbt,

      I took a second look at the application and I wasn't able to reproduce the same error.
      The code seems OK, but reading through the exception stack trace that you've sent me:

      at com.google.android.gcm.server.Sender.sendNoRetry(Sender.java:367)
      at com.google.android.gcm.server.Sender.send(Sender.java:261)
      at com.guestapp.MainActivityServlet.doPost(MainActivityServlet.java:58)

      I did noticed that your app is failing to send a message to GCM. This is mostly chance of wrong API_KEY you might enter.
      Please refer to my Part I blog post (http://smallsoftlab.blogspot.co.at/2013/07/push-notification-services-with-android.html) about necessary GCM credentials:

      "sender_auth_token - or API_KEY that is used on a server side in order to use GCM services"

      Hope this helps.

      Delete
  4. Hi,
    when i run this part i have 2 problems

    1) in the mainactivityservlet.java class i have the following problem, "the import com google.android.gcm.server cannot be resolved", i have tried to import gcm-server.jar as well as google play services lib, but it doesnt work.

    2) secondly, it shows that i cannot compile the 2 jsp files...

    please help me out

    ReplyDelete
    Replies
    1. Hi Jeet,

      Check your reference libraries in Java Build Path > Libraries.
      In my build configuration I reference following libraries:

      - gcm-server.jar
      - json-simple-1.1.1.jar
      - App Engine SDK (App Engine 1.7.5)
      - GWT SDK (GWT - 2.5.1)
      - JRE System Library (jdk1.7.0)

      Hope it helps,
      Vladimir

      Delete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Hi, when i try to obtain the GCM Registration ID with your code, i get an empty value and an error on the server side.
    Could you let me know what is happening?

    ReplyDelete
    Replies
    1. Hi vivek,

      if it is this part:

      Entity regId = new Entity("GCMDeviceIds",txtRegId);
      regId.setProperty("regid", txtRegId);

      I think the issue is that you didn't create actual database collection (table). I just realized I missed to describe this step.
      Check on your GAE instance's dashboard DataViewer and create a new datastore collection with name "GCMDeviceIds" and column "regid".

      Delete
    2. Also check https://code.google.com/apis/console/ and if you actually allowed datastore api usage for your GAE application.

      Hope this helps,
      Vladimir

      Delete
  7. Hello Vladimir Marinkovic


    I following you steps finished all,but after i send messages on website:which is mention "Message: warning-no-devices"could you tell me how to check the problem,please.i alredy set my project number and API key.

    ReplyDelete