Loading...

Blog

Latest blog posts

Google Cloud Messaging: Android and Ruby on Rails

In this entry we will create an RoR app that sends messages to an Android app using Google Cloud Messaging (GCM). We will keep the example as simple as possible to create a minimal working example. We recommend having a first look at the official GCM guide before starting this tutorial.

You can download from github the Android app and the RoR app from GitHub

Enable GCM

First of all we need to create a Google Api Project. We refer to the official GCM guide, getting started section We will need to create a project, with a server application and a client application. Once done we will have:

  • project id : Identifies the project in Google Api
  • server key : Key for the server application (RoR)
  • client key : Key for the client application (Android)

Create an Android app that listens to the notifications

We will create a new Android application using the sample code on official GCM guide, Android client section We have just simplified and cleaned it a little bit. Check there for more explanations.

First we need to add the following gradle dependency

compile 'com.google.android.gms:play-services:3.1.+'

Then add the following to your Manifest.xml. Replace com.mateuyabar.gcmsampleclient with your application package

<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" />
<permission android:name="com.mateuyabar.gcmsampleclient.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.mateuyabar.gcmsampleclient.permission.C2D_MESSAGE" />
<application >
    ...
    <receiver android:name=".GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
        <intent-filter>
            <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            <category android:name="com.mateuyabar.gcmsampleclient" />
        </intent-filter>
    </receiver>
    <service android:name=".GcmIntentService" />
</application>

Create a BroadcastReciever class GcmBroadcastReceiver.java

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // Explicitly specify that GcmIntentService will handle the intent.
        ComponentName comp = new ComponentName(context.getPackageName(),
                GcmIntentService.class.getName());
        // Start the service, keeping the device awake while it is launching.
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);
    }
}

And an IntentService that will process the received messages GcmIntentService.java

public class GcmIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    public static final String TAG = MainActivity.TAG;
    private NotificationManager mNotificationManager;

    public GcmIntentService() {
        super("GcmIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
        // The getMessageType() intent parameter must be the intent you received
        // in your BroadcastReceiver.
        String messageType = gcm.getMessageType(intent);

        if (!extras.isEmpty()) {  // has effect of unparcelling Bundle
            // If it's a regular GCM message, do some work.
            if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
                // Post notification of received message.
                sendNotification("Received: " + extras.toString());
                Log.i(TAG, "Received: " + extras.toString());
            }
        }
        // Release the wake lock provided by the WakefulBroadcastReceiver.
        GcmBroadcastReceiver.completeWakefulIntent(intent);
    }

    /**
     * Put the message into a notification and post it.
     * This is just one simple example of what you might choose to do with a GCM message.
     * @param msg
     */
    private void sendNotification(String msg) {
        mNotificationManager = (NotificationManager)
                this.getSystemService(Context.NOTIFICATION_SERVICE);

        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, MainActivity.class), 0);

        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(this)
        .setContentTitle("GCM Notification")
        .setStyle(new NotificationCompat.BigTextStyle()
        .bigText(msg))
        .setContentText(msg);

        mBuilder.setContentIntent(contentIntent);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
    }
}

Finally we change the MainActivity.java so that it registers to GCM. Update the SENDER_ID with the client key generated in the google console.

public class MainActivity extends AppCompatActivity {
    /**
     * Substitute you own sender ID here. This is the project number you got
     * from the API Console, as described in "Getting Started."
     */
    String SENDER_ID = UPDATE_WITH.SENDER_ID;


    public static final String PROPERTY_REG_ID = "registration_id";
    private static final String PROPERTY_APP_VERSION = "appVersion";
    private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

    /**
     * Tag used on log messages.
     */
    static final String TAG = "GCMSample";
    static final String TAG_ID = TAG+"-ID";


    GoogleCloudMessaging gcm;
    AtomicInteger msgId = new AtomicInteger();
    SharedPreferences prefs;
    Context context;

    String regid;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        context = getApplicationContext();

