Friday, May 2, 2014

SecurityException (@Parcel:readException:1327) {AdWorker #1}

From my research, this bug seems to be introduced when admob was integrated into Google Play service. You would see this exception when you use r15, r16 of Google Play services.

Currently, there's no solution. But the Google Team is aimming to fix this in r17.

NullPointerException (@ActionBarImplICS:getThemedContext:287) {main}

This most likely shows up when you are trying to hide the actionbar in your app's activities.

You may be using:

requestWindowFeature(Window.FEATURE_NO_TITLE);

However, you may see NullPointerException (@ActionBarImplICS:getThemedContext:287) {main} when users press the menu button in their devices. Some Android devices have the menu button as the hardware button, while Kindle Fire devices have the menu button as a software button.

To prevent the crash from happening, you can override the menu button key code to do nothing:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
   if ( keyCode == KeyEvent.KEYCODE_MENU ) {
       // do nothing
       return true;
   }
   return super.onKeyDown(keyCode, event);
}

Wednesday, April 23, 2014

Android Strikethrough HTML not rendering in EditText

I was working on supporting strikethrough in EditText of my app. Using StrikethroughSpan, I was able to show it in the EditText element. This strikethrough is converted to <strike></strike> when you use the toHtml() method.

However, when I try to render the strikethrough back to the EditText, I found the Html.fromHtml method does not support strikethough.

The only way to render the strikethough seems to be through the Webview. 

Saturday, February 8, 2014

StudyFlashcard 3.0 - Mobile Flashcards For Android

During the past month, I worked on a new version of StudyFlashcard For Android.

StudyFlashcard 3.0 is a mobile flashcards application for students to learn foreign languages, review exam materials, or simply as a memorization tool.
Here is the UI of the application:

















StudyFlashcard Android is different from other flashcard applications in the following ways:
1) Organize flashcards with #hashtags

Powerful #hashtag system lets you add flashcards to your midterms, exams, favorite lists, or secret lists. Study in whatever ways you imagine.
2) Study multiple subjects at once with Merge Play

Whether you have three exams on the same day or you are just bored of studying one subject for too long, Merge Play lets you study multiple flashcard sets together.
3) Study multiple questions at once with Cram Mode (Web only)

Cram mode offers a multi-column view that lets you study a lot of questions at once. This is perfect for a quick memory refresh before exams.
4) Perfect for learning vocabularies and foreign languages with three sided flashcards

Each StudyFlashcard has three sides: question, answer, and hint. Three-sided 
flashcards are perfect for learning Asian languages such as Chinese and Japanese.

Full list of features:

• Sync Flashcards with StudyFlashcard.com
• Score tracking
• hashtags
• play multiple sets of flashcards/quizzes at the same time
• Sign up is not necessary
• Works completely offline (You can create flashcards offline)
• Three sided flashcards/quizzes with question, answer and hint
• Can adjust text size

Give StudyFlashcard Android a try. Let us know what you think.

Saturday, October 12, 2013

Android - Connecting to SQLite through adb

This will only work for the Android emulator or a rooted phone. In unrooted/unlocked phones, the data files are locked.

cd into your platform-tools directory in your terminal
cd C:\Program Files (x86)\Android\android-sdk\platform-tools
Start the emulator and check if adb can detect the emulator
adb devices
Start Shell
adb shell
Connect to sqlite
sqlite3 /data/data/{your_package_name}/databases/{your_db}

Thursday, December 27, 2012

Use CellObject to browse Android SQLite data contents

This will only work with Android emulators or rooted phones.


Extract it. Find net.cellobject.sqlitexmlbrowser_1.2.0.jar.

Put net.cellobject.sqlitexmlbrowser_1.2.0.jar into the eclipce plugin directory.

Restart Eclipse.

Start the emulator.

Go to DDMS

Select your device on the left panel.

Then on the right side panel, navigate to 
data/data/{your_app_package_namespace}/databases/{app_name}
Click the database icon on the top bar of this right side panel.

You should now be able to see the table schema and browse the data in the CellObject SQLite Browser.

Sunday, December 16, 2012

Android selector needs drawable instead of just background


