Loading...

Blog

Latest blog posts

Google Cloud Messaging: Android and Ruby on Rails (part 2)

In this post we will continue developing the GCM Adroid Ruby application form the [previous minimal working example]. We want to improve it with:

  • When a device installs our client app, it will send its registration_id to our server. When we create a new notification we will send it to all the registered devices.
  • We will send text messages (message and title) that will be displayed as notifications in the device. The server will keep track of this notifications.

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

Server Models

Lets see what models we will need to store in the server

Device

We will create a Device model that will contain information about a device. It will have the following attributes:

  • id (UUID): Unique non modificable identifier of a device. We will use UUIDs.
  • registration_id (String): GCM registration id
Notes

A single device may update its registration_id. In order to store this changes we have added an unmodificable id (using UUIDs). We will use UUIDs so that an offline device can generate instances with id (maybe I will talk more about this in another post).

Notifications

We will store the notifications sent. It will have an String attribute with the title and another one for the message.

The RoR app

Lets create our rails app. In order to work with UUIDs we will use Postgresql database.

$ rails new gcm_sample2_server -d postgresql
$ cd gcm_sample2_server

Enable UUids

In order to use UUIDs we need to enable the uuid extension. To do so we need to create a migration

$ rails g migration EnableUuidOsspExtension
{% endhighlight%}
with the following contents

