Montag, 2. Dezember 2013

Android Push/GCM - A very simple sample

Mobile applications often get even more interesting, if there is some exchange with a server application. The usual request-response-model looks as follows: the client – the app in this case – sends a request to the server and receives a response – immediately and synchronously.


In many cases this is enough. But what does it look like, if interesting things happen on the server and the client has to be informed about it?


Regarding the client to be responsible to fetch the data, the client could be implemented to poll, i.e. ask the server regularly, if there is something new. The disadvantages are evident: if the interval is to small, unnecessary communication will happen in many cases, if the interval is to big, the information might be „not recent enough“.
So, there has to be a possibility to access the mobile devices from the server side. Finally the distributors of the mobile systems have to be responsible to do this, since only they have the information to access the devices. The options to notify the device via call or short message are not fine-grained enough, since they reach the whole device and not one single application. Therefore the providers provide so called „Push“ services, which allow to access the applications from the server side. The responsibilty to transmit the information is transfered to the server. For iPhones of course Apple provides the appropriate service. For Android the „Google Cloud Messaging“ (GCM) is a part of the so-called „Google Play Services“. These further contain other interfaces to Google services, e.g. Google Maps, Google+ or the In-App-Payment.


The mobile application has to register as a potential receiver for push messages (1, 2) and provide the connection data to the server component (3). Using the identification of the client, the server is able to notify a relevant information (4) to the push service (5). This is responsible for submitting it soon to the mobile device (6).
To demonstrate this topic as simple as possible, I will present an app that transfers its communication data directly to the GCM service. This example only makes sense for demonstration purposes. At least I can't imagine an application that could reasonably notify itself via the Push service. At most the notification of the app on another device would be a possibility. But from an architectual point of view this would simply replace the server component in the case described above.


To gain further information there is an useful „getting startted“ tutorial available on the Google website (http://developer.android.com/google/gcm/index.html). I will restrict myself to the pure Android application.

Preparation: Google Console

In the Google Cloud Console „https://cloud.google.com/console“ there has to be created a (new) project. This one has to set under „APIs & auth / APIs“ the „Google Cloud Messaging for Android“ to „ON“. The „Project Number“ should be noted, we need it later.
A ServerKey is necessary to secure accessing the GCM service. If it is not already available (to be found at „Registered apps“ as „ServerKey“), you have to generate it. This step is quite difficult in the Google Cloud Console, as you need a SHA fingerprint – described in the tutorial. But you still have the possibility to use the „old“ Google Apis Console. („https://code.google.com/apis/console/“), where you are able to generate a ServerKey by pressing a button (at „API Access“, „Create new Server key“). The Google Apis Console tries to redirect you to the Google Cloud Console. Actually – November 2013 – it is possible to call the old Console via „Dismiss“. If this way is not available anymore, you have to do generate the key as described in the tutorial.
The ServerKey is transferred to the GCM later. The Key is the one propagated at „API KEY“ and may be copied as text. The Key should be active and „Any IP address is allowed“ should be displayed.

Preparation: Development environment

The GCM is part of the „Google Play Services“. Those have to be installed in the „Android SDK Manager“. You find them in the folder „Extras“.
To use the library in Eclipse, it has to be imported into the workspace. Just use the „File > Import“ dialog to choose „Android > Existing Android Code Into Workspace“ and enter the root folder and „Copy Projects Into Workspace“. The root folder for the Google Play Library Project is „/extras/google/google_play_services/libproject/google-play-services_lib“. To use other environments than eclipse, please consult the GCM tutorial.
For the app to be created, a new android project is created, using the Google Play Library. Therefore in the properties of the new project at „Android“ under „Library“ add the „google-play-services_lib“ project.

Preparation: Architecture

We create an app, consisting of a single activity. This just contains a button, to trigger the sending of the push message. To receive the push message, there has to be implemented a BroadcastReceiver.
In the Manifest file some permissions need to be defined:

<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="de.kluck.push.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

<uses-permission android:name="de.kluck.push.permission.C2D_MESSAGE" />
For the use of the latest version of the Google Play Library inside of the application tag the following entry is needed:

<meta-data android:name="com.google.android.gms.version" 
 android:value="@integer/google_play_services_version" />

Activity first try – very simple

Two steps are necessary at least: registering the App at GCM and triggering the push message.
For the most simple case we use an Activity containing 2 Buttons, that trigger exactly these operations. We use the button with the id „registerButton“ for registering and querying the registration id. The registration id defines, for which App the device should be accessed. Since it is an external service access, it has to be done in an AsyncTask. We need the PROJECT_NUMBER for this access. This is the one, that is shown in the Google Cloud Console – as mentioned above. So, the device registers itself for exactly this project:

String PROJECT_NUMBER = "...";
Context context;
String regid;

...

context = getApplicationContext();
View registerButton = findViewById(R.id.registerButton);
registerButton.setOnClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
  new AsyncTask() {
   @Override
   protected String doInBackground(String... arg0) {
    String regId = null;
    try {
     String projectNumber = arg0[0];
         GoogleCloudMessaging gcm = 
      GoogleCloudMessaging.getInstance(context);
         regId = gcm.register(projectNumber);
              } catch (IOException e) {
     Log.e(TAG, "Error on register", e);
              }
    return regId;
   }
   @Override
   protected void onPostExecute(String result) {
    super.onPostExecute(result);
    regid = result;
   }
  }.execute(PROJECT_NUMBER);
     }
});
As trigger for the „real“ functionality we use a button with the id „pushButton“. Once again the call is done in a AsyncTask, that is propagated the registration id of the device, besides the API-Key of the application – received in the Google Cloud resp. Google Apis Console as described above. The latter will be hooked as Authorization Key to the HttpConnection. This should make sure, that only permitted access is possible. The RegistrationId is part of the content of the request. Besides the possibility to put the load data in an JSON string, there is the possibility to put the data right into the URI. This is not as flexible, espacially you are only able to access a single RegistrationId. In other words: you are able to give a list of RegistrationIds and the assigned devices are accessed in parallel. The „data“ part of the JSON object is not flexible at will, finally it is just a list of key value pairs.