The following will result in a crash saying that cannot inflate the drawable.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" android:background="#000000" />
  <item android:state_focused="true" android:background="#000000" />
  <item android:background="#FFFFFF" />
</selector>

The solution is to change the background to use drawable

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" android:drawable="@color/grey" />
  <item android:state_focused="true" android:drawable="@color/grey" />
  <item android:drawable="@color/white" />
</selector>

android:drawable attribute does not accept raw color.

Define color.xml in res/values.

<?xml version="1.0" encoding="utf-8"?>
<resources>
     <color name="black">#000</color>
     <color name="white">#fff</color>
     <color name="grey">#cccccc</color>
</resources>


Android Listview RuntimeException - Your content must have a ListView whose id attribute is android.R.id.list

"Your content must have a listview whose id is android.r.id.list"

Just make sure your listview's id is @android:id/list.

<ListView
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

Sunday, February 19, 2012

Blackberry Android Simulator Blinking Issue

Just submitted my first android port to Blackberry. In case you don't know, Blackberry Playbook OS 2 can run Android apps.

If you are using the simulator, you will notice whenever, you have an EditText, the whole screen will keep on blinking. After doing some research, I found the following:

http://twitter.com/#!/BlackBerryDev/status/169882992997711872

According to Blackberry, it seems like it's only the simulator that's doing that. I do not have an actual playbook, so I do not know if that's the case. So far, it took me half a day to convert my Android app to the Playbook.

The app I converted is the Ignite Study Flash Card.

PhoneGap and HTML5 - Ad CPM and CPC Calculator

Lately there are a lot of buzz about creating HTML5 PhoneGap applications on Android, iPhone, and Blackberry. I am not a big fan of writing HTML5 phone apps, since I think the HTML5 speed on the phone is still very laggy and slow.

If you have written any jquery mobile websites, you will probably see that the "apps" still feel like web pages whether than apps. It is very easy to tell whether an app is native or written in HTML5 on a phone. Feel free to comment below to let me know if you see any smooth HTML5 phone apps out there.

Anyways, I quickly wrote an app called Ad CPM and CPC Calculator. Below are the screen shots.




Description:


Ads CPM and CPC Calculator is the FIRST advertising calculator that lets you compute eCPC, eCPM, conversion rate, CTR, cost per conversion and campaign cost. You will never need to do tedious hand calculations again.

Ads CPM and CPC Calculator comes with three calculators:
Ad Rate Calculator lets you estimate the campaign cost by CPM and CPC.
Ad Inventory Calculator estimates impression and clicks given the budget.
Ad Spent Summary Calculator computes eCPC, eCPM, conversion rate, CTR, cost per conversion and campaign cost.


Android Market Link:

Download Ads CPM and CPC Calculator


Results:

I think the app is still quite laggy. It probably has to do with jquery mobile. Sometimes, the footer would bounce up or not refreshed probably. When switching through pages, it's not very responsive. You will have to click more than once on rare occasions.

I strongly recommend everyone to go native if possible. The only benefit for using HTML5 is it's fast and you can quickly launch to multiple platforms.

Sunday, January 15, 2012

IG Stock And Option Calculator For Android

Hi all, if you are a common stock trader using options to hedge your risk, you will find this tool useful. Whenever I purchase stocks, there are always a lot of calculations I compute to measure the risk.  I built this tool called IG Stock And Option Calculator to simply the whole tedious process.


Description:

IG Stock And Option Calculator includes 6 commonly used stock trading strategies for estimating risk.

Stock Return, Stop Loss, Long Call, Long Put, Covered Call, Protective Put.

Stock Return is for estimating or calculating your net profit from buying and selling a stock

Stop Loss calculates what prices you want to sell your stock at.

Long Call, Long Put, Cover Call, and Protective Put shows you the net profit and breakeven points.

Definitions:

A long call is simply the purchase of one call option.

A long put is simply the purchase of one put option.

A covered call is a financial market transaction in which holds a long position in an asset and writes (sells) call options on that same asset in an attempt to generate increased income from the asset.

A protective put is a risk-management strategy that investors can use to guard against the loss of unrealized gains. The put option acts like an insurance policy - it costs money, which reduces the investor's potential gains from owning the security, but it also reduces his risk of losing money if the security declines in value.


