Wednesday, September 28, 2011

How To Do Android C2DM Push Notifications Part 2

On the server side, in order to send a message, you need the following:
  • clientLogin id
  • role email account authorized to send c2dm messages (email and password)

If your application has a lot of traffic, you will want to write a windows service or a cron job to send out the messages in batch instead of sending them real time. The source code in this post will be based on a windows service written in C# (C# is very similar to Java).

In my database, I have the following tables:
  • c2dm_user - stores the user id and c2dm token pairs
  • c2dm_message - stores the messages for the windows service to send out in batch, say each minute 1000 messages

When user A sends a message to user B, a message alert will be inserted into the c2dm_message table if both users A and B exist on c2dm_user table. C2DM is not meant for heavy weighted messages. The intent is to alert the users that there are new messages. What I did is I use the unread message count as the payload. Users will see a notification saying "You got x messages!" in their mobile devices.


Getting the ClientLogin Id

public static String getC2dmAuthToken()
        {
            String responseFromServer = null;

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(CLIENT_LOGIN_URL);
                request.ContentType = "application/x-www-form-urlencoded";

                Stream dataStream = null;
                request.Method = "POST";

                dataStream = request.GetRequestStream();

                StringBuilder postData = new StringBuilder();

                postData.Append("Email=" + EMAIL);
                postData.Append("&Passwd=" + PASSWORD);
                postData.Append("&accountType=" + ACCOUNT_TYPE);
                postData.Append("&source=" + SOURCE);
                postData.Append("&service=" + SERVICE);

                byte[] byteArray = Encoding.UTF8.GetBytes(postData.ToString());

                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();


                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                dataStream = response.GetResponseStream();
                byte[] bytesToRead = new byte[response.ContentLength];

                int actuallyRead = 0;
                for (; actuallyRead < bytesToRead.Length; )
                    actuallyRead += dataStream.Read(bytesToRead, actuallyRead, bytesToRead.Length - actuallyRead);

                responseFromServer = Encoding.UTF8.GetString(bytesToRead);

                dataStream.Close();
                response.Close();
            }
            catch (Exception e)
            {
                // log exception
            }

            if (responseFromServer != null && !responseFromServer.Equals(""))
            {
                if (responseFromServer.Contains("Auth="))
                {
                    responseFromServer = responseFromServer.Substring(responseFromServer.IndexOf("Auth=") + 5);
                    responseFromServer = responseFromServer.Replace("\n", "");
                }
                else
                    responseFromServer = null;
            }

            return responseFromServer;
        }

If your email is a gmail mail, then ACCOUNT_TYPE is "GOOGLE"; service is "ac2dm"; source is a log of what is your app.  If you get an error, register here again - http://code.google.com/android/c2dm/signup.html, and make sure it's a gmail account.


Sending a message

public static String SendMessage(string userId, string registrationId, string msg)
        {
            if (authToken == null || authToken.Equals(""))
            {
                authToken = getC2dmServerAuthKey();
                if (authToken == null || authToken.Equals(""))
                {
                    authToken = getC2dmAuthToken();
                }
            }

            ServicePointManager.ServerCertificateValidationCallback += delegate(
                                            object sender,
                                            X509Certificate certificate,
                                            X509Chain chain,
                                            SslPolicyErrors sslPolicyErrors)
            {
                return true;
            };

            String responseFromServer = null;

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(SEND_MESSAGE_URL);
                request.Timeout = 2000;
                request.KeepAlive = true;
                request.ContentType = "application/x-www-form-urlencoded";

                request.Headers.Add("Authorization", "GoogleLogin auth=" + authToken);

                Stream dataStream = null;

                request.Method = "POST";

                StringBuilder postData = new StringBuilder();
                postData.Append("registration_id=" + registrationId);
                postData.Append("&collapse_key=0");
                postData.Append("&data.payload=" + msg);
                postData.Append("&data.userid=" + userId);

                byte[] byteArray = Encoding.UTF8.GetBytes(postData.ToString());
                request.ContentLength = byteArray.Length;


                dataStream = request.GetRequestStream();
                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();

                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                if (response != null)
                {
                    if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
                    {
                        // 401 - unauthorized, 403 = forbidden
                        throw new Exception("401");
                    }
                    else if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
                    {
                        // 503 - implement exponential backoff
                        throw new Exception("501");
                    }
                    else if (response.StatusCode == HttpStatusCode.OK)
                    {
                        string updateClientAuth = response.GetResponseHeader("Update-Client-Auth");
                        if (!string.IsNullOrEmpty(updateClientAuth))
                        {
                            authToken = updateClientAuth;
                        }

                        responseFromServer = (new StreamReader(response.GetResponseStream())).ReadToEnd();

                    }
                    dataStream.Close();
                }
                response.Close();
            }
            catch (Exception e)
            {
                if (e.Message != null)
                {
                    if (e.Message.IndexOf("401") > 0)
                    {
                        throw new Exception("401");
                    }
                    else if (e.Message.IndexOf("503") > 0)
                    {
                        throw new Exception("503");
                    }
                }
                throw new Exception(e.Message);
            }

            if (responseFromServer != null && !responseFromServer.Equals(""))
            {
                processSendMessageResponse(responseFromServer, userId, registrationId);
            }

            return responseFromServer;
        }