```ruby
class EnableUuidOsspExtension < ActiveRecord::Migration
  def change
     enable_extension 'uuid-ossp'
  end
end

We will execute this migration later.

Server Devices

We will use scaffold generator for model/cotroller/view generation. This may generate more features than the needed ones, but we can use them for testing porposes. Clean will be required if you are going to use this in a real project.

$ rails g scaffold Device registration_id:string

We will need to change the CreateDevices migration so that the id is a UUID. We will also set the registration_id as a database index for faster queries.

class CreateDevices < ActiveRecord::Migration
  def change
    create_table :devices, id: :uuid  do |t|
      t.string :registration_id
      t.timestamps null: false
    end
    add_index :devices, :registration_id
  end
end

Moreover, we will need to change the default controller (devices_controller.rb) so that it allows that on the create operation, to recieve a device id. By default this is not allowed. We will need to change the create function:

# POST /devices
# POST /devices.json
def create
  @device = Device.new(device_params)
  #we allow the id to be sent as a parameter when model created
  if device_params[:id]
    @device.id = device_params[:id]
  end

  ...
end

And the device_params function to allow the id as a parameter

def device_params
  params.require(:device).permit(:registration_id, :id)
end

Nothing more to change on Devices.

Server Notifications

Lets create Notifications; scaffold generation will be enough.

$ rails g scaffold Notification title:string message:string

GCM Messages

Finnaly, we need to create a GCM message each time a notification is created. We first need to configure RPush Gem.

We add the RPush Gem to our gemfile.

gem 'rpush'

And initialize it (rpush)

$ bundle install
$ rpush init

We will configure rpush for canonical-IDs, as descrived in the [Canonical-IDs rpus wiki]. To do so we will edit rpush.rb (in the initializers folder) and add (or uncomment) the following inside Rpush.reflect

Rpush.reflect do |on|
    (...)
    # Called when the GCM returns a canonical registration ID.
  # You will need to replace old_id with canonical_id in your records.
    on.gcm_canonical_id do |old_id, canonical_id|
      d = Device.find_by_registration_id(old_id)
      d.update_attributes(registration_id: canonical_id)
  end
    (...)
end

Now lets edit our notification controller so that when a notification is created, we send it to the existing devices. We will add the following private method to notifications_controller.rb that sends the GCM notifications to all the registered devices.

def send_gcm_notification notification
  if !Rpush::Gcm::App.find_by_name("android-gcm-sample2")
    app = Rpush::Gcm::App.new
    app.name = "android-gcm-sample2"
    app.auth_key = "UPDATE_WITH_YOUR_SERVER_KEY"
    app.connections = 1
    app.save!
  end

  n = Rpush::Gcm::Notification.new
  n.app = Rpush::Gcm::App.find_by_name("android-gcm-sample2")
  n.registration_ids = Device.all.map{|device| device.registration_id}
  message = notification.message
  n.data = notification.as_json
  n.save!
end

And update the create method to create the notifications

def create
  @notification = Notification.new(notification_params)

  respond_to do |format|
    if @notification.save
      send_gcm_notification @notification
      format.html { redirect_to @notification, notice: 'Notification was successfully created.' }
      format.json { render :show, status: :created, location: @notification }
    else
      format.html { render :new }
      format.json { render json: @notification.errors, status: :unprocessable_entity }
    end
  end
end

Last steps

We need to do some last steps to finish with our server app.

protectfromforgery

From our mobile application we will call the json api to create devices, therefore we will need to change the default protectfromforgery. You can read more about this in the [rails protectfromforgery definition]. To do so, we edit the application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

Start the server

Finally we need to build the database and start application. Note that db:create may fail if database.yml database credentials are not correct. Notice also that, in the rails s command we have added the "--b YOULOCALIP" (change by you local ip, e.g: 192.168.0.20). The reason for this is that our mobile device will need to connect to the rails server, therefore, we need to listen to our external ip.

$ rake db:create
$ rake db:migrate
$ rails s --b YOU_LOCAL_IP

Separatelly we will need to start also rpush, otherwise GCM messages will not be sent,

rpush start

Our server is finished and running.

Android client

Lets create the Android client. To do so, we will modify the [previous minimal working example].

Device storage and synchronization

For the data storage and synchronization we will use [Android Pillow]. [Android Pillow] is a library that will help us to store data locally and synchronize it to the server when possible. It's still in early development stages but fits perfect for our case. If you prefer using any other libary you will only need to update the Device controller.

Device Model

Lets create the Device model. Apart from the registrationId we will store also the appVersion (as we did in the [first post]).

/**
 * Stores information about a device GCM registration
 */
public class Device extends AbstractIdentificableModel {
    String registrationId;
    int appVersion;

    public Device(){}

    public Device(String registrationId, int appVersion) {
        this.registrationId = registrationId;
        this.appVersion = appVersion;
    }

    public String getRegistrationId() {
        return registrationId;
    }

    public void setRegistrationId(String registrationId) {
        this.registrationId = registrationId;
    }

    public int getAppVersion() {
        return appVersion;
    }

    public void setAppVersion(int appVersion) {
        this.appVersion = appVersion;
    }
}

Device Controller

The DeviceController will be in charge of the storage our device information locally and synchonize it to our server. A SynchSingleInstanceDataSource from [Android Pillow] will fit our needs.

/**
 * Device storage operations
 */
public class DeviceDataSource extends SynchSingleInstanceDataSource<Device> {
    public DeviceDataSource(ISynchLocalSingleInstanceDataSource<Device> dbSource, IRestMapping<Device> restMap, Context context) {
        super(Device.class, dbSource, restMap, context);
    }
}
Configurin Android Pillow

Lets configure Android Pillow to use this Controller. We will first add android_pillow.xml in res/xml. Update the url with you ip (server url).

<?xml version="1.0" encoding="utf-8"?>
<androidpillow xmlns:android="http://schemas.android.com/apk/res/android">
    <modelConfigurations class="com.trito.blog.gcmsample2client.pillow.PillowConfiguration"/>
    <url value="http://192.168.2.5:3000"/>
</androidpillow>

And create the PillowConfiguration

public class PillowConfiguration implements IModelConfigurations {
    @Override
    public List<ModelConfiguration<?>> getModelConfigurators(Context context, PillowConfigXml config) {
        String url = config.getUrl();
        List<ModelConfiguration<?>> configurations = new ArrayList<>();

        SharedPreferences preferences = context.getSharedPreferences("PILLOW_PREFERENCE_MODELS", Context.MODE_PRIVATE);
        //Device model configuration
        DefaultModelConfiguration<Device> deviceConfiguration = new DefaultModelConfiguration<Device>(context, Device.class, new TypeToken<Collection<Device>>(){}, url);
        SingleModelKeyValueDataSource<Device> deviceLocalDataSource = new SingleModelKeyValueDataSource<Device>(Device.class, preferences);
        deviceConfiguration.setDataSource(new DeviceDataSource(deviceLocalDataSource, deviceConfiguration.getRestMapping(), context));
        configurations.add(deviceConfiguration);

        return configurations;
    }
}

And initialize Pillow in the Application. Remember to set the new GcmSample2ClientApplication to the manifest.xml

public class GcmSample2ClientApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Pillow.setConfigurationFile(R.xml.android_pillow);
    }
}

GCM Registration

In the last example all our codes where in the MainActivity. Now we will separate the GCM functionallity to a class. We have modified the previous behaviour to use the DeviceDataSource to store the registration id.

/**
 * Helper class for Google cloud Message Registration.
 */
public class GCMRegistration {
    private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
    static final String TAG = "GCMRegistration";


    String senderId;
    Activity context;

    GoogleCloudMessaging gcm;
    DeviceDataSource deviceDataSource;
    String regid;

    public GCMRegistration(Activity context, String senderId) {
        this.context = context;
        this.senderId = senderId;
        deviceDataSource = (DeviceDataSource) Pillow.getInstance(context).getDataSource(Device.class);
    }

    /**
     * Checks if Google Play Services are found. If so, it checks if the device has been registered
     * with GCM if not, it registers and stored local & remote the information.
     */
    public void register(){
        if (checkPlayServices(context)) {
            gcm = GoogleCloudMessaging.getInstance(context);
            regid = getRegistrationId();

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

    /**
     * Registers the application with GCM servers asynchronously.
     * Stores the registration ID and app versionCode
     */
    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(senderId);
                    msg = "Device registered, registration ID=" + regid;

                    storeRegistrationId(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);
    }



    /**
     * Stores the registration ID and app versionCode
     *
     * @param regId registration ID
     */
    private void storeRegistrationId(String regId) {
        int appVersion = getAppVersion(context);
        Device device = new Device(regId, appVersion);
        try {
            deviceDataSource.set(device).get();
        } catch (PillowError pillowError) {
            throw new BreakFastException(pillowError);
        }
    }

    /**
     * Gets the current registration ID for application on GCM service.
     * 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() {
        Device device;
        try {
            device = deviceDataSource.get().get();
        } catch (PillowError pillowError) {
            throw new BreakFastException(pillowError);
        }
        if (device==null) {
            Log.i(TAG, "Registration not found.");
            return "";
        }

        int registeredVersion = device.getAppVersion();
        int currentVersion = getAppVersion(context);
        if (registeredVersion != currentVersion) {
            Log.i(TAG, "App version changed.");
            return "";
        }
        return device.getRegistrationId();
    }

    /**
     * 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.
     * If the error is unRecoverable it finishes the activity
     */
    public boolean checkPlayServices(Activity activity) {
        int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(activity);
        if (resultCode != ConnectionResult.SUCCESS) {
            if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
                GooglePlayServicesUtil.getErrorDialog(resultCode, activity, PLAY_SERVICES_RESOLUTION_REQUEST).show();
            } else {
                Log.i(TAG, "This device is not supported.");
                activity.finish();
            }
            return false;
        }
        return true;
    }

    /**
     * @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);
        }
    }
}

Now we have a much clean MainActivity

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;

    GCMRegistration gcmRegistration;

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

        gcmRegistration = new GCMRegistration(this, SENDER_ID);
        gcmRegistration.register();
    }

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

Final changes

We will update the GcmIntentService so that we obtain the title and message from the GCM message, and create a nofitication with this data.

@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(extras.getString("title"), extras.getString("message"));
            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 title, 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(title)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setStyle(new NotificationCompat.BigTextStyle().bigText(msg))
                    .setContentText(msg);

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

Thats all.

Showcase

Open the Android application. If you check in http://yourip:3000/devices a new device should be added. If so, create a new notification at http://yourip:3000/notifications/new and see that the notification is displayed in your Android application.

Common dummy errors

If somthing is not working you may have done one of this dummy errors

The devices are not created on the server

When you start your rails server remember to make it listen your local ip, not localhost

$ rails s --b YOU_LOCAL_IP

The notifications are not beeing sent to my device

A part from starting your rails server, remember to start rpush.

rpush start

If this is not the problem, check on you database to see if the notifications are sent.

Work To do

We have still some work to do

  • If our Android app, when registerInBackground fails, we should try to connect again (using an exponential back-off)

[previous minimal working example]:/blog/android/rubyonrails/gcm/2015/05/13/Google-CloudMessaging-with-Ruby-On-Rails.html [first post]:/blog/android/rubyonrails/gcm/2015/05/13/Google-CloudMessaging-with-Ruby-On-Rails.html [Canonical-IDs rpus wiki]:https://github.com/rpush/rpush/wiki/Canonical-IDs [rails protectfromforgery definition]:http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html [Android Pillow]:http://androidpillow.com [GitHub]:https://github.com/trito/bloggcmandrorpart2