Product Features:
  •  Estimate stock return
  • Calculate sell price for stop loss order
  • Calculate break even price, net profit for long call
  • Calculate break even price, net profit for long put
  • Calculate break even price, net profit for long covered call
  • Calculate break even price, net profit for protective put
Screenshots:




Download Link:
IG Stock And Option Calculator

Saturday, December 17, 2011

Simple SMS Scheduler

Few weeks ago, I attended a seminar in a university. It was about gathering student resources to start a business. The club provides information about how to get fundings, mentors, etc. It also have a section about creating mobile apps. One of the guys there was talking about an SMS scheduler to send messages to his girlfriend. I found it simple and interesting.

I built the following part time:

App Name: Simple SMS Scheduler
Market Link: https://market.android.com/details?id=com.ignitesms.android

Description:

Simple SMS Scheduler is a clean and simple tool for sending automatic messages at a chosen time. 
Features:
  • Can choose recipients from contact list
  • Sent SMS shows up on default android messaging app
  • Optional delivery and sent reporting via status bar notification
  • Delivery and error history

Screenshots:





Feel free to try it out at https://market.android.com/details?id=com.ignitesms.android.

Feel free to let me know what you think. Feedback, criticisms are always welcomed.

Friday, December 16, 2011

Using javascript in webview to call native code

We will define two js functions in android

  • goToHome
  • showToast

for javacript calls from the webview.

You would call the functions like
<script type="text/javascript">
  window.jsinterface.goToHome()
  window.jsinterface.showToast()
</script>
Define the following in your android activity as an inner class.
public class JSInterface {
public void goToHome() {
Intent i = new Intent(getApplicationContext(), Home.class);
startActivity(i);
}
public void showToast(final String msg) { Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
}
}
In your onCreate function, intialize the webview similar to below:
WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.setVisibility(View.GONE);
mWebView.addJavascriptInterface(new JSInterface(), "jsinterface");
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int progress) {
Logger.v("webview", String.format("progress changed: %d", progress));
if (progress == 100) {
// webpage loaded completely
} else {
// webpage is loading
}
}
//@Override
public boolean onJsAlert(WebView view, String url,
String message, JsResult result) {
// if you do js alerts, this will show them as toast
Toast.makeText(getBaseContext(), message, Toast.LENGTH_SHORT).show();
result.confirm();
return true;
}
});

Basicly, that's it!

Android soft keyboard not showing on webview

Sometimes your webview may not show the soft keyboard.  This has to do with the focus and the timing that the webview is rendered.

If the soft keyboard does not show up, try to add the following to your webview:

mWebView.requestFocus(View.FOCUS_DOWN);
mWebView.setOnTouchListener(new View.OnTouchListener() {
       @Override
       public boolean onTouch(View v, MotionEvent event) {
           switch (event.getAction()) {
               case MotionEvent.ACTION_DOWN:
               case MotionEvent.ACTION_UP:
                   if (!v.hasFocus()) {
                       v.requestFocus();
                   }
                   break;
           }
           return false;
       }
   });

Thursday, November 24, 2011

Android how to schedule multiple alarms

The Android alarm service is very useful if for scheduling tasks to be done at future dates.  A common use is passing a PendingIntent to the AlarmManager.  When the alarm wakes up the phone, a broadcastReceiver is used to perform the desired actions:

Intent intent = new Intent(a, AlarmReceiver.class);
intent.putExtra("row_id", rowId);
final int alarmId = (int) System.currentTimeMillis();
PendingIntent sender = PendingIntent.getBroadcast(a, alarmId, intent,
    PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) a.getSystemService(a.ALARM_SERVICE);
    am.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);

Notice the "alarmId" above.  By setting different private request codes as an argument for the getBroadcast function, you can schedule multiple alarms.  If you use the same id, the previous alarms scheduled will be cancelled.

How to call Android contact list

1) Add the following to your AndroidManifest:

<uses-permission android:name="android.permission.READ_CONTACTS">

2) Call the Contact Picker from your activity by using the following intent:

Intent i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(i, CHOOSE_CONTACT);

