package com.charlesmadere.android.classygames.gcm; import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.graphics.Color; import android.os.Bundle; import android.os.Vibrator; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.TaskStackBuilder; import android.text.Html; import android.util.Log; import com.charlesmadere.android.classygames.GameFragmentActivity; import com.charlesmadere.android.classygames.GameOverActivity; import com.charlesmadere.android.classygames.R; import com.charlesmadere.android.classygames.models.Notification; import com.charlesmadere.android.classygames.models.Person; import com.charlesmadere.android.classygames.server.Server; import com.charlesmadere.android.classygames.utilities.Utilities; import com.google.android.gms.gcm.GoogleCloudMessaging; import org.json.JSONException; import org.json.JSONObject; import java.util.*; /** * This code is largely taken from the Android GCM documentation: * https://developer.android.com/google/gcm/client.html#sample-receive */ public final class GCMIntentService extends IntentService { private final static String LOG_TAG = Utilities.LOG_TAG + " - GCMIntentService"; private final static String PREFERENCES_NAME = "GCMIntentService_Preferences"; private final static int GCM_MAX_SIMULTANEOUS_NOTIFICATIONS = 6; private final static int GCM_NOTIFICATION_ID = 0; private final static int GCM_NOTIFICATION_LIGHTS_DURATION_ON = 1024; // milliseconds private final static int GCM_NOTIFICATION_LIGHTS_DURATION_OFF = 15360; // milliseconds private final static int GCM_NOTIFICATION_VIBRATION_DURATION = 160; // milliseconds public GCMIntentService() { // I realize that this constructor looks goofy, but it's how the // Google documentation does it! super("GCMIntentService"); } @Override protected void onHandleIntent(final Intent intent) { final GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this); final Bundle data = intent.getExtras(); if (data == null || data.isEmpty()) { Log.i(LOG_TAG, "received GCM that is devoid of data"); } else { // Filter messages based on message type. Since it is likely that // GCM will be extended in the future with new message types, just // ignore any any message types you're not interested in, or that // you don't recognize. final String messageType = gcm.getMessageType(intent); if (Utilities.validString(messageType) && (!messageType.equalsIgnoreCase(GoogleCloudMessaging.MESSAGE_TYPE_DELETED) && !messageType.equalsIgnoreCase(GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR))) { handleMessage(data); } else { Log.i(LOG_TAG, "received GCM with a messageType we don't care about: \"" + messageType + "\""); } } // Release the wake lock provided by the WakefulBroadcastReceiver. GCMBroadcastReceiver.completeWakefulIntent(intent); } /** * Processes a received push notification. Once this method has completed, * a notification will be shown on the Android device's notification bar. * If a notification is not showing, then that means that this method * detected some sort of error with the received push notification's data. * In that case, the fact that an error occurred will be Log.e'd. * * @param data * The Bundle object as received from the getExtras() method of the Intent * given in this class's onHandleIntent() method. */ private void handleMessage(final Bundle data) { // Retrieve input parameters. These input parameters determine what // type of push notification has been received, as well as who sent us // this notification. // All push notifications must have a particular game that they refer to. // This is that game's ID. final String parameter_gameId = data.getString(Server.POST_DATA_GAME_ID); // This is which game the push notification is referring to. It can be // checkers, chess... final String parameter_gameType = data.getString(Server.POST_DATA_GAME_TYPE); // The type of message that this is. Could be new game, new move, game over // lose, or game over win. final String parameter_messageType = data.getString(Server.POST_DATA_MESSAGE_TYPE); // The Facebook friend information of the guy that triggered this push // notification. final String parameter_personId = data.getString(Server.POST_DATA_ID); final String parameter_personName = data.getString(Server.POST_DATA_NAME); if (Utilities.validString(parameter_gameId, parameter_gameType, parameter_personId, parameter_messageType, parameter_personName)) // Verify that all of these Strings are both not null and that their // length is greater than or equal to 1. This way we ensure that all of // this input data is not corrupt. { final byte whichGame = Byte.parseByte(parameter_gameType); final byte messageType = Byte.parseByte(parameter_messageType); final long personId = Long.parseLong(parameter_personId); if (Person.isIdValid(personId) && Person.isNameValid(parameter_personName) && (Server.validGameTypeValue(whichGame) || Server.validMessageTypeValue(messageType))) { final Person person = new Person(personId, parameter_personName); final Notification notification = new Notification(parameter_gameId, whichGame, messageType, person); handleVerifiedMessage(notification); } else { Log.e(LOG_TAG, "Received partially malformed GCM message!"); } } else { Log.e(LOG_TAG, "Received completely malformed GCM message!"); } } /** * Further acts upon a push notification message as received from the * Classy Games server. * * @param notification * The bundled up notification data as received from this class's incoming * Intent object. */ private void handleVerifiedMessage(final Notification notification) { // begin building a notification to show to the user final Builder builder = new Builder(this) .setAutoCancel(true) .setContentTitle(getString(R.string.classy_games)) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_large)) .setSmallIcon(R.drawable.notification_small); final LinkedList<Notification> existingNotifications = saveCurrentNotificationAndGrabExisting(notification); if (existingNotifications == null || existingNotifications.size() <= 1) // We just looked into the cache of notifications and came out with // either just 1 or none. So we're going to show the simple, standard // Android notification. { if (notification.isMessageTypeNewGame() || notification.isMessageTypeNewMove()) // Check to see if the type of the received push notification is either // a new game or a new move. { handleNewGameOrNewMoveMessage(builder, notification); } else if (notification.isMessageTypeGameOverLose() || notification.isMessageTypeGameOverWin()) // Check to see if the type of the received push notification is either // a game loss or a game won. { handleWinOrLoseMessage(builder, notification); } else // The received message was of a type that doesn't make any sense. Log // it as an error. { Log.e(LOG_TAG, "Received GCM message that contained an unknown message type of \"" + notification.getMessageType() + "\"."); } } else // The notification cache has more than 1 notifications in it. So we're // going to show the nifty, multi-line Android notification. { final int notificationSize = existingNotifications.size(); final String summaryText = getResources().getQuantityString(R.plurals.x_game_notifications, notificationSize, notificationSize); final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle() .setBigContentTitle(getString(R.string.classy_games)) .setSummaryText(summaryText); for (final Notification existingNotification : existingNotifications) { final String inboxLine = "<b>" + existingNotification.getPerson().getName() + "</b> " + existingNotification.getReadableMessageType(this); inboxStyle.addLine(Html.fromHtml(inboxLine)); } builder.setContentText(summaryText) .setStyle(inboxStyle); if (notification.isMessageTypeGameOverLose() || notification.isMessageTypeGameOverWin()) { builder.setTicker(getString(R.string.game_with_x_is_now_over, notification.getPerson().getName())); } else if (notification.isMessageTypeNewGame() || notification.isMessageTypeNewMove()) { builder.setTicker(getString(R.string.ol_x_sent_you_some_class, notification.getPerson().getName())); } final Intent gameIntent = new Intent(this, GameFragmentActivity.class) .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); final TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addNextIntentWithParentStack(gameIntent); buildAndShowNotification(builder, stackBuilder); } } /** * Handles building an Android notification that represents a new game or * a new move. * * @param builder * A partially built notification Builder object. * * @param notification * The bundled up notification data as received from this class's incoming * Intent object. */ private void handleNewGameOrNewMoveMessage(final Builder builder, final Notification notification) { builder.setTicker(getString(R.string.ol_x_sent_you_some_class, notification.getPerson().getName())); if (notification.isMessageTypeNewGame()) { builder.setContentText(getString(R.string.new_game_from_x, notification.getPerson().getName())); } else if (notification.isMessageTypeNewMove()) { builder.setContentText(getString(R.string.new_move_from_x, notification.getPerson().getName())); } final Intent gameIntent = new Intent(this, GameFragmentActivity.class) .putExtra(GameFragmentActivity.KEY_NOTIFICATION, notification) .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); final TaskStackBuilder stackBuilder = TaskStackBuilder.create(this) .addNextIntentWithParentStack(gameIntent); buildAndShowNotification(builder, stackBuilder); } /** * Handles building an Android notification that represents a won or a lost * game. * * @param builder * A partially built notification Builder object. * * @param notification * The bundled up notification data as received from this class's incoming * Intent object. */ private void handleWinOrLoseMessage(final Builder builder, final Notification notification) { builder.setTicker(getString(R.string.game_with_x_is_now_over, notification.getPerson().getName())); if (notification.isMessageTypeGameOverLose()) { builder.setContentText(getString(R.string.you_lost_the_game_with_x, notification.getPerson().getName())); } else if (notification.isMessageTypeGameOverWin()) { builder.setContentText(getString(R.string.you_won_the_game_with_x, notification.getPerson().getName())); } final Bundle extras = new Bundle(); extras.putSerializable(GameOverActivity.KEY_NOTIFICATION, notification); final Intent gameOverIntent = new Intent(this, GameOverActivity.class) .putExtras(extras); final TaskStackBuilder stackBuilder = TaskStackBuilder.create(this) .addNextIntentWithParentStack(gameOverIntent); buildAndShowNotification(builder, stackBuilder); } /** * This method will finalize and, finally, show a notification in the * Android device's notification bar. * * @param builder * A completely built notification Builder object. * * @param stackBuilder * A TaskStackBuilder object that is completely prepared. */ private void buildAndShowNotification(final Builder builder, final TaskStackBuilder stackBuilder) { final PendingIntent gamePendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(gamePendingIntent); if (Utilities.checkIfSettingIsEnabled(this, R.string.settings_key_show_notification_light, true)) { // only turn on the notification light if the user has // specified that he or she wants it on builder.setLights(Color.MAGENTA, GCM_NOTIFICATION_LIGHTS_DURATION_ON, GCM_NOTIFICATION_LIGHTS_DURATION_OFF); } // show the notification final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(GCM_NOTIFICATION_ID, builder.build()); if (Utilities.checkIfSettingIsEnabled(this, R.string.settings_key_vibrate, false)) { final Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(GCM_NOTIFICATION_VIBRATION_DURATION); } } /** * Saves the given Notification object to the notifications cache and then * returns a LinkedList of all cached notifications plus this new one. * * @param notification * The newly received Notification object. * * @return * Returns a LinkedList of all cached notifications in addition to the * given Notification object. They'll be sorted in received order. This * means that the oldest one will be the first entry. */ private LinkedList<Notification> saveCurrentNotificationAndGrabExisting(final Notification notification) { final SharedPreferences sPreferences = getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); try { final JSONObject notificationJSON = notification.makeJSON(); final String notificationString = notificationJSON.toString(); final String notificationKey = String.valueOf(System.nanoTime()); sPreferences.edit() .putString(notificationKey, notificationString) .commit(); } catch (final JSONException e) { Log.w(LOG_TAG, "JSONException when trying to cache this notification's data!", e); } final LinkedList<Notification> notifications = new LinkedList<Notification>(); try { @SuppressWarnings("unchecked") final Map<String, String> map = (Map<String, String>) sPreferences.getAll(); if (map != null && !map.isEmpty()) { final Set<String> set = map.keySet(); for (final String id : set) { final String notificationString = map.get(id); final JSONObject notificationJSON = new JSONObject(notificationString); final Notification newNotification = new Notification(id, notificationJSON); notifications.add(newNotification); } Collections.sort(notifications, new Comparator<Notification>() { @Override public int compare(final Notification curly, final Notification larry) { return (int) (curly.getTime() - larry.getTime()); } }); while (notifications.size() > GCM_MAX_SIMULTANEOUS_NOTIFICATIONS) { notifications.removeLast(); } } } catch (final JSONException e) { Log.w(LOG_TAG, "JSONException occurred when grabbing existing notifications!", e); notifications.clear(); } return notifications; } /** * Clears all cached notification data and rids the Android status bar of * any currently showing notifications. * * @param context * The Context of the Activity or Fragment that you're calling this method * from. */ public static void clearNotifications(final Context context) { ((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE)).cancelAll(); context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit().clear().commit(); } }