Create an Instant Messaging app using Google Cloud Messaging (GCM)
DownloadKeywords: Google Cloud Messaging Google Play services ContentProvider SQLiteOpenHelper BroadcastReceiver NotificationManager SimpleCursorAdapter CursorLoader ActionBar DialogFragment ListFragment ViewBinder ContentResolver PreferenceFragment Google App Engine Google Plugin JPA Servlet
Contents- Overview
- Integrating Google Play Services
- Create an API project
- Obtain an API Key
- Create a new Eclipse Android project
- The Android Manifest file
- The Application class
- The Data Model
- The Content Provider
- GCM Utility class
- Server Utility class
- The GCM BroadcastReceiver
- Main Activity
- Add Contact DialogFragment
- Messages ListFragment
- Chat Activity
- Settings screen
- Install the Google Plugin for Eclipse
- Create new Web Application Project
- JPA model
- Servlets
- Deploy to App Engine
- Testing the app
6. The Android Manifest file
The official documentation specifies few permissions required to use GCM for Android. Add the following lines to the manifest.<permission android:name="com.appsrox.instachat.permission.C2D_MESSAGE" android:protectionLevel="signature" /> <uses-permission android:name="com.appsrox.instachat.permission.C2D_MESSAGE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />Notice that we create a new permission com.appsrox.instachat.permission.C2D_MESSAGE. Do not forget to change the package name if you use a different package for your application.
Next add a <receiver> to the <application> element to register a BroadcastReceiver to receive messages sent by the GCM Framework. Notice that we set the category as our application package in <intent-filter> for the receiver.
<application android:name=".Common" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name=".client.GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" > <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> <category android:name="com.appsrox.instachat" /> </intent-filter> </receiver> </application>You may optionally create an intent service to handle the intents received by the broadcast receiver. However, we choose to handle the intent in the receiver itself for creating notifications. We'll implement the BroadcastReceiver later.
7. The Application class
This Application class gets instantiated first before all other components in the application. So its a good place to declare globals in an application although it's not mandatory. In manifest we specified the Application class by giving a value to android:name attribute of the <application> element.To use it create a class that extends Application and override its onCreate() method.
public class Common extends Application { public static String[] email_arr; private static SharedPreferences prefs; @Override public void onCreate() { super.onCreate(); prefs = PreferenceManager.getDefaultSharedPreferences(this); List<String> emailList = getEmailList(); email_arr = emailList.toArray(new String[emailList.size()]); } private List<String> getEmailList() { List<String> lst = new ArrayList<String>(); Account[] accounts = AccountManager.get(this).getAccounts(); for (Account account : accounts) { if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { lst.add(account.name); } } return lst; } }We add an utility method to get a list of all accounts on the device. This requires android.permission.GET_ACCOUNTS permission which we have already declared in the manifest. The user can choose one of these email addresses in the Settings screen to be used as the chat ID.
Later we'll add a few public getter methods to this class when we develop the Settings screen so that it's a common place to access application-wide preferences.
8. The Data model
The data model of the application comprises of two tables. The Profile table holds the user's name, chat email ID, and count of new messages. The Messages table holds all messages sent or received by the application along with time. Android provides SQLite database for data persistence. We'll next implement a ContentProvider to interact with the database.9. The Content Provider
ContentProvider provides a standard interface to manage access to a structured set of data. It should be registered in the manifest just like any other application component.<provider android:name=".DataProvider" android:authorities="com.appsrox.instachat.provider" android:exported="false" > </provider>Add the above snippet to the manifest and create a class DataProvider that extends ContentProvider. We'll have to implement few methods since ContentProvider is an abstract class.
public class DataProvider extends ContentProvider { public static final String COL_ID = "_id"; public static final String TABLE_MESSAGES = "messages"; public static final String COL_MSG = "msg"; public static final String COL_FROM = "email"; public static final String COL_TO = "email2"; public static final String COL_AT = "at"; public static final String TABLE_PROFILE = "profile"; public static final String COL_NAME = "name"; public static final String COL_EMAIL = "email"; public static final String COL_COUNT = "count"; private DbHelper dbHelper; @Override public boolean onCreate() { dbHelper = new DbHelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }We take help of SQLiteOpenHelper class provided by Android for managing the database. Create a nested DbHelper class that extends SQLiteOpenHelper.
private static class DbHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "instachat.db"; private static final int DATABASE_VERSION = 1; public DbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table messages (_id integer primary key autoincrement, msg text, email text, email2 text, at datetime default current_timestamp);"); db.execSQL("create table profile (_id integer primary key autoincrement, name text, email text unique, count integer default 0);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }Add the following code to the DataProvider class.
public static final Uri CONTENT_URI_MESSAGES = Uri.parse("content://com.appsrox.instachat.provider/messages"); public static final Uri CONTENT_URI_PROFILE = Uri.parse("content://com.appsrox.instachat.provider/profile"); private static final int MESSAGES_ALLROWS = 1; private static final int MESSAGES_SINGLE_ROW = 2; private static final int PROFILE_ALLROWS = 3; private static final int PROFILE_SINGLE_ROW = 4; private static final UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("com.appsrox.instachat.provider", "messages", MESSAGES_ALLROWS); uriMatcher.addURI("com.appsrox.instachat.provider", "messages/#", MESSAGES_SINGLE_ROW); uriMatcher.addURI("com.appsrox.instachat.provider", "profile", PROFILE_ALLROWS); uriMatcher.addURI("com.appsrox.instachat.provider", "profile/#", PROFILE_SINGLE_ROW); }Next, implement each overridden method of ContentProvider where we make use of UriMatcher to match the Uris passed to the methods.
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = dbHelper.getReadableDatabase(); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); switch(uriMatcher.match(uri)) { case MESSAGES_ALLROWS: case PROFILE_ALLROWS: qb.setTables(getTableName(uri)); break; case MESSAGES_SINGLE_ROW: case PROFILE_SINGLE_ROW: qb.setTables(getTableName(uri)); qb.appendWhere("_id = " + uri.getLastPathSegment()); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = dbHelper.getWritableDatabase(); long id; switch(uriMatcher.match(uri)) { case MESSAGES_ALLROWS: id = db.insertOrThrow(TABLE_MESSAGES, null, values); if (values.get(COL_TO) == null) { db.execSQL("update profile set count=count+1 where email = ?", new Object[]{values.get(COL_FROM)}); getContext().getContentResolver().notifyChange(CONTENT_URI_PROFILE, null); } break; case PROFILE_ALLROWS: id = db.insertOrThrow(TABLE_PROFILE, null, values); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } Uri insertUri = ContentUris.withAppendedId(uri, id); getContext().getContentResolver().notifyChange(insertUri, null); return insertUri; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch(uriMatcher.match(uri)) { case MESSAGES_ALLROWS: case PROFILE_ALLROWS: count = db.update(getTableName(uri), values, selection, selectionArgs); break; case MESSAGES_SINGLE_ROW: case PROFILE_SINGLE_ROW: count = db.update(getTableName(uri), values, "_id = ?", new String[]{uri.getLastPathSegment()}); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch(uriMatcher.match(uri)) { case MESSAGES_ALLROWS: case PROFILE_ALLROWS: count = db.delete(getTableName(uri), selection, selectionArgs); break; case MESSAGES_SINGLE_ROW: case PROFILE_SINGLE_ROW: count = db.delete(getTableName(uri), "_id = ?", new String[]{uri.getLastPathSegment()}); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } private String getTableName(Uri uri) { switch(uriMatcher.match(uri)) { case MESSAGES_ALLROWS: case MESSAGES_SINGLE_ROW: return TABLE_MESSAGES; case PROFILE_ALLROWS: case PROFILE_SINGLE_ROW: return TABLE_PROFILE; } return null; }
At this point you may want to get the latest source code by installing the app from Google Play and extract it from the Settings screen.