3) get the phone through the contact id; you will need to query the contentResolver twice

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);

  switch (requestCode) {
    case (CHOOSE_CONTACT):
    if (resultCode == Activity.RESULT_OK) {
        Uri contactInfo = data.getData();
        Cursor c = managedQuery(contactInfo, null, null, null, null);
        if (c.moveToFirst()) {
                String contactId = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID));
                String name = c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
                String hasPhone = c.getString(c.getColumnIndex(
                        ContactsContract.Contacts.HAS_PHONE_NUMBER));
                String phoneNumber = "";
                if (hasPhone != null &amp;&amp; hasPhone.equalsIgnoreCase("1")) {
                        Cursor phones = getContentResolver()
                                .query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,                                                            
                                           ContactsContract.CommonDataKinds.Phone.CONTACT_ID
                                           + " = " + contactId, null, null);

                               while (phones.moveToNext()) {
                                        phoneNumber = phones.getString(phones
                                                .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                               }
                       phones.close();
               }

               // do something with the name and phone; you can also accesss other information such as photo
        }
    }
    break;
  }
}

Saturday, November 19, 2011

Passing data between activities or context objects

You can pass data between activities, broadcastReceivers, etc, by using Intent and Bundle Objects as follows:
Intent intent = new Intent(FROM_ACTIVITY, TO_ACTIVITY);
Bundle bundle = new Bundle();
bundle.putString("data", "Hello World!");
intent.putExtras(bundle);
Similarly, retreiving an object:
Bundle bundle = intent.getExtras();
String data = b.getInt("data", "");
The second argument is the default value. Notice that the bundle object is an argument of the onCreate method for every activity.

Thursday, November 17, 2011

Android sending a sms

1) Add the following permissions in the AndroidManifest.xml:

<uses-permission android:name="android.permission.SEND_SMS"></uses-permission>
<uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>


2) Import the SMSManager library:

import android.telephony.SmsManager;


3) The simplest way to send a sms is as follows:

public static void sendSMS(String context, String phoneNumber, String message) {      
    PendingIntent pi = PendingIntent.getActivity(this, 0,
            new Intent(context, YOUR_ACTIVITY), 0);              
    SmsManager sms = SmsManager.getDefault();
    sms.sendTextMessage(phoneNumber, null, message, pi, null);      
}


4) To test the sending, start two emulators. The top left corner of the windows is the port number (ex. 5554). Use the port number as the phone number and try to send a message.


5) To get the status about the message after you send it, you can create the sending intent and delivery intent as the broadcastReciever.

public static void sendSMS(Context context, String phoneNumber, String message)
{
        PendingIntent sentPi = PendingIntent.getBroadcast(context, 0,
                new Intent(SENT), 0);
   
        PendingIntent deliveredPi = PendingIntent.getBroadcast(context, 0,
                new Intent(DELIVERED), 0);
   
            registerReceiver(new BroadcastReceiver(){
                @Override
                public void onReceive(Context arg0, Intent arg1) {
                    switch (getResultCode())
                    {
                        case Activity.RESULT_OK:
                            Toast.makeText(context, "SMS sent",
                                    Toast.LENGTH_SHORT).show();
                            break;
                        case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                            Toast.makeText(context, "Generic failure",
                                    Toast.LENGTH_SHORT).show();
                            break;
                        case SmsManager.RESULT_ERROR_NO_SERVICE:
                            Toast.makeText(context, "No service",
                                    Toast.LENGTH_SHORT).show();
                            break;
                        case SmsManager.RESULT_ERROR_NULL_PDU:
                            Toast.makeText(context, "Null PDU",
                                    Toast.LENGTH_SHORT).show();
                            break;
                        case SmsManager.RESULT_ERROR_RADIO_OFF:
                            Toast.makeText(context, "Radio off",
                                    Toast.LENGTH_SHORT).show();
                            break;
                    }
                }
            }, new IntentFilter(SENT));
   
            registerReceiver(new BroadcastReceiver(){
                @Override
                public void onReceive(Context arg0, Intent arg1) {
                    switch (getResultCode())
                    {
                        case Activity.RESULT_OK:
                            Toast.makeText(context, "SMS delivered",
                                    Toast.LENGTH_SHORT).show();
                            break;
                        case Activity.RESULT_CANCELED:
                            Toast.makeText(context, "SMS not delivered",
                                    Toast.LENGTH_SHORT).show();
                            break;                      
                    }
                }
            }, new IntentFilter(DELIVERED));
           
        SmsManager sms = SmsManager.getDefault();
        sms.sendTextMessage(phoneNumber, null, message, sentPi, deliveredPi);      
}


