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.

Wednesday, May 25, 2011

Basic calls for getting the location data from Android

The following is some basic calls to the GPS to retreive either the WIFI or cellular locations. The following code is NOT battery optimized, but serve as the starting point for location retreival

LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
updateLocation(location);  // define your own function here
}

public void onStatusChanged(String provider, int status, Bundle extras) {
Log.v(TAG, "onStatusChanged");
}

public void onProviderEnabled(String provider) {
Log.v(TAG, "onProviderEnabled");
}

public void onProviderDisabled(String provider) {
Log.v(TAG, "onProviderDisabled");
}
};

LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
Constant.GPS_REQUEST_TIME, 0, locationListener);
} else {
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
Constant.GPS_REQUEST_TIME, 0, locationListener);
}


The location object will give you access to the longtitude and latitude.

Friday, May 13, 2011

Android - a rounded corner background panel

This example demonstrates how you can create an image by xml declaration. You will use the <shape> xml object and this file will need to be put in the image directories. The following example shows how to build a red oval panel and a white box panel.


drawable-mdpi/white_box.xml - white rounded corner box

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF"/>
    <corners android:radius="5px"/>
    <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

drawable-mdpi/red_box.xml - red oval shaped box

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FF0000"/>
    <corners android:radius="20px"/>
    <padding android:left="5dp" android:top="0dp" android:right="5dp" android:bottom="0dp" />
</shape>

res/values/styles.xml


We will create a customStyle to use these panels.


<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="customStyle">
        <item name="android:textColor">#ffffff</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">12dip</item>
        <item name="android:gravity">center</item>
        <item name="android:background">@drawable/red_box</item>
  </style>
</resources>

Using the white box panel

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<TextView android:id="@+id/tv" android:layout_width="wrap_content" style="@style/customStyle"
android:layout_height="wrap_content" android:layout_margin="50dip" android:text="demo text"/>
</RelativeLayout>

Friday, May 6, 2011

Android - How to add a header and a footer to listView

In the following example we add a previous button in the header, next button in the footer.


Activity Code:

ListView list = getListView();
View headerView = View.inflate(this, R.layout.header, null);
list.addHeaderView(headerView);
View footerView = View.inflate(this, R.layout.footer, null);
list.addFooterView(footerView);


Header XML Layout - header.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:padding="5dip">

<Button android:id="@+id/previous"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />

</RelativeLayout>


Footer XML Layout - footer.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:padding="5dip">

<Button android:id="@+id/next"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />

</RelativeLayout>

Thursday, May 5, 2011

Android - How to Boot a Service When You Turn on the Phone

A typical usage for booting services when a phone starts is a message pull notification system. In the following, I have written a BoardcastReceiver to listen to the system boot event.


First, Create a BroadcastReciever:

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

public class ActivateAtBootReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent)
{
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {

Intent i = new Intent();
i.setAction("com.pof.android.MessageService");
context.startService(i);

}
}

}

In AndroidManifest.xml,

Add the following permission inside the manifest tag:

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


Add the following to inside the application tag:


<service android:name=".MessageService"
android:process=":MessageService"
android:label="Message Notification">
<intent-filter>
<action android:name="com.pof.android.MessageService">
</action>
</intent-filter>
</service>

<receiver android:name=".ActivateAtBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED">
</action>
<category android:name="android.intent.category.HOME">
</category>
</intent-filter>
</receiver>

Note: MessageService is created by me. You will want to replace that with your own service.

Android - How to Hide a Button Dynamically

The following shows how to hide the view image button when a user's profile has no images.

Button viewImagesButton = (Button) findViewById(R.id.view_images_button);
if(images.size() <= 0) {
viewImagesButton.setVisibility(android.view.View.INVISIBLE);
}

Similarily you can show the button by:

viewImagesButton.setVisibility(android.view.View.VISIBLE);