Aug 1 2009

Details of Interfacing to Google Voice, Part 0 of n

Category: Joel Ivory Johnson @ 11:43

In case it needs explanation, (n>=0) means that there is a possibility that I may never post on this again.  There are a lot of problem sets that I would like to solve.  The Google Voice client is a problem set that interest me, but looking at the priority of the other things I want to do this ranks pretty low.  Rather than leave it alone altogether I decided that I would post some information that would assist some one else in getting started with a solution. 

  • Communicating with the Google Voice Service : Generic Description
  • Applying Communications Requirements to .Net
  • Considerations in Building a Solution for Windows Mobile

Communicating with the Google Voice Service : Generic Description

Here I describe what must be done to communicate with Google Voice independent of the solution environment that you choose to use. The information as presented here will have the widest applicability to developers but require each developer to have knowledge in how each of these requirements can be implemented in their solution environment.

The Google Voice service uses a combination of JSON and HTML/XML for it's communications. All communication with the service occurs over Secure HTTP. With each request sent there are a set of cookies that must be sent also, otherwide a request will fail. In most cases it is sufficient to just send back the cookies that Google Voice has added to its responses.  However there are two cookies that you must make on your own: "GALX" and "_rnr_se."  I have absolutly no clue what information that either of these saves, I only know how to retrieve them.  These two values can be retrieved during the login process.

Logging In

The login process is simple. Begin by sending a GET response to https://google.com/voice . The response to this request may contain cookies that you will need to save and it may contain a redirect. If a redirect is encountered request then request the page to which you are redirected. Remember to include the cookies that you received from the previour response.  This may result in yet another redirect.  Follow the redirects in a loop until you receive a response that is not a redirect.  The cyclic redirecting can occur on some other request so just remember to always respond in this manner. The final page that you reach contains a piece of information that you need to add to your cookie collection.  Look in the resulting page for a hidden input field name GALX. Ad the value found in this field to a cookie also named GALX.  If you do not perform this step then all of the following instructions will fail. Once this cookies is created you can complete the login request by posting the login information to "/accounts/ServiceLoginAuth?service=grandcentral". The login information contains the following:

NameValue
contine https://www.google.com/voice/account/signin
service grandcentral
GALX read above instructions
Email Google Voice User ID
Passwd Google Voice Password
PersistentCookie yes
rmShown 1
signIn Sign in
asts Empty String

Some of the above parameters may not be necessary.  I've not tested for the minimal set required for a login. If the result of posting this data is a successful login the resulting page will contain a hidden field named "_rnr_se".  Save this value.  You will need it to successfully send SMS messages, initiate calls, and cancel call request.

Retrieving the Inbox Contents and Other Data

The process of retrieving information from your inbox and other areas of Google Voice will conform to the same pattern. A listing of the URLs for the various functionality is available from various places all over the internet (I retrieved the information from http://posttopic.com/topic/google-voice-add-on-development). I'll save you the trouble of clicking on that link and relist the URLs here:

Functionality URL
Inbox XML https://www.google.com/voice/inbox/recent/inbox/
Starred Calls XML https://www.google.com/voice/inbox/recent/starred/
All Calls XML https://www.google.com/voice/inbox/recent/all/
Spam XML https://www.google.com/voice/inbox/recent/spam/
Trash XML https://www.google.com/voice/inbox/recent/trash/
Voicemail XML https://www.google.com/voice/inbox/recent/voicemail/
SMS XML https://www.google.com/voice/inbox/recent/sms/
Recorded Calls XML https://www.google.com/voice/inbox/recent/recorded/
Placed Calls XML https://www.google.com/voice/inbox/recent/placed/
Received Calls XML https://www.google.com/voice/inbox/recent/received/
Missed Calls XML https://www.google.com/voice/inbox/recent/missed/

The response contains a combination of JSON and and HTML.  It looks like this:

<?xml version="1.0" encoding="UTF-8" ?>
<response>
   <json><!CDATA[

   ]]></json>
   <html><!CDATA[


   ]]></html>
</response>

The JSON portion of the message contains a list of message in the requested category.  But the information available in this structure is generic. You can see from where the message came, the time at which it arrived, it's spam, trash, and read/unread classification, but not its contents.  The get the message contents you must look in the HTML portion of the response. Within the root of the HTML section the collection of elements at the root level are <div> tags.  All of these tags have an ID attribute. The string in the ID can be used to map a message from the HTML portion of the message to the json portion.  What I've not examined is whether the HTML section contains the full information on the message (in which cas the JSON portion can be ignored all together) or if a union of the data in these two sections is required to get a full set of attributes.

Sending an SMS, Initiating a Call, And Cancelling a Call

Unlike the other exposed functionality to initiate or cancel a call or send an SMS you will need to post data.  Remeber the _rnr_se value that I mentioned earlier?  You will need it here. Here are the URLs that you need for these functions and the parameters that must be posted to invoke them.