That's it for sending a sms on Android.

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;
        }

       
    }
}

Tuesday, September 27, 2011

How To Do Android C2DM Push Notifications Part 1

There are two ways to do message notification: a local pull notification service, or a server side push notification service.

This post is about doing push notifications (C2DM).

You need the following:
  • An email account that is authorized to send and recieve C2DM messages. C2DM is currently is beta; You can request an account here - http://code.google.com/android/c2dm/signup.html. (It is best to use a gmail account. I tried to use my own hosted account and it doesn't seem to work.)
  • An application that's already on the market.
  • A server that you can push messages to the C2DM servers.
We will use the following case to develop the C2DM notification.

We have a mobile membership app where users constantly send messages to each other. Users want to be notified on their phones whenever they get a message.

Each application on the phone can request C2DM tokens. Our server will send messages using these tokens. So each user id in our membership site will have a corresponding token (or multiple tokens when a user uses more than one device to access the app).

High Level Overview of How Everything Works together:

Token Registration:
  1. Device requests registration from a C2DM server.
  2. C2DM server sends back a token (or errors) to the device.
  3. Device sends the user id, and the token to our server.
  4. Our server saves the user id, token pair for future use.

Sending a messge:
  1. User A sends a message to user B in our application.
  2. Server pushes this message to the C2DM server. (Ex. Windows Service or a Cron job)
  3. The C2DM server will send the message to the corresponding device. 

Technical Implementation

Token Registration:

In your Android application, you will want to request a token and store it in a persistent storage, (preference object, or sql lite). If the token exists, you don't need to request it. The C2DM server can also renew or deactivate the token anytime. You will also need to have a broadcast Reciever to listen to these events. Note that if you recieve a 503, or other status that indicate server busy, you will need to implement exponential backoff to prevent yourself from getting banned by Google.

Android Manifest


<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission android:name="com.demo.android.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.demo.android.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<service android:name=".C2DMReceiver" />

<receiver android:name=".C2DMBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND">
    <!-- Receive the actual message -->
          <intent-filter>
              <action android:name="com.google.android.c2dm.intent.RECEIVE" />
              <category android:name="com.demo.android" />
          </intent-filter>
          <!-- Receive the registration id -->
          <intent-filter>
              <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
              <category android:name="com.demo.android" />
          </intent-filter>
</receiver>

Note that C2DM requires Android SDK 2.2 or above. You will want to replace com.demo.android to your app's package name.

Next we will define four classes:
  • C2DMBaseReceiver.java - handles token registration, receiving c2dm messages
  • C2DMBroadcastReceiver.java - listens to C2DM events and starts the C2DMReceiver intentService
  • C2DMmessaging.java - for storing registration id and related data in the shared preference object
  • C2DMreceiver.java - handles token registration, receiving c2dm messages on a higher level

C2DMBaseReceiver.java

package com.demo.android;

import java.io.IOException;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.util.Log;

/**
 * Base class for C2D message receiver. Includes constants for the strings used
 * in the protocol.
 */
public abstract class C2DMBaseReceiver extends IntentService {
    private static final String C2DM_RETRY = "com.google.android.c2dm.intent.RETRY";

    public static final String REGISTRATION_CALLBACK_INTENT = "com.google.android.c2dm.intent.REGISTRATION";
    private static final String C2DM_INTENT = "com.google.android.c2dm.intent.RECEIVE";

    // Logging tag
    private static final String TAG = "C2DM";

    // Extras in the registration callback intents.
    public static final String EXTRA_UNREGISTERED = "unregistered";

    public static final String EXTRA_ERROR = "error";

    public static final String EXTRA_REGISTRATION_ID = "registration_id";

