├── .gitignore ├── src └── com │ └── grokkingandroid │ └── sampleapp │ └── samples │ └── gcm │ └── ccs │ └── server │ ├── RegisterProcessor.java │ ├── PayloadProcessor.java │ ├── EchoProcessor.java │ ├── MessageProcessor.java │ ├── ProcessorFactory.java │ ├── CcsMessage.java │ ├── PseudoDao.java │ └── CcsClient.java ├── README.md └── LICENSE-2.0.txt /.gitignore: -------------------------------------------------------------------------------- 1 | #Android generated 2 | bin 3 | gen 4 | lint.xml 5 | 6 | #Eclipse 7 | .project 8 | .classpath 9 | .settings 10 | .checkstyle 11 | 12 | #IntelliJ IDEA 13 | .idea 14 | *.iml 15 | *.ipr 16 | *.iws 17 | classes 18 | gen-external-apklibs 19 | 20 | #Netbeans 21 | nbproject 22 | build 23 | manifest.mf 24 | 25 | #vi 26 | *.swp 27 | 28 | #other editors 29 | *.bak 30 | 31 | #Maven 32 | target 33 | release.properties 34 | pom.xml.* 35 | 36 | #Ant 37 | build.xml 38 | ant.properties 39 | local.properties 40 | proguard.cfg 41 | proguard-project.txt 42 | 43 | #Other 44 | .DS_Store 45 | tmp 46 | *.tgz 47 | /dist/ -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/RegisterProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 18 | 19 | /** 20 | * Handles a user registration. 21 | */ 22 | public class RegisterProcessor implements PayloadProcessor{ 23 | 24 | @Override 25 | public void handleMessage(CcsMessage msg) { 26 | String accountName = msg.getPayload().get("account"); 27 | PseudoDao.getInstance().addRegistration(msg.getFrom(), accountName); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/PayloadProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 18 | 19 | /** 20 | * All messages from the user have a specific format. 21 | * The Action field defines, what the action is about. An example 22 | * is the action com.grokkingandroid.sampleapp.samples.gcm.action.REGISTER, 23 | * used to tell the server about a newly registered user. 24 | *
25 | * Any further fields are specific for the given action. 26 | */ 27 | public interface PayloadProcessor { 28 | 29 | void handleMessage(CcsMessage msg); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/EchoProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 17 | 18 | /** 19 | * Handles an echo request. 20 | */ 21 | public class EchoProcessor implements PayloadProcessor{ 22 | 23 | @Override 24 | public void handleMessage(CcsMessage msg) { 25 | PseudoDao dao = PseudoDao.getInstance(); 26 | CcsClient client = CcsClient.getInstance(); 27 | String msgId = dao.getUniqueMessageId(); 28 | String jsonRequest = 29 | CcsClient.createJsonMessage( 30 | msg.getFrom(), 31 | msgId, 32 | msg.getPayload(), 33 | null, 34 | null, // TTL (null -> default-TTL) 35 | false); 36 | client.send(jsonRequest); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/MessageProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 17 | 18 | /** 19 | * Handles an echo request. 20 | */ 21 | public class MessageProcessor implements PayloadProcessor{ 22 | 23 | @Override 24 | public void handleMessage(CcsMessage msg) { 25 | PseudoDao dao = PseudoDao.getInstance(); 26 | CcsClient client = CcsClient.getInstance(); 27 | String msgId = dao.getUniqueMessageId(); 28 | String jsonRequest = 29 | CcsClient.createJsonMessage( 30 | msg.getFrom(), 31 | msgId, 32 | msg.getPayload(), 33 | null, 34 | null, // TTL (null -> default-TTL) 35 | false); 36 | client.send(jsonRequest); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/ProcessorFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 18 | 19 | public class ProcessorFactory { 20 | 21 | private static final String PACKAGE = "com.grokkingandroid.sampleapp.samples.gcm"; 22 | private static final String ACTION_REGISTER = PACKAGE + ".REGISTER"; 23 | private static final String ACTION_ECHO = PACKAGE + ".ECHO"; 24 | private static final String ACTION_MESSAGE = PACKAGE + ".MESSAGE"; 25 | 26 | public static PayloadProcessor getProcessor(String action) { 27 | if (action == null) { 28 | throw new IllegalStateException("action must not be null"); 29 | } 30 | if (action.equals(ACTION_REGISTER)) { 31 | return new RegisterProcessor(); 32 | } 33 | else if (action.equals(ACTION_ECHO)) { 34 | return new EchoProcessor(); 35 | } 36 | else if (action.equals(ACTION_MESSAGE)) { 37 | return new MessageProcessor(); 38 | } 39 | throw new IllegalStateException("Action " + action + " is unknown"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/CcsMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 17 | 18 | import java.util.Map; 19 | 20 | /** 21 | * Represents a message for CCS based massaging. 22 | */ 23 | public class CcsMessage { 24 | 25 | /** 26 | * Recipient-ID. 27 | */ 28 | private String mFrom; 29 | /** 30 | * Sender app's package. 31 | */ 32 | private String mCategory; 33 | /** 34 | * Unique id for this message. 35 | */ 36 | private String mMessageId; 37 | /** 38 | * Payload data. A String in Json format. 39 | */ 40 | private Map mPayload; 41 | 42 | public CcsMessage(String from, String category, String messageId, Map payload) { 43 | mFrom = from; 44 | mCategory = category; 45 | mMessageId = messageId; 46 | mPayload = payload; 47 | } 48 | 49 | public String getFrom() { 50 | return mFrom; 51 | } 52 | 53 | public String getCategory() { 54 | return mCategory; 55 | } 56 | 57 | public String getMessageId() { 58 | return mMessageId; 59 | } 60 | 61 | public Map getPayload() { 62 | return mPayload; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/PseudoDao.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Random; 25 | import java.util.Set; 26 | 27 | /** 28 | * This class acts as a DAO replacement. There is no 29 | * persistent state. As soon as you kill the server, all state will 30 | * be lost. 31 | * 32 | * You have to take care of persisting messages as well as 33 | * recipients for proper apps! 34 | */ 35 | public class PseudoDao { 36 | 37 | private final static PseudoDao instance = new PseudoDao(); 38 | private final static Random sRandom = new Random(); 39 | private final Set mMessageIds = new HashSet(); 40 | private final Map> mUserMap = new HashMap>(); 41 | private final List mRegisteredUsers = new ArrayList(); 42 | private final Map mNotificationKeyMap = new HashMap(); 43 | 44 | private PseudoDao() { 45 | } 46 | 47 | public static PseudoDao getInstance() { 48 | return instance; 49 | } 50 | 51 | public void addRegistration(String regId, String accountName) { 52 | synchronized(mRegisteredUsers) { 53 | if (!mRegisteredUsers.contains(regId)) { 54 | mRegisteredUsers.add(regId); 55 | } 56 | if (accountName != null) { 57 | List regIdList = mUserMap.get(accountName); 58 | if (regIdList == null) { 59 | regIdList = new ArrayList(); 60 | mUserMap.put(accountName, regIdList); 61 | } 62 | if (!regIdList.contains(regId)) { 63 | regIdList.add(regId); 64 | } 65 | } 66 | } 67 | } 68 | 69 | public List getAllRegistrationIds() { 70 | return Collections.unmodifiableList(mRegisteredUsers); 71 | } 72 | 73 | public List getAllRegistrationIdsForAccount(String account) { 74 | List regIds = mUserMap.get(account); 75 | if (regIds != null) { 76 | return Collections.unmodifiableList(regIds); 77 | } 78 | return null; 79 | } 80 | 81 | public String getNotificationKeyName(String accountName) { 82 | return mNotificationKeyMap.get(accountName); 83 | } 84 | 85 | public void storeNotificationKeyName(String accountName, String notificationKeyName) { 86 | mNotificationKeyMap.put(accountName, notificationKeyName); 87 | } 88 | 89 | public Set getAccounts() { 90 | return Collections.unmodifiableSet(mUserMap.keySet()); 91 | } 92 | 93 | public String getUniqueMessageId() { 94 | int nextRandom = sRandom.nextInt(); 95 | while (mMessageIds.contains(nextRandom)) { 96 | nextRandom = sRandom.nextInt(); 97 | } 98 | return Integer.toString(nextRandom); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readme 2 | This is a sample project to showcase Google Cloud Messaging using upstream messaging. This 3 | project is a very simply standalone server. Though that's not part of this sample 4 | I was able to utilize this project from within a Java webapp. 5 | 6 | The project was originally created for a talk I held at the [Dutch Android User Group](www.dutchaug.org) 7 | meeting in Utrecht on January, 16th 2014. If you ever have the possibility to 8 | visit a meeting of the Dutch AUG, I stringly recommend to do so. It has been a gorgeous evening! 9 | 10 | 11 | ## Libraries used 12 | This project makes use of [Smack](http://www.igniterealtime.org/projects/smack/) and 13 | [Json-simple](http://code.google.com/p/json-simple/). The code is based on a sample class provided 14 | by Google on the [XMPP documentation page](http://developer.android.com/google/gcm/ccs.html). 15 | 16 | 17 | ## Most important classes 18 | All classes are within the `com.grokkingandroid.sampleapp.samples.gcm.ccs.server` package. 19 | 20 | `CcsClient` contains a main method which takes three arguments: 21 | 22 | 1. The project number 23 | 2. The API key 24 | 3. A registration id to send a test message to 25 | 26 | If you start it that way, the GUI of the Smack library is used to show incoming and outgoing messages. 27 | 28 | Of course, you also can use `CcsClient` from within any other project. In my case I used it from within 29 | a web project to send messages to the Android client. 30 | 31 | In that case you first have to call `prepareClient()` and pass it the project number and the api key as arguments. 32 | The third argument decides whether the GUI should be shown or not. On servers you have to set this to `false`. 33 | 34 | The registration ids of the Android clients and all message are stored in memory. This is managed by the 35 | `PseudoDao` class. This should help you to get started with a real persistence solution for production. 36 | 37 | For this sample all incoming messages must follow a certain format. That is, they must contain at least 38 | an `action` key with a supported value. This `action` key determines which `PayloadProcessor` to create. 39 | The implementations of `PayloadProcessor` (`EchoProcessor`, `MessageProcessor` and `RegisterProcessor`) 40 | finally handle the incoming messages and perform the appropriate actions. 41 | 42 | 43 | ## Credentials 44 | **To run this project you need a GCM-project number and an API key.** You can read more about it on the 45 | [Getting Started page of Google's documentation](http://developer.android.com/google/gcm/gs.html). 46 | 47 | Those are passed into the `CcsClient` by either calling the `prepareClient()` method or by providing those 48 | as arguments to the main method. 49 | 50 | Keep in mind: For using Google Cloud Messaging with XMPP **you need to apply first**. 51 | See for example the note at the top of the [http://developer.android.com/google/gcm/notifications.html](User Notification page). 52 | You can find a current link there as well. 53 | 54 | 55 | ## Relevant Blogposts on [Grokking Android](http://www.grokkingandroid.com/) 56 | Right now I haven't finished the blog post about GCM. It's in the making and should be up pretty soon. 57 | 58 | 59 | ## Developed by 60 | 61 | *Wolfram Rittmeyer* - You can contact me via: 62 | 63 | * [Grokking Android (my blog)](http://www.grokkingandroid.com) 64 | 65 | * [Google+](https://plus.google.com/+WolframRittmeyer) 66 | 67 | * [Twitter](https://twitter.com/RittmeyerW) 68 | 69 | 70 | ## Thanks 71 | A big thanks to the organizers of the meetup in Utrecht. It was a great evening with good presentations 72 | and interesting conversations in the breaks! 73 | 74 | Also thanks to all my readers and blog commenters. Your feedback helps me to stay on track and to go on 75 | with projects like this. Without my blog (and thus you) I probably woouln't have been invited in the first place :-) 76 | 77 | Special thanks also to the Android crowd on G+. You're an awesome crowd. I have gained many insights 78 | by reading posts there, by following links to blog posts or by discussions on G+! And it was great to meet some of you 79 | in Utrecht. 80 | 81 | And finally: This readme was created using [dillinger.io](http://dillinger.io). Thanks for this service. 82 | 83 | 84 | ## License 85 | Copyright 2014 Wolfram Rittmeyer 86 | 87 | Licensed under the Apache License, Version 2.0 (the "License"); 88 | you may not use this file except in compliance with the License. 89 | You may obtain a copy of the License at 90 | 91 | http://www.apache.org/licenses/LICENSE-2.0 92 | 93 | Unless required by applicable law or agreed to in writing, software 94 | distributed under the License is distributed on an "AS IS" BASIS, 95 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 96 | See the License for the specific language governing permissions and 97 | limitations under the License. 98 | 99 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/com/grokkingandroid/sampleapp/samples/gcm/ccs/server/CcsClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Wolfram Rittmeyer. 3 | * 4 | * Portions Copyright Google Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.grokkingandroid.sampleapp.samples.gcm.ccs.server; 19 | 20 | import org.jivesoftware.smack.ConnectionConfiguration; 21 | import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; 22 | import org.jivesoftware.smack.ConnectionListener; 23 | import org.jivesoftware.smack.PacketInterceptor; 24 | import org.jivesoftware.smack.PacketListener; 25 | import org.jivesoftware.smack.XMPPConnection; 26 | import org.jivesoftware.smack.XMPPException; 27 | import org.jivesoftware.smack.filter.PacketTypeFilter; 28 | import org.jivesoftware.smack.packet.DefaultPacketExtension; 29 | import org.jivesoftware.smack.packet.Message; 30 | import org.jivesoftware.smack.packet.Packet; 31 | import org.jivesoftware.smack.packet.PacketExtension; 32 | import org.jivesoftware.smack.provider.PacketExtensionProvider; 33 | import org.jivesoftware.smack.provider.ProviderManager; 34 | import org.jivesoftware.smack.util.StringUtils; 35 | import org.json.simple.JSONValue; 36 | import org.json.simple.parser.ParseException; 37 | import org.xmlpull.v1.XmlPullParser; 38 | 39 | import java.util.HashMap; 40 | import java.util.List; 41 | import java.util.Map; 42 | import java.util.Random; 43 | import java.util.logging.Level; 44 | import java.util.logging.Logger; 45 | 46 | import javax.net.ssl.SSLSocketFactory; 47 | 48 | /** 49 | * Sample Smack implementation of a client for GCM Cloud Connection Server. 50 | * Most of it has been taken more or less verbatim from Googles 51 | * documentation: http://developer.android.com/google/gcm/ccs.html 52 | *
53 | * But some additions have been made. Bigger changes are annotated like that: 54 | * "/// new". 55 | *
56 | * Those changes have to do with parsing certain type of messages 57 | * as well as with sending messages to a list of recipients. The original code 58 | * only covers sending one message to exactly one recipient. 59 | */ 60 | public class CcsClient { 61 | 62 | public static final Logger logger = Logger.getLogger(CcsClient.class.getName()); 63 | 64 | public static final String GCM_SERVER = "gcm.googleapis.com"; 65 | public static final int GCM_PORT = 5235; 66 | 67 | public static final String GCM_ELEMENT_NAME = "gcm"; 68 | public static final String GCM_NAMESPACE = "google:mobile:data"; 69 | 70 | static Random random = new Random(); 71 | XMPPConnection connection; 72 | ConnectionConfiguration config; 73 | 74 | /// new: some additional instance and class members 75 | private static CcsClient sInstance = null; 76 | private String mApiKey = null; 77 | private String mProjectId = null; 78 | private boolean mDebuggable = false; 79 | 80 | /** 81 | * XMPP Packet Extension for GCM Cloud Connection Server. 82 | */ 83 | class GcmPacketExtension extends DefaultPacketExtension { 84 | 85 | String json; 86 | 87 | public GcmPacketExtension(String json) { 88 | super(GCM_ELEMENT_NAME, GCM_NAMESPACE); 89 | this.json = json; 90 | } 91 | 92 | public String getJson() { 93 | return json; 94 | } 95 | 96 | @Override 97 | public String toXML() { 98 | return String.format("<%s xmlns=\"%s\">%s", GCM_ELEMENT_NAME, 99 | GCM_NAMESPACE, json, GCM_ELEMENT_NAME); 100 | } 101 | 102 | @SuppressWarnings("unused") 103 | public Packet toPacket() { 104 | return new Message() { 105 | // Must override toXML() because it includes a 106 | @Override 107 | public String toXML() { 108 | 109 | StringBuilder buf = new StringBuilder(); 110 | buf.append(""); 127 | buf.append(GcmPacketExtension.this.toXML()); 128 | buf.append(""); 129 | return buf.toString(); 130 | } 131 | }; 132 | } 133 | } 134 | 135 | public static CcsClient getInstance() { 136 | if (sInstance == null) { 137 | throw new IllegalStateException("You have to prepare the client first"); 138 | } 139 | return sInstance; 140 | } 141 | 142 | public static CcsClient prepareClient(String projectId, String apiKey, boolean debuggable) { 143 | synchronized(CcsClient.class) { 144 | if (sInstance == null) { 145 | sInstance = new CcsClient(projectId, apiKey, debuggable); 146 | } 147 | } 148 | return sInstance; 149 | } 150 | 151 | private CcsClient(String projectId, String apiKey, boolean debuggable) { 152 | this(); 153 | mApiKey = apiKey; 154 | mProjectId = projectId; 155 | mDebuggable = debuggable; 156 | } 157 | 158 | private CcsClient() { 159 | // Add GcmPacketExtension 160 | ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME, 161 | GCM_NAMESPACE, new PacketExtensionProvider() { 162 | 163 | @Override 164 | public PacketExtension parseExtension(XmlPullParser parser) 165 | throws Exception { 166 | String json = parser.nextText(); 167 | GcmPacketExtension packet = new GcmPacketExtension(json); 168 | return packet; 169 | } 170 | }); 171 | } 172 | 173 | /** 174 | * Returns a random message id to uniquely identify a message. 175 | * 176 | *

177 | * Note: This is generated by a pseudo random number generator for 178 | * illustration purpose, and is not guaranteed to be unique. 179 | * 180 | */ 181 | public String getRandomMessageId() { 182 | return "m-" + Long.toString(random.nextLong()); 183 | } 184 | 185 | /** 186 | * Sends a downstream GCM message. 187 | */ 188 | public void send(String jsonRequest) { 189 | Packet request = new GcmPacketExtension(jsonRequest).toPacket(); 190 | connection.sendPacket(request); 191 | } 192 | 193 | /// new: for sending messages to a list of recipients 194 | /** 195 | * Sends a message to multiple recipients. Kind of like the old 196 | * HTTP message with the list of regIds in the "registration_ids" field. 197 | */ 198 | public void sendBroadcast(Map payload, String collapseKey, 199 | long timeToLive, Boolean delayWhileIdle, List recipients) { 200 | Map map = createAttributeMap(null, null, payload, collapseKey, 201 | timeToLive, delayWhileIdle); 202 | for (String toRegId: recipients) { 203 | String messageId = getRandomMessageId(); 204 | map.put("message_id", messageId); 205 | map.put("to", toRegId); 206 | String jsonRequest = createJsonMessage(map); 207 | send(jsonRequest); 208 | } 209 | } 210 | 211 | /// new: customized version of the standard handleIncomingDateMessage method 212 | /** 213 | * Handles an upstream data message from a device application. 214 | */ 215 | public void handleIncomingDataMessage(CcsMessage msg) { 216 | if (msg.getPayload().get("action") != null) { 217 | PayloadProcessor processor = ProcessorFactory.getProcessor(msg.getPayload().get("action")); 218 | processor.handleMessage(msg); 219 | } 220 | } 221 | 222 | /// new: was previously part of the previous method 223 | /** 224 | * 225 | */ 226 | private CcsMessage getMessage(Map jsonObject) { 227 | String from = jsonObject.get("from").toString(); 228 | 229 | // PackageName of the application that sent this message. 230 | String category = jsonObject.get("category").toString(); 231 | 232 | // unique id of this message 233 | String messageId = jsonObject.get("message_id").toString(); 234 | 235 | @SuppressWarnings("unchecked") 236 | Map payload = (Map) jsonObject.get("data"); 237 | 238 | CcsMessage msg = new CcsMessage(from, category, messageId, payload); 239 | 240 | return msg; 241 | } 242 | 243 | /** 244 | * Handles an ACK. 245 | * 246 | *

247 | * By default, it only logs a INFO message, but subclasses could override it 248 | * to properly handle ACKS. 249 | */ 250 | public void handleAckReceipt(Map jsonObject) { 251 | String messageId = jsonObject.get("message_id").toString(); 252 | String from = jsonObject.get("from").toString(); 253 | logger.log(Level.INFO, "handleAckReceipt() from: " + from + ", messageId: " + messageId); 254 | } 255 | 256 | /** 257 | * Handles a NACK. 258 | * 259 | *

260 | * By default, it only logs a INFO message, but subclasses could override it 261 | * to properly handle NACKS. 262 | */ 263 | public void handleNackReceipt(Map jsonObject) { 264 | String messageId = jsonObject.get("message_id").toString(); 265 | String from = jsonObject.get("from").toString(); 266 | logger.log(Level.INFO, "handleNackReceipt() from: " + from + ", messageId: " + messageId); 267 | } 268 | 269 | /** 270 | * Creates a JSON encoded GCM message. 271 | * 272 | * @param to RegistrationId of the target device (Required). 273 | * @param messageId Unique messageId for which CCS will send an "ack/nack" 274 | * (Required). 275 | * @param payload Message content intended for the application. (Optional). 276 | * @param collapseKey GCM collapse_key parameter (Optional). 277 | * @param timeToLive GCM time_to_live parameter (Optional). 278 | * @param delayWhileIdle GCM delay_while_idle parameter (Optional). 279 | * @return JSON encoded GCM message. 280 | */ 281 | public static String createJsonMessage(String to, String messageId, Map payload, 282 | String collapseKey, Long timeToLive, Boolean delayWhileIdle) { 283 | return createJsonMessage(createAttributeMap(to, messageId, payload, 284 | collapseKey, timeToLive, delayWhileIdle)); 285 | } 286 | 287 | public static String createJsonMessage(Map map) { 288 | return JSONValue.toJSONString(map); 289 | } 290 | 291 | public static Map createAttributeMap(String to, String messageId, Map payload, 292 | String collapseKey, Long timeToLive, Boolean delayWhileIdle) { 293 | Map message = new HashMap(); 294 | if (to != null) { 295 | message.put("to", to); 296 | } 297 | if (collapseKey != null) { 298 | message.put("collapse_key", collapseKey); 299 | } 300 | if (timeToLive != null) { 301 | message.put("time_to_live", timeToLive); 302 | } 303 | if (delayWhileIdle != null && delayWhileIdle) { 304 | message.put("delay_while_idle", true); 305 | } 306 | if (messageId != null) { 307 | message.put("message_id", messageId); 308 | } 309 | message.put("data", payload); 310 | return message; 311 | } 312 | 313 | /** 314 | * Creates a JSON encoded ACK message for an upstream message received from 315 | * an application. 316 | * 317 | * @param to RegistrationId of the device who sent the upstream message. 318 | * @param messageId messageId of the upstream message to be acknowledged to 319 | * CCS. 320 | * @return JSON encoded ack. 321 | */ 322 | public static String createJsonAck(String to, String messageId) { 323 | Map message = new HashMap(); 324 | message.put("message_type", "ack"); 325 | message.put("to", to); 326 | message.put("message_id", messageId); 327 | return JSONValue.toJSONString(message); 328 | } 329 | 330 | /// new: NACK added 331 | /** 332 | * Creates a JSON encoded NACK message for an upstream message received from 333 | * an application. 334 | * 335 | * @param to RegistrationId of the device who sent the upstream message. 336 | * @param messageId messageId of the upstream message to be acknowledged to 337 | * CCS. 338 | * @return JSON encoded nack. 339 | */ 340 | public static String createJsonNack(String to, String messageId) { 341 | Map message = new HashMap(); 342 | message.put("message_type", "nack"); 343 | message.put("to", to); 344 | message.put("message_id", messageId); 345 | return JSONValue.toJSONString(message); 346 | } 347 | 348 | /** 349 | * Connects to GCM Cloud Connection Server using the supplied credentials. 350 | * @throws XMPPException 351 | */ 352 | public void connect() throws XMPPException { 353 | config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT); 354 | config.setSecurityMode(SecurityMode.enabled); 355 | config.setReconnectionAllowed(true); 356 | config.setRosterLoadedAtLogin(false); 357 | config.setSendPresence(false); 358 | config.setSocketFactory(SSLSocketFactory.getDefault()); 359 | 360 | // NOTE: Set to true to launch a window with information about packets sent and received 361 | config.setDebuggerEnabled(mDebuggable); 362 | 363 | // -Dsmack.debugEnabled=true 364 | XMPPConnection.DEBUG_ENABLED = true; 365 | 366 | connection = new XMPPConnection(config); 367 | connection.connect(); 368 | 369 | connection.addConnectionListener(new ConnectionListener() { 370 | 371 | @Override 372 | public void reconnectionSuccessful() { 373 | logger.info("Reconnecting.."); 374 | } 375 | 376 | @Override 377 | public void reconnectionFailed(Exception e) { 378 | logger.log(Level.INFO, "Reconnection failed.. ", e); 379 | } 380 | 381 | @Override 382 | public void reconnectingIn(int seconds) { 383 | logger.log(Level.INFO, "Reconnecting in %d secs", seconds); 384 | } 385 | 386 | @Override 387 | public void connectionClosedOnError(Exception e) { 388 | logger.log(Level.INFO, "Connection closed on error."); 389 | } 390 | 391 | @Override 392 | public void connectionClosed() { 393 | logger.info("Connection closed."); 394 | } 395 | }); 396 | 397 | // Handle incoming packets 398 | connection.addPacketListener(new PacketListener() { 399 | 400 | @Override 401 | public void processPacket(Packet packet) { 402 | logger.log(Level.INFO, "Received: " + packet.toXML()); 403 | Message incomingMessage = (Message) packet; 404 | GcmPacketExtension gcmPacket 405 | = (GcmPacketExtension) incomingMessage.getExtension(GCM_NAMESPACE); 406 | String json = gcmPacket.getJson(); 407 | try { 408 | @SuppressWarnings("unchecked") 409 | Map jsonMap 410 | = (Map) JSONValue.parseWithException(json); 411 | 412 | handleMessage(jsonMap); 413 | } catch (ParseException e) { 414 | logger.log(Level.SEVERE, "Error parsing JSON " + json, e); 415 | } catch (Exception e) { 416 | logger.log(Level.SEVERE, "Couldn't send echo.", e); 417 | } 418 | } 419 | }, new PacketTypeFilter(Message.class)); 420 | 421 | // Log all outgoing packets 422 | connection.addPacketInterceptor(new PacketInterceptor() { 423 | @Override 424 | public void interceptPacket(Packet packet) { 425 | logger.log(Level.INFO, "Sent: {0}", packet.toXML()); 426 | } 427 | }, new PacketTypeFilter(Message.class)); 428 | 429 | connection.login(mProjectId + "@gcm.googleapis.com", mApiKey); 430 | logger.log(Level.INFO, "logged in: " + mProjectId); 431 | } 432 | 433 | private void handleMessage(Map jsonMap) { 434 | // present for "ack"/"nack", null otherwise 435 | Object messageType = jsonMap.get("message_type"); 436 | 437 | if (messageType == null) { 438 | CcsMessage msg = getMessage(jsonMap); 439 | // Normal upstream data message 440 | try { 441 | handleIncomingDataMessage(msg); 442 | // Send ACK to CCS 443 | String ack = createJsonAck(msg.getFrom(), msg.getMessageId()); 444 | send(ack); 445 | } 446 | catch (Exception e) { 447 | // Send NACK to CCS 448 | String nack = createJsonNack(msg.getFrom(), msg.getMessageId()); 449 | send(nack); 450 | } 451 | } else if ("ack".equals(messageType.toString())) { 452 | // Process Ack 453 | handleAckReceipt(jsonMap); 454 | } else if ("nack".equals(messageType.toString())) { 455 | // Process Nack 456 | handleNackReceipt(jsonMap); 457 | } else { 458 | logger.log(Level.WARNING, "Unrecognized message type (%s)", 459 | messageType.toString()); 460 | } 461 | } 462 | 463 | public static void main(String[] args) { 464 | final String projectId = args[0]; 465 | final String password = args[1]; 466 | final String toRegId = args[2]; 467 | 468 | CcsClient ccsClient = CcsClient.prepareClient(projectId, password, true); 469 | 470 | try { 471 | ccsClient.connect(); 472 | } catch (XMPPException e) { 473 | e.printStackTrace(); 474 | } 475 | 476 | // Send a sample hello downstream message to a device. 477 | String messageId = ccsClient.getRandomMessageId(); 478 | Map payload = new HashMap(); 479 | payload.put("message", "Simple sample sessage"); 480 | String collapseKey = "sample"; 481 | Long timeToLive = 10000L; 482 | Boolean delayWhileIdle = true; 483 | ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey, 484 | timeToLive, delayWhileIdle)); 485 | } 486 | } 487 | --------------------------------------------------------------------------------