Thursday, 15 November 2012

GDG DevFest KL 2012 Code Lab - Android + App Engine + GCM

This is the step-by-step guide to the Code Lab for GDG DevFest KL 2012 to build a simpler version of the Android application "Info Trafik" with an App Engine server.

1. Requirements:
  • A Google account to access Google API Console.
  • Eclipse
  • Google Plugin for Eclipse (Google App Engine SDK, Android ADT)
  • Android SDK (requires Android SDK Tools, Android SDK Platform-tools, SDK Platform (API 16), Android Support Library, Google Cloud Messaging for Android Library)

Setup Google API Access
2. Open Google API Console
3. If you haven't created an API project yet, click the "Create Project …"
If you have created API projects before, choose Other projects > Create
4. Enter a name and click Create Project
5. Copy down the SENDER_ID from the url (e.g. https://code.google.com/apis/console/?pli=1#project:XXXXXXXXXXXX:services where XXXXXXXXXXXX is the SENDER_ID)
6. On the Services page, enable "Google Cloud Messaging for Android"
7. On the API Access page, select "Create new Server key..." and proceed with Create
8. Under the Key for server apps, copy down the API key

Setup Google App Engine
9. Open Google App Engine Adminstration Console 
10. Select "Create Application". For 1st time user, you need to verify the account by receiving a text message
11. Enter an Application Identifier & Application Title and select "Create"

Build Google App Engine Application
12. Start Eclipse and sign in with your Google Account (bottom right corner)
13. Select New Web Application Project
14. Enter the Project name "Codelab" & Package "my.devfestkl.codelab", untick Use Google Web Toolkit and click Finish
15. Open CodelabServlet.java in src/my.devfestkl.codelab
16. Add these imports
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import com.google.appengine.labs.repackaged.org.json.JSONArray;
import com.google.appengine.labs.repackaged.org.json.JSONException;
import com.google.appengine.labs.repackaged.org.json.JSONObject;
replace
 
resp.setContentType("text/plain");
resp.getWriter().println("Hello, world");
with
String last_id = "0";
// retrieve the Twitter search last ID 

URL url = new URL("http://search.twitter.com/search.json?q=from:LLMInfotrafik&since_id=" + last_id);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()), 8);
StringBuilder builder = new StringBuilder();
String line = "";
while ((line = reader.readLine()) != null) {
    builder.append(line);
}
try {
    JSONObject jObject = new JSONObject(builder.toString());
    JSONArray jResults = jObject.getJSONArray("results");
    if (jResults.length() != 0) {
     // retrieve all GCM registration ID
      for (int i = 0; i < jResults.length(); i++) {
      JSONObject jResult = jResults.getJSONObject(i);
      // save the new Twitter search last ID
      resp.getWriter().append(jResult.getString("text") + "\r\n");
            // send the GCM message
     }
    } else {
     resp.getWriter().append("No new tweet\r\n");
    }
} catch (JSONException e) {
    e.printStackTrace();
} 
(Optional) change the LLMInfotrafik to another Twitter user
17. Select Deploy to App Engine
18. Click on App Engine project settings...
19. Enter the Application Identifier from Step 11 to Application ID, click OK and proceed to Deploy
20. Open the link http://<Application ID>.appspot.com in a browser and click Codelab and you should see the list of Twitter search results. Click refresh you will get the list of Twitter search results again.
21. Add these imports
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;
replace
 // retrieve the Twitter search last ID
with
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();  
try {
    Key lastIdPreviousKey = KeyFactory.createKey("LastId", "lastid");
    Entity lastIdPrevious = datastoreService.get(lastIdPreviousKey);
    last_id = (String) lastIdPrevious.getProperty("last_id");
} catch (EntityNotFoundException e) {
    e.printStackTrace();
}
and replace
// save the new Twitter search last_id
with
if (i == 0) {
    Key lastIdKey = KeyFactory.createKey("LastId", "lastid");
    Entity lastId = new Entity(lastIdKey);
    lastId.setProperty("last_id", jResult.getString("id_str"));
    datastoreService.put(lastId);
}
22. Deploy again and refresh http://<Application ID>.appspot.com and click Codelab and you should still see the list of Twitter search results. But click refresh this time you will get "No new tweet" as now only new tweet will be fetched in every refresh.