    public static final String ERR_SERVICE_NOT_AVAILABLE = "SERVICE_NOT_AVAILABLE";
    public static final String ERR_ACCOUNT_MISSING = "ACCOUNT_MISSING";
    public static final String ERR_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
    public static final String ERR_TOO_MANY_REGISTRATIONS = "TOO_MANY_REGISTRATIONS";
    public static final String ERR_INVALID_PARAMETERS = "INVALID_PARAMETERS";
    public static final String ERR_INVALID_SENDER = "INVALID_SENDER";
    public static final String ERR_PHONE_REGISTRATION_ERROR = "PHONE_REGISTRATION_ERROR";

    // wakelock
    private static final String WAKELOCK_KEY = "C2DM_LIB";

    private static PowerManager.WakeLock mWakeLock;
    private final String senderId;


    public C2DMBaseReceiver(String senderId) {
        // senderId is used as base name for threads, etc.
        super(senderId);
        this.senderId = senderId;
    }

    /**
     * Called when a cloud message has been received.
     */
    protected abstract void onMessage(Context context, Intent intent);

    /**
     * Called on registration error. Override to provide better error messages.
     *
     * This is called in the context of a Service - no dialog or UI.
     */
    public abstract void onError(Context context, String errorId);

    /**
     * Called when a registration token has been received.
     */
    public void onRegistered(Context context, String registrationId)
            throws IOException {
        // registrationId will also be saved
    }

    /**
     * Called when the device has been unregistered.
     */
    public void onUnregistered(Context context) {
    }

    @Override
    public final void onHandleIntent(Intent intent) {
        try {
            Context context = getApplicationContext();
            if (intent.getAction().equals(REGISTRATION_CALLBACK_INTENT)) {
                handleRegistration(context, intent);
            } else if (intent.getAction().equals(C2DM_INTENT)) {
                onMessage(context, intent);
            } else if (intent.getAction().equals(C2DM_RETRY)) {
                C2DMMessaging.register(context, senderId);
            }
        } finally {
            // Release the power lock, so phone can get back to sleep.
            // The lock is reference counted by default, so multiple
            // messages are ok.

            // If the onMessage() needs to spawn a thread or do something else,
            // it should use it's own lock.
            mWakeLock.release();
        }
    }

    static void runIntentInService(Context context, Intent intent) {
        if (mWakeLock == null) {
            PowerManager pm = (PowerManager) context
                    .getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    WAKELOCK_KEY);
        }
        mWakeLock.acquire();

        String receiver = context.getPackageName() + ".C2DMReceiver";
        intent.setClassName(context, receiver);

        context.startService(intent);
    }

    private void handleRegistration(final Context context, Intent intent) {
        final String registrationId = intent
                .getStringExtra(EXTRA_REGISTRATION_ID);
        String error = intent.getStringExtra(EXTRA_ERROR);
        String removed = intent.getStringExtra(EXTRA_UNREGISTERED);

        if (removed != null) {
            // Remember we are unregistered
            C2DMMessaging.clearRegistrationId(context);
            onUnregistered(context);
            return;
        } else if (error != null) {
            // we are not registered, can try again
            C2DMMessaging.clearRegistrationId(context);
            // Registration failed
            //Log.e(TAG, "Registration error " + error);
            onError(context, error);
            if ("SERVICE_NOT_AVAILABLE".equals(error)) {
                long backoffTimeMs = C2DMMessaging.getBackoff(context);

                //Log.d(TAG, "Scheduling registration retry, backoff = " + backoffTimeMs);
                Intent retryIntent = new Intent(C2DM_RETRY);
                PendingIntent retryPIntent = PendingIntent
                        .getBroadcast(context, 0 /* requestCode */, retryIntent,
                                0 /* flags */);

                AlarmManager am = (AlarmManager) context
                        .getSystemService(Context.ALARM_SERVICE);
                am.set(AlarmManager.ELAPSED_REALTIME, backoffTimeMs,
                        retryPIntent);

                // Next retry should wait longer.
                backoffTimeMs *= 2;
                C2DMMessaging.setBackoff(context, backoffTimeMs);
            }
        } else {
            try {
                onRegistered(context, registrationId);
                C2DMMessaging.setRegistrationId(context, registrationId);
            } catch (IOException ex) {
                Log.e(TAG, "Registration error " + ex.getMessage());
            }
        }
    }
}



