From b2183054c809af25d9032492e2895d83b6d00fd3 Mon Sep 17 00:00:00 2001
From: Peter Philipp <peter.philipp@cando-image.com>
Date: Sat, 30 Jun 2012 08:13:47 +0200
Subject: [PATCH] Added GCM support since C2DM is deprecated. Added dependency
 to services module. Added services action to unregister a
 token. Added README.txt

---
 README.txt                              |   23 +++++
 includes/push_notifications.admin.inc   |   26 +++++-
 includes/push_notifications.service.inc |   23 ++++-
 push_notifications.info                 |    3 +-
 push_notifications.module               |  152 +++++++++++++++++++++++++++++--
 5 files changed, 212 insertions(+), 15 deletions(-)
 create mode 100644 README.txt

diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..f7df140
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,23 @@
+The Push Notifications module provides the feature set to send out push
+notifications to iOS (iPhone/iPad) and Android devices using Apple's
+Push Notification Service (APNS) as well as Google's Android Cloud to Device
+Messaging framework (C2DM) and or Google's Cloud Messaging for Android (GCM).
+This module does not rely on any external services and allows site owners to
+send out push notifications to any mobile device for free.
+
+## REST Interface
+Mobile apps can register the device by calling the REST interface provided by
+the service module. Don't forget to enable the push_notifications ressource.
+
+{token} = The device token.
+{type} = The type of the device - currently supported: ios or android.
+
+---Register
+URL: http://my-drupal-installation/services_module_endpoint/push_notifications
+Method: "POST"
+Payload: token={token}&type={type}
+
+---Unregister
+http://my-drupal-installation/services_module_endpoint/push_notifications/{token}
+Method: "DELETE"
+Payload:
diff --git a/includes/push_notifications.admin.inc b/includes/push_notifications.admin.inc
index af76cd2..c92222c 100644
--- a/includes/push_notifications.admin.inc
+++ b/includes/push_notifications.admin.inc
@@ -164,6 +164,19 @@ function push_notifications_admin_form($form_state) {
     '#default_value' => variable_get('push_notifications_c2dm_password', ''),
   );
 