Register users’ GCM Registration ID
23. Now we will create a new Servlet in App Engine Project to handle the storage of GCM registration ID. Select the package "my.devfestkl.codelab" and select "New -> Class"
24. Enter the Name "RegisterServlet", Superclass "javax.servlet.http.HttpServlet" and click Finish
25. Select Source -> Override/Implement Methods..., tick on doPost(HttpServletequest, HttpServletResponse) and click OK
26. Add these imports
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
replace
super.doPost(req, resp);
with
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
Entity regId = new Entity("RegId");
if(req.getParameter("regId")!=null && !req.getParameter("regId").equals("")) {
    regId.setProperty("reg_id", req.getParameter("regId"));
    datastoreService.put(regId);
    resp.getWriter().append(regId + " successfully registered\r\n");
} else {
    resp.getWriter().append("No registration ID\r\n");      
}
27. Open web.xml in war/WEB-INF
28. Add these right after <web-app xmlns="......">

     Register
     my.devfestkl.codelab.RegisterServlet


     Register
     /register
 
29. Deploy the Google App Engine application again and now we will shift to the creation of the Android client

Setup Android Application for GCM
30. Select New -> Android Application Project
31. Enter Application Name "Codelab Android", Package Name "my.devfestkl.codelab" and click Next
32. Complete the Configure Launcher Icon and click Next
33. BlankActivity is selected and click Next, modify the Title to "Codelab Android" and click Finish
34. Select the libs folder, right click and select Import
35. Select File System and click Next
36. Browse to <Android SDK Folder>/extras/google/gcm/gcm-client/dist and click OK
37. Tick gcm.jar and click Finish
38. Add these inside <manifest> in AndroidManifest.xml
[NOTE: There is a bug in the Syntax Highlighter that displays repeated tags as nested tags]







39. Add these inside <application> in AndroidManifest.xml
[NOTE: There is a bug in the Syntax Highlighter that displays repeated tags as nested tags]

        
             
             
             
        


Build Android Application classes
40. Create a new Java Class in Codelab Android Project by selecting New->Class
41. Enter Name "GCMIntentService", Superclass "com.google.android.gcm.GCMBaseIntentService" and click Finish
42. Add this inside <application> in AndroidManifest.xml

43. Open MainActivity.java under src/my.devfestkl.codelab in Codelab Android Project
44. Add this at the top of the class with the SENDER ID from Step 5
public static String SENDER_ID = "YOUR SENDER ID"; 
45. Add this import
import com.google.android.gcm.GCMRegistrar; 
Add these into onCreate(Bundle savedInstanceState) below setContentView(R.layout.activity_main)
String regId = GCMRegistrar.getRegistrationId(this);
if (regId.equals("")) {
    GCMRegistrar.register(this, SENDER_ID);
} 
46. Create a new Java Class in in Codelab Android Project by selecting New->Class
47. Enter Name "RegisterTask", Superclass "android.os.AsyncTask" and click Finish
48. Add these into the top of the class

private Context context;
    
public RegisterTask(Context context) {
     super();
     this.context = context;
}
49. Add these imports
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import com.google.android.gcm.GCMRegistrar;
Add these into doInBackground(String... params) before "return null"
String urlString = "http://<Application ID>.appspot.com/register";
     