Note that we are sending the ClientLogin Id in the header for authentication. If you get a 503, implement exponential backoff to avoid getting banned by Google.


Error Processing

public static void processSendMessageResponse(String response, String userId, String registrationId)
        {
            if (response == null || response.Equals(""))
                return;

            if (response.StartsWith("Error="))
            {
                string error = response.Substring(response.IndexOf("Error=") + 6);
                switch (error.ToLower().Trim())
                {
                    case "quotaexceeded":
                        break;
                    case "devicequotaexceeded":
                        break;
                    case "invalidregistration":
                        break;
                    case "notregistered":
                        int s1 = DAL.logServiceStartTime(9999, 0, Thread.CurrentThread.Name + " deleting token");
                        DAL.deleteToken(registrationId);
                        break;
                    case "messagetoobig":
                        break;
                    case "missingcollapsekey":
                        break;
                    default:
                        break;
                }

            }

        }

You may want to record the errors.


10 threaded C2DM Windows Service

using System;
using System.Collections.Generic;
using System.Configuration;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Text;
using System.Threading;

namespace C2dmService
{
    public partial class C2dmService : ServiceBase
    {

        private static int BACKOFF_FACTOR = 2;
        private static int INITIAL_BACKOFF = 1; // 1 min
        private static int DEBUG_TIMEOUT = 30000;

        public static bool isRunning = false;
        public static bool stopRequested = false;
        private static Object queuelock = new Object();
        private static int NUM_MSGS_PER_THREAD = 500;

        private Queue<ScheduledAlert> mQueue;

        private List<Thread> c2dmThreads = new List<Thread>();

        public C2dmService()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            Thread.Sleep(DEBUG_TIMEOUT);

            stopRequested = false;

            while (c2dmThreads.Count < 10)
            {
                c2dmThreads.Add(null);
            }

            if (c2dmThreads != null && c2dmThreads.Count > 0)
            {
                for (int i = 0; i < c2dmThreads.Count; i++)
                {
                    if (c2dmThreads[i] != null)
                    {
                        if (c2dmThreads[i].IsAlive)
                            c2dmThreads[i].Abort();

                        c2dmThreads[i] = null;
                    }

                    c2dmThreads[i] = new Thread(Run);
                    if (c2dmThreads[i].Name == null)
                        c2dmThreads[i].Name = "c2dm Thread " + i;
                    c2dmThreads[i].Start();
                }
            }

            isRunning = true;
        }

        protected override void OnStop()
        {
            stopRequested = true;

            int counter = 0;

            Thread.Sleep(500);

            while (isRunning && counter++ < 60)
            {
                if (c2dmThreads != null && c2dmThreads.Count > 0)
                {
                    for (int i = 0; i < c2dmThreads.Count; i++)
                    {
                        if (c2dmThreads[i] != null)
                        {
                            c2dmThreads[i].Join(500);
                        }
                    }
                }
            }
        }

        public void Run()
        {
            DateTime currentTime = DateTime.Now;

            while (true)
            {
                int counter = 0;
                while (counter++ < 60)
                {
                    if (C2dmService.stopRequested)
                    {
                        C2dmService.isRunning = false;
                        return;
                    }

                    Thread.Sleep(1000);
                }

                executeMsgSchedulerReporting();
            }
        }

        private bool executeMsgSchedulerReporting()
        {
            int backoff = INITIAL_BACKOFF;
            int recordId = -1;

            try
            {
                List<ScheduledAlert> alerts = new List<ScheduledAlert>();

              
                lock (queuelock)
                {
                    if (mQueue == null || (mQueue != null && mQueue.Count <= 0))
                    {
                        mQueue = DAL.getScheduledAlerts();
                    }

                    int numMsgsToFetch = (mQueue.Count > NUM_MSGS_PER_THREAD) ? NUM_MSGS_PER_THREAD : mQueue.Count;

                    for (int i = 0; i < numMsgsToFetch; i++)
                        alerts.Add(mQueue.Dequeue());
                }


                if (alerts.Count > 0)
                {
                    foreach (ScheduledAlert a in alerts)
                    {
                        try
                        {
                            C2dmMessaging.SendMessage(a.Receiver_id.ToString(), a.Token, a.Count.ToString());
                        }
                        catch (Exception e2)
                        {
                            if (e2.Message != null)
                            {
                                if(e2.Message.Equals("401"))
                                {
                                    // renew authkey
                                    C2dmMessaging.renewC2dmAuthToken();
                                }
                                else if (e2.Message.Equals("503"))
                                {
                                    // google c2dm service is unavailable - exponential backoff
                                    Thread.Sleep(backoff * 60);
                                    backoff *= BACKOFF_FACTOR;
                                }
                            }
                        }
                    }
                }

            }
            catch (Exception e)
            {
                EventLog.WriteEntry(C2DM_SERVICE, C2DM_SERVICE + " an error - " + e.ToString(), EventLogEntryType.Error);
            }

            EventLog.WriteEntry(C2DM_SERVICE, C2DM_SERVICE + " has finished.", EventLogEntryType.SuccessAudit);

            return false;
        }

       
    }
}

No comments:

Post a Comment