C2DMBroadcastReceiver.java

package com.demo.android;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class C2DMBroadcastReceiver extends BroadcastReceiver {
   
    @Override
    public final void onReceive(Context context, Intent intent) {
        C2DMBaseReceiver.runIntentInService(context, intent);
        setResult(Activity.RESULT_OK, null, null);       
    }
}


C2DMmessaging.java

package com.demo.android;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

public class C2DMMessaging {
    public static final String EXTRA_SENDER = "sender";
    public static final String EXTRA_APPLICATION_PENDING_INTENT = "app";
    public static final String REQUEST_UNREGISTRATION_INTENT = "com.google.android.c2dm.intent.UNREGISTER";
    public static final String REQUEST_REGISTRATION_INTENT = "com.google.android.c2dm.intent.REGISTER";
    public static final String LAST_REGISTRATION_CHANGE = "last_registration_change";
    public static final String BACKOFF = "backoff";

    // package
    static final String PREFERENCE = "com.google.android.c2dm";
   
    private static final long DEFAULT_BACKOFF = 30000;

    public static void register(Context context,
            String senderId) {
        Intent registrationIntent = new Intent(REQUEST_REGISTRATION_INTENT);
        registrationIntent.putExtra(EXTRA_APPLICATION_PENDING_INTENT,
                PendingIntent.getBroadcast(context, 0, new Intent(), 0));
        registrationIntent.putExtra(EXTRA_SENDER, senderId);
        context.startService(registrationIntent);
    }

    public static void unregister(Context context) {
        Intent regIntent = new Intent(REQUEST_UNREGISTRATION_INTENT);
        regIntent.putExtra(EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context,
                0, new Intent(), 0));
        context.startService(regIntent);
    }

    public static String getRegistrationId(Context context) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        String registrationId = prefs.getString("dm_registration", "");
        return registrationId;
    }

    public static long getLastRegistrationChange(Context context) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        return prefs.getLong(LAST_REGISTRATION_CHANGE, 0);
    }
   
    static long getBackoff(Context context) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        return prefs.getLong(BACKOFF, DEFAULT_BACKOFF);
    }
   
    static void setBackoff(Context context, long backoff) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        Editor editor = prefs.edit();
        editor.putLong(BACKOFF, backoff);
        editor.commit();
    }

    static void clearRegistrationId(Context context) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        Editor editor = prefs.edit();
        editor.putString("dm_registration", "");
        editor.putLong(LAST_REGISTRATION_CHANGE, System.currentTimeMillis());
        editor.commit();
    }

    static void setRegistrationId(Context context, String registrationId) {
        final SharedPreferences prefs = context.getSharedPreferences(
                PREFERENCE,
                Context.MODE_PRIVATE);
        Editor editor = prefs.edit();
        editor.putString("dm_registration", registrationId);
        editor.commit();
    }
}


C2DMreceiver.java

package com.demo.android;

import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

public class C2DMReceiver extends C2DMBaseReceiver {
    public C2DMReceiver() {
        // Email address currently not used by the C2DM Messaging framework
        super(YOUR_ROLE_ACCOUNT_EMAIL);
    }

    @Override
    public void onRegistered(Context context, String registrationId)
            throws java.io.IOException {
        Log.e("C2DM", "Registration ID arrived.");
        Log.e("C2DM", registrationId);

        if (registrationId != null) {
            // send user id and registration id (token) to server
        }
    }
   
    public void onUnregistered(Context context) {
        // unregister user id from server
    }

    @Override
    protected void onMessage(Context context, Intent intent) {
        // Extract the payload from the message
        Bundle extras = intent.getExtras();
        if (extras != null) {
            // this can be the number of unread messages
            String payload = (String) extras.get("payload");           
            NotificationUtil.showPushNotificationAlerts(context, payload);
        }
    }

    @Override
    public void onError(Context context, String errorId) {
        // error processing
    }
   
}

Note that YOUR_ROLE_ACCOUNT_EMAIL is the email you registered for C2DM.

For more information about error handling, you can visit http://code.google.com/android/c2dm/index.html.

This concludes the Android cilent side code. In Part 2 of this series, I will cover the server side implementation where you send out a message to the C2DM server.