FunctionalityURLParameters
Send SMS https://www.google.com/voice/call/connect/
ParameterValue
id Empty String
phoneNumber Number to which you are sending message
text message
_rnr_se acquired at login
Initiate Call https://www.google.com/voice/call/cancel/
ParameterValue
outgoingNumber Number you intend to call
forwardingNumber The Number of the phone that you will be using
subscriberNumber undefined
remember 0
_rnr_se acquired at login
Cancel Call https://www.google.com/voice/sms/send/
ParameterValue
outgoingNumber undefined
forwardingNumber undefined
cancelType C2C
_rnr_se acquired at login
There's not to much to say about these.  Just post the messages and watch them work.

Applying the Communications Requirements to .Net

In testing this information out I implemented code to test some of the functionality. My experience may help others that plan to make a client in .Net.  If you are a developer of some other solution environment feel free to tune out now.  The rest of this post will have little applicability to your solution environment.
The first thing to know is that you must set the AllowAutoRedirect property to false. This property is true by default.  If you leave it enabled then when it is performing the redirect it is ignoring all of the cookies that come down from the server.  It took my hours to figure out why I was not receiving some expected cookies before I found out the problem was in allowing auto-redirects.  I don't know if this is a bug or not, but at the very least it is something of which you need to be aware.  To ensure uniform creation of my HttpWebRequest objects I defined a few functions that would take care of creating them so that I could ensure consistency.  When data is being posted you must set the content type of the request to "application/x-www-form-urlencoded".  To extract the GALX and _rnr_se values I made use of regular expressions.  Regularexpressions worked well for getting something up and running as quickly as possible but the expressions I used are inflexible; if the fields they target were formatted in a different way the expression matching will fail.
I had two implementations of my test code.  One was interacting with the Xml/JSON functions.  The other was loading information from the mobile version of google voice and performing parsing on the data.  Both versions had to rely on some of the same functionality.  So the basics of the HttpWebRequest handling and the extraction of the GALX and _rnr_se values were handled in the base class.
    public abstract class HttpPhone : IPhone
    {
        const string ServiceDomain = "www.google.com";
        const string UserAgent = "Prometheos";

        protected string  _userName;
        protected string  _password;
        protected string _rnr_se;

        private CookieContainer _sessionCookieContainer;

        protected CookieContainer SessionCookieContainer
        {
            get 
            {
                if (_sessionCookieContainer==null)
                    _sessionCookieContainer = new CookieContainer();
                return _sessionCookieContainer;
            }
        }

        protected HttpWebResponse GetResponse(string service)
        {
            return GetResponse(service, null);
        }

        HttpWebRequest GetNewRequest(string targetUrl)
        {
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(targetUrl);
            request.CookieContainer = SessionCookieContainer;
            request.UserAgent = UserAgent;
            request.AllowAutoRedirect = false;
            request.Accept = "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, */*";
            return request;
        }
        protected HttpWebResponse GetResponse(string service, string postData)
        {
            return GetResponse(service, postData, null);
        }
        protected HttpWebResponse GetResponse(string service, string postData, string contentType)
        {

            string targetUrl;
            if(service.Contains(':'))
                targetUrl=service;
            else
                targetUrl = String.Format("https://{0}{1}",ServiceDomain,service);
            HttpWebRequest request = GetNewRequest(targetUrl);
            if ((postData!=null)&&(postData.Length>0))
            {
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";
                byte[] postDataBytes = Encoding.UTF8.GetBytes(postData);
                request.ContentLength = postDataBytes.Length;
                Stream dataStream = request.GetRequestStream();
                dataStream.Write(postDataBytes, 0, postDataBytes.Length);
                dataStream.Close();

            }
            if ((contentType != null) && (contentType.Length > 0))
                request.ContentType = contentType;
            HttpWebResponse retVal = (HttpWebResponse)request.GetResponse();
            //Follow the redirects (if present)
            while (retVal.StatusCode == HttpStatusCode.Found)
            {
                request = GetNewRequest(retVal.Headers["Location"]);
                retVal = (HttpWebResponse)request.GetResponse();
            }
            return retVal;

        }


        protected string UrlEncodeParamList(params string[] itemList)
        {
            if ((itemList.Length & (~1)) != itemList.Length)
                throw new ArgumentException("There must be an even number of items passed");
            StringBuilder sbParamList = new StringBuilder();
            for (int i = 0; i < itemList.Length ; i+=2)
            {
                sbParamList.AppendFormat("{0}={1}", HttpUtility.UrlEncode(itemListIdea), HttpUtility.UrlEncode(itemList[i + 1]) );
                if (i + 3 < itemList.Length)
                    sbParamList.Append("&");
            }
            return sbParamList.ToString();
        }

        protected HttpPhone(string userName, string password)
        {
            _userName = userName;
            _password = password;
        }

        protected string GetGalx(string source)
        {
            Regex regexGALX = new Regex(".*name=\"GALX\"\\s+value=\"(?[^\"]*)\".*");
            Match m = regexGALX.Match(source);
            return m.Groups["galx"].Value;
        }

        protected string Get_rnr_se(string source)
        {
            Regex regex_rnr_se = new Regex(".*name=\"_rnr_se\"\\s*(type=\"hidden\"\\s*)?value=\"(?[^\"]*)\".*", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
            Match m = regex_rnr_se.Match(source);
            return m.Groups["target"].Value;
        }
public abstract bool Login(); public abstract void SendSms(string targetNumber, string message); public abstract void InitiateCall(string targetNumber, string forwardingNumber); public abstract void CancelCall(); public abstract IMessageCollection RetrieveMessages(MessageType m); }

With all of this functionality in place making calls, cancelling calls, and sending SMS is super easy!

        public override void SendSms(string targetNumber, string message)
        {
            string postData = UrlEncodeParamList(
                "id",String.Empty,
                "phoneNumber",targetNumber,
                "text", message,
                "_rnr_se",_rnr_se);
            GetResponse(URL_SendSms, postData);
        }

        public override void InitiateCall(string targetNumber, string forwardingNumber)
        {
            string postData = UrlEncodeParamList(
                "outgoingNumber", targetNumber,
                "forwardingNumber", forwardingNumber,
                "subscriberNumber","undefined",
                "remember","0",
                "_rnr_se",_rnr_se
                );
            GetResponse(URL_InitiateCall, postData);
        }

        public override void CancelCall()
        {
            string postData = UrlEncodeParamList(
                "outgoingNumber", "undefined",
                "forwardingNumber", "undefined",
                "subscriberNumber", "undefined",
                "remember", "0",
                "_rnr_se", _rnr_se
                );
            GetResponse(URL_CancelCall, postData);
        }

If you want to see if this code really works or not I've wrapped it up in a simple UI that will let you invoke it. UI guidelines were thrown to the wind.  A screen shot of the interace is below. A few things to know, one yur user ID and password are successfully authenticated the Username and password text boxes are ignored for the rest of the life of the program. And I didn't update the visual state attributes of the button.  It turns blue once it has focus. If you dig through the code you can find where I started to parse the JSON messages. The actual code is attached to the bottom of this page.  Feel free to download it.

 

 

Considerations in Building a Solution for Windows Mobile

 For Windows Mobile there are several ways that some one can go about creating a solution for a Google Voice client. I've thought a little about making simple an integrated clients.

Saving the Credentials

Saving the user's credentials on the unit should be done with care. If at all possible I would suggest not saving the user's credentials at all. Instead save the cookies that were created when the user authenticated. Make use of the encryption classes offered by .Net too.

Simple Client

A simple client would only start when the user starts it and run until either the user terminates it or it is terminated by a memory reclamation cycle. When the client is running it would pool the Google Voice service ever few minutes and update itself. When the client closes the user will receive no notification of new messages until th next time the application is started.  If this application  is run on a Windows Mobile Standard device a decision must be made on what occurs when the application goes to the background. Windows Mobile Standard devices are always on, so an application in the background has an opportunity to have a more significant impact on battery life. When in the background I would suggest increasing the pooling interval. When the application is in the background also remember not to update the screen. Doing so waste energy while providing no benefit.

 Continual Updates and Notifications

There will likely be a number of users that want to be notified day round about the new messages they receive on Google Voice. For such a solution do not make a program that continuously runs in unattended mode. I wrote a guide on power management on Windows Mobile last year. Within the guide I make mention to how to run a program in unattended mode so that it can continue running when a device goes to "sleep." I've gotten occasional e-mails from developers that have used this information to keep their program running continuously with data connections up and running wondering why their device's battery life wasbeing compromosed by so much. For an application that needs to pool a service on timed intervals unattended mode is only part of the answer. The preferred battery-friendly solution is to use scheduled system events to occasionally bring the device from suspended mode to unattended mode.  When an event occurs the device can poll the service and notify the user if a new message was received.  When it is done it can release unattended mode so the device goes back to sleep.

To save memory such an application can completly terminate itself when it gets done checking for new messages. It can schedule itself to start up again later. I wrote a guide on automatically starting a program on Windows Mobile that contains information on how you can do this.

Further Integrating the Application into Windows Mobile

I've considered how such an application could be better integrated in Windows Mobile. I personally would like to see the incoming SMSes from Google appear in my SMS Inbox along side the other messages that I already have and would prefer to use that interface so that I have one less place in which to look for my messages. The Google Voice client would need to use CEMAPI to manipulate the SMS folder.  If you are using .Net and needed information on interfacing to CEMAPI use the wrapper that rtw33 posted on CodeProject.com.

Download Code

Tags:

Comments

1.
pingback directory.lifehelper.net says:

Pingback from directory.lifehelper.net

Articles about _rnr_se volume 1 «  Article Directory

Comments are closed