View pushButton = findViewById(R.id.pushButton);
pushButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        new AsyncTask() {
            @Override
                protected Void doInBackground(String... arg0) {
                    try {
                        String apiKey = arg0[0];
                        String regId = arg0[1];
                        URL url =
                            new URL("https://android.googleapis.com/gcm/send");
                        HttpURLConnection connection =
                            (HttpURLConnection)url.openConnection();
                        connection.setDoOutput(true);
                        connection.setRequestMethod("POST");
                        connection.setRequestProperty("Authorization",
                            "key=" + apiKey);
                        connection.setRequestProperty("Content-Type",
                            "application/json");
                        OutputStreamWriter writer =
                            new OutputStreamWriter(
                                connection.getOutputStream());
                        writer.write("{\"registration_ids\" : [\"" + regId +
                            "\"],\"data\" : { \"action\" : \"push\"}}");
                        writer.close();

                        int responseCode = connection.getResponseCode();
                        Log.i(TAG, "responseCode: " + responseCode);
                    } catch (Exception e) {
                        Log.e(TAG, "Error on service call", e);
                    }
                    return null;
                }
            }.execute(API_KEY, regid);
        }
    });
}

BroadcastReceiver

The BroadcastReceiver to receive the push messages is defined in the Manifest file with the appropriate rights and intent-filter:

<receiver
    android:name="de.kluck.push.GcmBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <category android:name="de.kluck.push" />
    </intent-filter>
</receiver>
The implementation in this example only „toasts“ the „action“ content of the Intent Extra:

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Result: " +
            intent.getStringExtra("action"), Toast.LENGTH_SHORT).show();
        setResultCode(Activity.RESULT_OK);
    }
}
The key value pairs of the JSON „data“ part of the HTTP request mentioned above are found in the Extras of the Intent. So you have the possibility to transfer data from the server to the device. Quantity and structure are restricted, so in many cases it will be implemented, so that for example just an id is transfered via push and the complex data is fetched via the „normal“ request-response access.

Activity second try – a little bit more clean

At the example above two things are disturbing: at first the GooglePlayServices are regarded as existing by the App. Since they are services that have to be installed seperately, the existence should be checked, before they are used. Secondly the registration of the App has to be done explicitely via pushing the button. A „real“ application should to this transparently in the background and since it is an external call, this should not be done more often then absolutely necessary.
To check if the availability of the GooglePlayServices on the device, a GooglePlayServicesUtil is available, practically containing methods for the output of the appropriate error dialogs:

private static final String TAG = "MainActivity";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

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;
}
Only in the case of successfully passing the test, the RegistrationId of the device can be requested via the GoogleCloudMessaging. The RegistrationId finally fixes, which App should be accessed on the device:

private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";

String PROJECT_NUMBER = "...";
String API_KEY = "...";

GoogleCloudMessaging gcm;
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 (checkPlayServices()) {
        //  Proceed with GCM registration.
        gcm = GoogleCloudMessaging.getInstance(this);
        regid = getRegistrationId(context);

        if (regid.isEmpty()) {
            registerInBackground();
        }
    } else {
        Log.i(TAG, "No valid Google Play Services APK found.");
    }
}
According to the GCM tutorial and with the background, that the access of the GCM service should not be done with every start of the App, the RegistrationId is stored in the SharedPreferences of the app and only requested again if necessary. Namely if the app was changed significantly.

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

private SharedPreferences getGCMPreferences(Context context) {
    return getSharedPreferences(MainActivity.class.getSimpleName(),
        Context.MODE_PRIVATE);
}

private static int getAppVersion(Context context) {
    try {
        PackageInfo packageInfo = context.getPackageManager()
            .getPackageInfo(context.getPackageName(), 0);
        return packageInfo.versionCode;
    } catch (NameNotFoundException e) {
        throw new RuntimeException("Could not get package name: " + e);
    }
}
Only if no or no actual RegistrationId is stored in the SharedPreferences, the service has to be accessed - using an asynchronous access, of course:

private void registerInBackground() {
    new AsyncTask() {
        @Override
        protected String doInBackground(Void... params) {
            String msg = "";
            try {
                if (gcm == null) {
                    gcm = GoogleCloudMessaging.getInstance(context);
                }
                regid = gcm.register(PROJECT_NUMBER);
                msg = "Device registered, registration ID=" + regid;

                storeRegistrationId(context, regid);
            } catch (IOException ex) {
                msg = "Error :" + ex.getMessage();
            }
            return msg;
        }

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

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();
}
That was the administrative overhead. To really be clean, you should check the PlayServices in the onResume method of the activity, just to be sure, that the have not been unistalled meanwhile:

@Override
protected void onResume() {
    super.onResume();
    checkPlayServices();
}

Testing the App

The given app is not able to be executed in a virtual device and has to be tested on a real device. After the button is pressed there should relatively soon the Toast with the message be seen – not very exciting.

Summary

As already mentioned above, this example is unrealistic because of the lack of a server component. I hope to have demonstrated in a nutshell, what is necessary to use the push service in the App. The relevant parts of the server component are restricted to accessing an HTTP service. This has to happen analoguous to the access inside of the App.

1 Kommentar:

  1. Impressive and useful coding so, i want

    Please send code me on , gjsheladiait@gmail.com

    Thanks, God Bless You!!!

    AntwortenLöschen