try {
    URL url = new URL(urlString);
    HttpURLConnection connection = (HttpURLConnection)url.openConnection();
    connection.setConnectTimeout(10000);
    connection.setDoOutput(true);
      
    String data = "regId=" + params[0];
      
    OutputStream os = connection.getOutputStream();
    os.write(data.getBytes());
    os.flush();
    os.close();
    InputStream is = connection.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader reader = new BufferedReader(isr, 8);
    StringBuilder builder = new StringBuilder();
    String line = "";
    while((line = reader.readLine())!=null) {
     builder.append(line);
    }
    reader.close();
    isr.close();
    is.close();
    String response = builder.toString();
    if(response.contains("successfully registered")) {
     GCMRegistrar.setRegisteredOnServer(context, true);
    }   
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
50. Open GCMIntentService.java under src/my.devfestkl.codelab in Codelab Android Project
51. Add this constructor at the top of the class
public GCMIntentService() {
     super(MainActivity.SENDER_ID);
}
52. Add this into onRegistered(Context arg0, String arg1)
new RegisterTask(this).execute(arg1);
53. Add these imports
import android.app.NotificationManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.app.PendingIntent;
54. Add this into onMessage(Context arg0, Intent arg1)
PendingIntent pi = PendingIntent.getActivity(arg0, 0, null, 0);
Builder builder = new NotificationCompat.Builder(this);
builder
   .setAutoCancel(true)
   .setContentTitle("New GCM Message")
   .setContentText(arg1.getStringExtra("tweet"))
   .setSmallIcon(R.drawable.ic_launcher)
   .setTicker("New GCM Message")
   .setContentIntent(pi)
   .setWhen(System.currentTimeMillis());     
NotificationManager mgr = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
mgr.notify(0, builder.build());    

Complete Google App Engine Application
55. Select the folder war/WEB-INF/lib in Codelab Google App Engine Project, right click and select Import
56. Browse to <Android SDK Folder>/extras/google/gcm/gcm-server/dist and click OK
57. Tick gcm-server.jar and click Finish
58. Select the folder war/WEB-INF/lib in Codelab Google App Engine Project, right click and select Import
59. Browse to <Android SDK Folder>/extras/google/gcm/gcm-server/lib and click OK
60. Tick json_simple-1.1.jar and click Finish
61. Right click on the project Codelab and select Properties
62. Select Java Build Path at the left bar and then select the Libraries tab
63. Click Add JARs and select gcm-server.jar from Codelab/war/WEB-INF/lib and click OK
64. Click Add JARs and select json_simple-1.1.jar from Codelab/war/WEB-INF/lib and click OK
65. Open CodelabServlet.java in src/my.devfestkl.codelab in Codelab Google App Engine Project
66. Add these imports
import java.util.ArrayList;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
67. Replace
// retrieve all GCM registration ID 
with
ArrayList<String> devices = new ArrayList<String>();     
Query query = new Query("RegId");
PreparedQuery preparedQuery = datastoreService.prepare(query);    
for (Entity regId : preparedQuery.asIterable()) {      
    String reg_id = (String) regId.getProperty("reg_id");
    devices.add(reg_id);
}
68. Replace
 
// send the GCM message
with (API Key from Step 8)
if(!devices.isEmpty()) {
 Sender sender = new Sender(<API KEY>);
    Message message = new Message.Builder()
      .addData("tweet", jResult.getString("text"))
      .build();        
    sender.send(message, devices, 5);
}
69. Deploy the Google App Engine application again
70. Select the Codelab Android project and then select Run->Run As->Android Application to run the Android Application either on an emulator or a device. For emulator, remember to add a Google Account first in Menu->Settings->Accounts & Sync->Add account->Google

Setup Cron for Google App Engine Application 71. Create a file cron.xml in war/WEB-INF in Codelab Google App Engine Project 72. Add these into cron.xml

   
  /codelab
  Perform Twitter search for new result every 2 minutes
  every 2 minutes
   

73. Deploy the Google App Engine application again

To Do:
  • Add security constraints to Codelab servlet so that only admin can refresh the page from browser
  • Modify the welcome page to remove the link to the Codelab servlet and add additional information to download the accompanying Android Application
  • Add an Unregister Servlet to remove GCM registration ID
  • Add an AsynTask in Android App to call Unregister Servlet in App Engine Server
  • Add a redirect to the home page for the Get request to both Register and Unregister Servlet
  • Add SQLite database to store results being pushed
  • Add a ListView to display the results from the SQLite database
  • Add a PendingIntent to the notification to show the ListView of results
  • Handle any error in MulticastResult returned by Sender.send()
Post a Comment