+  $form['configuration_gcm'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('GCM Push Notifications'),
+    '#description' => t('Requires a valid GCM API key. Get one from the !google_api_console.', array('!signup' => l(t('Google APIs Console '), 'https://code.google.com/apis/console', array('attributes' => array('target' => '_blank'))))),
+  );
+
+  $form['configuration_gcm']['push_notifications_gcm_api_key'] = array(
+    '#type' => 'textfield',
+    '#title' => t('GCM Api key'),
+    '#description' => t('Enter the apiy key fo a GCM "instance"'),
+    '#default_value' => variable_get('push_notifications_gcm_api_key', ''),
+  );
+
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => 'Save Configuration',
@@ -203,6 +216,9 @@ function push_notifications_admin_form_submit($form, &$form_state) {
   variable_set('push_notifications_c2dm_username', $form_state['values']['push_notifications_c2dm_username']);
   variable_set('push_notifications_c2dm_password', $form_state['values']['push_notifications_c2dm_password']);
 
+  // Set GCM API key.
+  variable_set('push_notifications_gcm_api_key', $form_state['values']['push_notifications_gcm_api_key']);
+
   // Set the APNS stream limit.
   variable_set('push_notifications_apns_stream_context_limit', $form_state['values']['stream_context_limit']);
 
@@ -249,7 +265,7 @@ function push_notifications_mass_push_form($form_state) {
 
   // Only show Android option if C2DM credentials are available.
   $recipients_options = array('ios' => t('iOS (iPhone/iPad)'));
-  if (PUSH_NOTIFICATIONS_C2DM_USERNAME && PUSH_NOTIFICATIONS_C2DM_PASSWORD) {
+  if ((PUSH_NOTIFICATIONS_C2DM_USERNAME && PUSH_NOTIFICATIONS_C2DM_PASSWORD) || PUSH_NOTIFICATIONS_GCM_API_KEY) {
     $recipients_options['android'] = t('Android');
   }
   $form['recipients'] = array(
@@ -331,7 +347,13 @@ function push_notifications_mass_push_form_submit($form, &$form_state) {
     // Get all Android recipients.
     $tokens_android = push_notifications_get_tokens(PUSH_NOTIFICATIONS_TYPE_ID_ANDROID, $language);
     if (!empty($tokens_android)) {
-      $result = push_notifications_c2dm_send_message($tokens_android, $payload);
+      // Prefer GCM since it is more efficient.
+      if (PUSH_NOTIFICATIONS_GCM_API_KEY) {
+        $result = push_notifications_gcm_send_message($tokens_android, $payload);
+      }
+      else {
+        $result = push_notifications_c2dm_send_message($tokens_android, $payload);
+      }
       $dsm_type = ($result['success']) ? 'status' : 'error';
       drupal_set_message($result['message'], $dsm_type);
     }
diff --git a/includes/push_notifications.service.inc b/includes/push_notifications.service.inc
index 4d1ac06..61e65a3 100644
--- a/includes/push_notifications.service.inc
+++ b/includes/push_notifications.service.inc
@@ -20,7 +20,7 @@ function _push_notifications_service_create_device_token($data) {
   if (!isset($data['token']) || !isset($data['type'])) {
     return services_error(t('At least one parameter is missing.'), 400);
   }
-  
+
   // Default language to English and validate language setting.
   if (isset($data['language'])) {
     // Make sure this is a valid language code.
@@ -29,7 +29,7 @@ function _push_notifications_service_create_device_token($data) {
     if (!array_key_exists($data['language'], $languages)) {
       return services_error(t('This is not a valid ISO 639 language code'), 404);
     }
-    
+
     // Optionally, only allow enabled languages.
     if (variable_get('push_notifications_require_enabled_language')) {
       $available_languages = language_list();
@@ -80,4 +80,21 @@ function _push_notifications_service_create_device_token($data) {
       'message' => 'This token was successfully stored in the database.'
     );
   }
-}
\ No newline at end of file
+}
+
+/**
+ * Deletes a already registered token.
+ *
+ * @param $data
+ * @return array|mixed
+ */
+function _push_notifications_service_delete_device_token($token) {
+  if (empty($token)) {
+    return services_error(t('Token parameter is missing.'), 400);
+  }
+  push_notifications_purge_token($token);
+  return array(
+    'success' => 1,
+    'message' => 'The token was successfully removed from the database.'
+  );
+}
diff --git a/push_notifications.info b/push_notifications.info
index 17c8561..0f54fc6 100644
--- a/push_notifications.info
+++ b/push_notifications.info
@@ -4,4 +4,5 @@ package = other
 core = 7.x
 files[] = push_notifications.install
 files[] = push_notifications.module
-configure = admin/config/services/push_notifications
\ No newline at end of file
+configure = admin/config/services/push_notifications
+dependencies[] = services
diff --git a/push_notifications.module b/push_notifications.module
index e1bbd97..e0ef6f9 100644
--- a/push_notifications.module
+++ b/push_notifications.module
@@ -47,6 +47,14 @@ define('PUSH_NOTIFICATIONS_C2DM_CLIENT_LOGIN_ACTION_URL', variable_get('push_not
 // C2DM Server Post URL
 define('PUSH_NOTIFICATIONS_C2DM_SERVER_POST_URL', variable_get('push_notifications_c2dm_server_post_url', 'https://android.apis.google.com/c2dm/send'));
 
+//
+// GCM Variables
+//
+// C2DM Credentials.
+define('PUSH_NOTIFICATIONS_GCM_API_KEY', variable_get('push_notifications_gcm_api_key', ''));
+define('PUSH_NOTIFICATIONS_GCM_CLIENT_LOGIN_ACTION_URL', variable_get('push_notifications_gcm_client_login_action_url', 'https://www.google.com/accounts/ClientLogin'));
+// C2DM Server Post URL
+define('PUSH_NOTIFICATIONS_GCM_SERVER_POST_URL', variable_get('push_notifications_gcm_server_post_url', 'https://android.googleapis.com/gcm/send'));
 
 
 /**
@@ -125,6 +133,10 @@ function push_notifications_permission() {
       'title' => t('Register Device Token'),
       'description' => t('Allows users to register a device token.'),
     ),
+    'remove device token' => array(
+      'title' => t('Remove Device Token'),
+      'description' => t('Allows users to remove a device token.'),
+    ),
   );
 }
 
@@ -167,7 +179,27 @@ function push_notifications_services_resources() {
             'description' => 'Language',
             'optional' => TRUE,
             'source' => 'data',
-          ),          
+          ),
+        ),
+      ),
+      'delete' => array(
+        'help' => 'Removes a registered a device token. Only needs the token.',
+        'callback' => '_push_notifications_service_delete_device_token',
+        'file' => array(
+          'type' => 'inc',
+          'module' => 'push_notifications',
+          'name' => 'includes/push_notifications.service'
+        ),
+        'access arguments' => array('remove device token'),
+        'access arguments append' => FALSE,
+        'args' => array(
+          array(
+            'name' => 'token',
+            'type' => 'string',
+            'description' => 'Device Token',
+            'optional' => FALSE,
+            'source' => array('path' => '0'),
+          ),
         ),
       ),
     ),
@@ -235,11 +267,11 @@ function push_notifications_store_token($token = '', $type_id = '', $uid = '', $
   if (!is_string($token) || !is_numeric($type_id) || !is_numeric($uid)) {
     return FALSE;
   }
-  
+
   // Default language to site default.
   if ($language == '') {
     $default_language = language_default();
-    $language = $default_language->language;    
+    $language = $default_language->language;
   }
 
   // Write record.
@@ -497,7 +529,7 @@ function push_notifications_c2dm_send_message($tokens, $payload) {
 
     // If Google's server returns a reply, but that reply includes an error, log the error message.
     if ($info['http_code'] == 200 && (isset($response) && preg_match('/Error/', $response))) {
-      watchdog('push_notifications', 'Google\'s Server returned an error: ' . $response, NULL, WATCHDOG_ERROR);
+      watchdog('push_notifications', "Google's Server returned an error: " . $response, NULL, WATCHDOG_ERROR);
 
       // If the device token is invalid or not registered (anymore because the user
       // has uninstalled the application), remove this device token.
@@ -519,6 +551,108 @@ function push_notifications_c2dm_send_message($tokens, $payload) {
   return $result;
 }
 
+/**
+ * Send out push notifications through GCM.
+ *
+ * @link http://developer.android.com/guide/google/gcm/index.html
+ *
+ * @param $tokens
+ *   Array of gcm tokens
+ * @param $payload
+ *   Payload to send.
+ *
+ * @return
+ *   Array with the following keys:
+ *   - count_attempted (# of attempted messages sent)
+ *   - count_success   (# of successful sends)
+ *   - success         (# boolean)
+ *   - message         (Prepared result message)
+ */
+function push_notifications_gcm_send_message($tokens, $payload) {
+  if (!is_array($tokens) || empty($payload) || (is_array($tokens) && empty($tokens))) {
+    return FALSE;
+  }
+
+  // Define an array of result values.
+  $result = array(
+    'count_attempted' => 0,
+    'count_success' => 0,
+    'success' => 0,
+    'message' => '',
+  );
+
+  // Define the header.
+  $headers = array();
+  $headers[] = 'Content-Type:application/json';
+  $headers[] = 'Authorization:key=' . PUSH_NOTIFICATIONS_GCM_API_KEY;
+
+
+  // Check of many token bundles can be build.
+  $token_bundles = ceil(count($tokens) / 1000);
+  $result['count_attempted'] = count($tokens);
+
+  // Send a push notification to every recipient.
+  for ($i = 0; $i < $token_bundles; $i++) {
+    // Create a token bundle.
+    $bundle_tokens = array_slice($tokens, $i * 1000, 1000, FALSE);
+
+    // Convert the payload into the correct format for C2DM payloads.
+    // Prefill an array with values from other modules first.
+    $data = array();
+    foreach ($payload as $key => $value) {
+      if ($key != 'alert') {
+        $data['data'][$key] = $value;
+      }
+    }
+    // Fill the default values required for each payload.
+    $data['registration_ids'] = $bundle_tokens;
+    $data['collapse_key'] = (string) time();
+    $data['data']['message'] = $payload['alert'];
+
+    $curl = curl_init();
+    curl_setopt($curl, CURLOPT_URL, PUSH_NOTIFICATIONS_GCM_SERVER_POST_URL);
+    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
+    curl_setopt($curl, CURLOPT_POST, TRUE);
+    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
+    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
+    $response_raw = curl_exec($curl);
+    $info = curl_getinfo($curl);
+    curl_close($curl);
+
+    $response = FALSE;
+    if (isset($response_raw)) {
+      $response = json_decode($response_raw);
+    }
+
+    // If Google's server returns a reply, but that reply includes an error,
+    // log the error message.
+    if ($info['http_code'] == 200 && (!empty($response->failure))) {
+      watchdog('push_notifications', "Google's Server returned an error: " . $response_raw, NULL, WATCHDOG_ERROR);
+
+      // Analyze the failure.
+      foreach ($response->results as $token_index => $message_result) {
+        if (!empty($message_result->error)) {
+          // If the device token is invalid or not registered (anymore because the user
+          // has uninstalled the application), remove this device token.
+          if ($message_result->error == 'NotRegistered' || $message_result->error == 'InvalidRegistration') {
+            push_notifications_purge_token($bundle_tokens[$token_index]);
+            watchdog('push_notifications', 'GCM token not valid anymore. Removing token ' . $bundle_tokens[$token_index]);
+          }
+        }
+      }
+    }
+
+    // Count the successful sent push notifications if there are any.
+    if ($info['http_code'] == 200 && !empty($response->success)) {
+      $result['count_success'] += $response->success;
+    }
+  }
+
+  $result['message'] = t('Successfully sent !count_success Android push messages (attempted to send !count messages).', array('!count_success' => $result['count_success'], '!count' => $result['count_attempted']));
+  $result['success'] = TRUE;
+  return $result;
+}
 
 
 /**
@@ -576,7 +710,7 @@ function push_notifications_used_languages() {
   $query->fields('pnt', array('language'));
   $query->distinct();
   $result = $query->execute();
-  
+
   // Convert the records into an array with
   // full language code available.
   include_once DRUPAL_ROOT . '/includes/iso.inc';
@@ -586,14 +720,14 @@ function push_notifications_used_languages() {
   foreach ($result as $record) {
     $used_languages[$record->language] = $languages[$record->language][0];
   }
-  
+
   if (!empty($used_languages)) {
     // Sort the languages alphabetically.
     $used_langauges = asort($used_languages);
     // Add an "All" option.
-    array_unshift($used_languages, 'All Recipients');    
+    array_unshift($used_languages, 'All Recipients');
   }
-  
+
   return $used_languages;
 }
 
@@ -660,4 +794,4 @@ function push_notifications_apns_feedback_service() {
   // Give some feedback after the process finished.
   watchdog('push_notifications', '!count were removed after pulling the Apple feedback service.', array('!count' => $counter));
 
-}
\ No newline at end of file
+}
-- 
1.7.10.msysgit.1