        // Check device for Play Services APK. If check succeeds, proceed with
        //  GCM registration.
        if (checkPlayServices()) {
            gcm = GoogleCloudMessaging.getInstance(this);
            regid = getRegistrationId(context);

            if (regid.isEmpty()) {
                registerInBackground();
            } else {
                Log.i(TAG_ID, regid);
            }
        } else {
            Log.i(TAG, "No valid Google Play Services APK found.");
        }
    }

    /**
     * Registers the application with GCM servers asynchronously.
     * <p>
     * Stores the registration ID and app versionCode in the application's
     * shared preferences.
     */
    private void registerInBackground() {
        new AsyncTask<Void, String, String>() {
            @Override
            protected String doInBackground(Void... params) {
                String msg = "";
                try {
                    if (gcm == null) {
                        gcm = GoogleCloudMessaging.getInstance(context);
                    }
                    regid = gcm.register(SENDER_ID);
                    msg = "Device registered, registration ID=" + regid;

                    // You should send the registration ID to your server over HTTP.
                    // For this demo: we will copy it manually
                    sendRegistrationIdToBackend();

                    // Persist the registration ID - no need to register again.
                    storeRegistrationId(context, regid);
                } catch (IOException ex) {
                    ex.printStackTrace();
                    msg = "Error :" + ex.getMessage();
                    // If there is an error, don't just keep trying to register.
                    // Require the user to click a button again, or perform
                    // exponential back-off.
                }
                return msg;
            }

            @Override
            protected void onPostExecute(String msg) {
                Log.i(TAG, msg);
            }
        }.execute(null, null, null);

    }

    /**
     * Sends the registration ID to your server over HTTP. To implement.
     * For this demo: we will copy it manually
     */
    private void sendRegistrationIdToBackend() {
        Log.i(TAG_ID, regid);
        // Your implementation here.
    }

    /**
     * Stores the registration ID and app versionCode in the application's
     * {@code SharedPreferences}.
     *
     * @param context application's context.
     * @param regId registration ID
     */
    private void storeRegistrationId(Context context, String regId) {
        final SharedPreferences prefs = getGCMPreferences(context);
        int appVersion = getAppVersion(context);
        Log.i(TAG, "Saving regId on app version " + appVersion);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(PROPERTY_REG_ID, regId);
        editor.putInt(PROPERTY_APP_VERSION, appVersion);
        editor.commit();
    }

    /**
     * Gets the current registration ID for application on GCM service.
     * <p>
     * If result is empty, the app needs to register.
     *
     * @return registration ID, or empty string if there is no existing
     *         registration ID.
     */
    private String getRegistrationId(Context context) {
        final SharedPreferences prefs = getGCMPreferences(context);
        String registrationId = prefs.getString(PROPERTY_REG_ID, "");
        if (registrationId.isEmpty()) {
            Log.i(TAG, "Registration not found.");
            return "";
        }
        // Check if app was updated; if so, it must clear the registration ID
        // since the existing registration ID is not guaranteed to work with
        // the new app version.
        int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
        int currentVersion = getAppVersion(context);
        if (registeredVersion != currentVersion) {
            Log.i(TAG, "App version changed.");
            return "";
        }
        return registrationId;
    }

    /**
     * @return Application's version code from the {@code PackageManager}.
     */
    private static int getAppVersion(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            // should never happen
            throw new RuntimeException("Could not get package name: " + e);
        }
    }

    /**
     * @return Application's {@code SharedPreferences}.
     */
    private SharedPreferences getGCMPreferences(Context context) {
        // This sample app persists the registration ID in shared preferences, but
        // how you store the registration ID in your app is up to you.
        return getSharedPreferences(MainActivity.class.getSimpleName(),
                Context.MODE_PRIVATE);
    }


    @Override
    protected void onResume() {
        super.onResume();
        // You need to do the Play Services APK check here too.
        checkPlayServices();
    }

    /**
     * Check the device to make sure it has the Google Play Services APK. If
     * it doesn't, display a dialog that allows users to download the APK from
     * the Google Play Store or enable it in the device's system settings.
     */
    private boolean checkPlayServices() {
        int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (resultCode != ConnectionResult.SUCCESS) {
            if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
                GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                        PLAY_SERVICES_RESOLUTION_REQUEST).show();
            } else {
                Log.i(TAG, "This device is not supported.");
                finish();
            }
            return false;
        }
        return true;
    }
}

We have our client finished. Let open run it. Notice that we are using Google services, so we will need that the emulator is a Google Apis device not an Android one. Once running check at the logs and look for an id printed with tag GCM-ID. This id is the Registration Id of the device. Copy it for latter use.

The RoR server

We will create a RoR application that when we access to /notifications/create it will create a new GCM message.

Lets create a new RoR app

$ rails new gcm_sample_server

We will use rpush gem for the communication with GCM. We need to add it to our Gemfile:

gem 'rpush'

We will need to initialize rpush (on the rpush init command indicate yes to all questions)

$ cd gcm_sample_server
$ bundle install
$ rpush init

We create a controller with its view that we will use to generate push notifications

rails generate controller notifications create

Lets edit the notificationscontroller.rb. You will need to set app.authkey with the server key. Moreover, paste the Registration Id (the id that we copied form the Android application logs) to the registrations_ids.

class NotificationsController < ApplicationController
  def create
    if !Rpush::Gcm::App.find_by_name("android-gcm-sample")
      app = Rpush::Gcm::App.new
      app.name = "android-gcm-sample"
      app.auth_key = "UPDATE_WITH_SERVER_KEY"
      app.connections = 1
      app.save!
    end

    n = Rpush::Gcm::Notification.new
    n.app = Rpush::Gcm::App.find_by_name("android-gcm-sample")
    n.registration_ids = ["UPDATE_WITH_DEVICE_REGISTRATION_ID"]
    message = Time.new.to_s
    n.data = { message: message }
    n.save!
  end
end

First version done. We will need to start the server

$ rails s

Moreover rpush runs in a separate process, so we will need to start the rpush separately.

$ cd path_to_project
$ rpush start

Showcase

Everything is up and running. Run the android application and open http://localhost:3000/notifications/create in your browser. Check your android application logs to see if the message is received. Moreover, if the Android app is not open, a notification will be shown. You can also try to send a GCM message when the emulator is closed and receive it when it opens.

What we have done

We have created a Rails sever app that sends messages to GCM. We have created an Android app that receives this messages.

What is left

  • First of all we have hard-coded the client registration id in the controller and this is not correct. When the client connects to GCM and receives its registration id, it should send it to the Rails app. So we should create a new model (Device) in the rails app that stores this ids, and uses them when creating notifications.
  • In our Rails app we have defined the Rpush app in our controller. This should be moved somewhere else.
  • If our Android app, when registerInBackground fails, we should try to connect again (using an exponential back-off)
  • All the GCM stuff should be moved out from the Activity to an specific class.

In the next post where we continue developing this example.