├── .editorconfig ├── Extensions.md ├── LICENSE ├── Migration.md ├── Protocol.md ├── README.md └── Source ├── .editorconfig ├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── libs │ ├── jackson-core-asl-1.9.7.jar │ └── jackson-mapper-asl-1.9.7.jar └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── im │ └── delight │ └── android │ └── ddp │ ├── CallbackProxy.java │ ├── DdpCallback.java │ ├── Fields.java │ ├── Listener.java │ ├── Meteor.java │ ├── MeteorCallback.java │ ├── MeteorSingleton.java │ ├── MongoDb.java │ ├── Preferences.java │ ├── Protocol.java │ ├── ResultListener.java │ ├── SubscribeListener.java │ ├── UnsubscribeListener.java │ └── db │ ├── Collection.java │ ├── DataStore.java │ ├── Database.java │ ├── Document.java │ ├── Query.java │ └── memory │ ├── InMemoryCollection.java │ ├── InMemoryDatabase.java │ ├── InMemoryDocument.java │ └── InMemoryQuery.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── im │ │ └── delight │ │ └── android │ │ └── ddp │ │ └── examples │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ └── values │ └── strings.xml └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | ## Social 4 | 5 | * [Log in with "Google+"](https://gist.github.com/cprakashagr/08cc9084ee92c2e378a0) 6 | * [Log in with "Linked In"](https://gist.github.com/cprakashagr/08cc9084ee92c2e378a0) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | * From `v2.x.x` to `v3.x.x` 4 | * `Meteor#setCallback` is now `Meteor#addCallback`. 5 | * `Meteor#unsetCallback` is now `Meteor#removeCallback` or `Meteor#removeCallbacks`. 6 | * You now have to establish the connection to the server manually. Before, this was done automatically in the `Meteor` constructor. To update your code, call `Meteor#connect` somewhere after calling the `Meteor` constructor. It is recommended to place the call after registering the callback via `Meteor#addCallback`. 7 | * You now have to unregister all callbacks manually by calling either `Meteor#removeCallback` or `Meteor#removeCallbacks`. Before, this was done automatically when disconnecting. It is recommended to remove the callback(s) just after calling `Meteor#disconnect`, whenever you do that. 8 | * Exceptions triggered while connecting and disconnecting are now correctly delivered in `MeteorCallback#onException`. 9 | * From `v1.x.x` to `v2.x.x` 10 | * The minimum API level is now `9` (Android 2.3) instead of `8` (Android 2.2). 11 | * `MeteorCallback.onDisconnect(int code, String reason)` is now just `MeteorCallback.onDisconnect()` without the arguments. 12 | -------------------------------------------------------------------------------- /Protocol.md: -------------------------------------------------------------------------------- 1 | # Protocol 2 | 3 | The client library communicates with Meteor servers over the [Distributed Data Protocol](https://www.meteor.com/ddp) (DDP). 4 | 5 | ## Debugging 6 | 7 | ### Initialization 8 | 9 | 1. Run [this online tool](http://software.hixie.ch/utilities/js/websocket/) that you will use to connect to your *local* Meteor instance 10 | 1. Enter the WebSocket URL (and leave the protocol field empty): 11 | 12 | `ws://localhost:3000/websocket` 13 | 14 | 1. Initialize the DDP connection (using an older protocol that does not require `ping`/`pong` events): 15 | 16 | `{"msg":"connect","version":"pre1","support":["pre1"]}` 17 | 18 | ### Exemplary commands 19 | 20 | * Create a new record (document) `users/jane` on the server: 21 | 22 | ```javascript 23 | {"msg":"method","method":"/database/update","params":[{"_id":"/users/jane"}, {"_id":"/users/jane","_priority":null,"_value":"Jane Doe"},{"upsert":true}],"id":"client-event-1"} 24 | ``` 25 | 26 | * Subscribe to a single location or path (document) from the server: 27 | 28 | ```javascript 29 | {"msg":"sub","id":"subscription-1","name":"node","params":["/users", true]} 30 | ``` 31 | 32 | * Delete a the document at a certain (child of root collection) by ID: 33 | 34 | ```javascript 35 | {"msg":"method","method":"/database/remove","params":[{"_id":"/users/john"}],"id":"client-event-2"} 36 | ``` 37 | 38 | * Call some arbitrary method defined on the server: 39 | 40 | ```javascript 41 | {"msg":"method","method":"myMethodName","params":[],"id":"client-event-3"} 42 | ``` 43 | 44 | ### Verifying against a Meteor client app written in JavaScript 45 | 46 | 1. In your web browser, open a website built with Meteor, such as the [official Meteor website](https://www.meteor.com/) itself. 47 | 1. Open the browser's console 48 | 1. Type `JSON.stringify(Object.keys(Meteor.default_connection._methodHandlers).sort());` to view all methods defined on the client 49 | 1. Type `JSON.stringify(Object.keys(Meteor.default_connection._mongo_livedata_collections).sort());` to view all collections accessible to the client 50 | 1. Type `JSON.stringify(new Meteor.Collection("myCollection")._collection._docs._map);` to look up the contents of any collection called `myCollection` as seen by the client 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-DDP 2 | 3 | This library implements the [Distributed Data Protocol](https://www.meteor.com/ddp) (DDP) from Meteor for clients on Android. 4 | 5 | Connect your native Android apps, written in Java, to apps built with the [Meteor](https://www.meteor.com/) framework and build real-time features. 6 | 7 | ## Motivation 8 | 9 | * Have you built a web application with Meteor? 10 | * Using this library, you can build native Android apps that can talk to your Meteor server and web application. 11 | * Are you primarily an Android developer (who has never heard of Meteor)? 12 | * With "Android-DDP", you can use a Meteor server as your backend for real-time applications on Android. 13 | * Doesn't Meteor provide built-in features for Android app development already? 14 | * With Meteor's built-in features, your Android app will be written in HTML, CSS and JavaScript, wrapped in a `WebView`. It will not be a *native* app. 15 | * By using this library, however, you can write native Android apps in Java while still using Meteor as your real-time backend. 16 | 17 | ## Requirements 18 | 19 | * Android 2.3+ 20 | 21 | ## Installation 22 | 23 | * Add this library to your project 24 | * Declare the Gradle repository in your root `build.gradle` 25 | 26 | ```gradle 27 | allprojects { 28 | repositories { 29 | maven { url "https://jitpack.io" } 30 | } 31 | } 32 | ``` 33 | 34 | * Declare the Gradle dependency in your app module's `build.gradle` 35 | 36 | ```gradle 37 | dependencies { 38 | compile 'com.github.delight-im:Android-DDP:v3.3.1' 39 | } 40 | ``` 41 | 42 | * Add the Internet permission to your app's `AndroidManifest.xml`: 43 | 44 | ```xml 45 | 46 | ``` 47 | 48 | ## Usage 49 | 50 | * Creating a new instance of the DDP client 51 | 52 | ```java 53 | public class MyActivity extends Activity implements MeteorCallback { 54 | 55 | private Meteor mMeteor; 56 | 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | // ... 62 | 63 | // create a new instance 64 | mMeteor = new Meteor(this, "ws://example.meteor.com/websocket"); 65 | 66 | // register the callback that will handle events and receive messages 67 | mMeteor.addCallback(this); 68 | 69 | // establish the connection 70 | mMeteor.connect(); 71 | } 72 | 73 | public void onConnect(boolean signedInAutomatically) { } 74 | 75 | public void onDisconnect() { } 76 | 77 | public void onDataAdded(String collectionName, String documentID, String newValuesJson) { 78 | // parse the JSON and manage the data yourself (not recommended) 79 | // or 80 | // enable a database (see section "Using databases to manage data") (recommended) 81 | } 82 | 83 | public void onDataChanged(String collectionName, String documentID, String updatedValuesJson, String removedValuesJson) { 84 | // parse the JSON and manage the data yourself (not recommended) 85 | // or 86 | // enable a database (see section "Using databases to manage data") (recommended) 87 | } 88 | 89 | public void onDataRemoved(String collectionName, String documentID) { 90 | // parse the JSON and manage the data yourself (not recommended) 91 | // or 92 | // enable a database (see section "Using databases to manage data") (recommended) 93 | } 94 | 95 | public void onException(Exception e) { } 96 | 97 | @Override 98 | public void onDestroy() { 99 | mMeteor.disconnect(); 100 | mMeteor.removeCallback(this); 101 | // or 102 | // mMeteor.removeCallbacks(); 103 | 104 | // ... 105 | 106 | super.onDestroy(); 107 | } 108 | 109 | } 110 | ``` 111 | 112 | * Singleton access 113 | * Creating an instance at the beginning 114 | 115 | ```java 116 | MeteorSingleton.createInstance(this, "ws://example.meteor.com/websocket") 117 | // instead of 118 | // new Meteor(this, "ws://example.meteor.com/websocket") 119 | ``` 120 | 121 | * Accessing the instance afterwards (across `Activity` instances) 122 | 123 | ```java 124 | MeteorSingleton.getInstance() 125 | // instead of 126 | // mMeteor 127 | ``` 128 | 129 | * All other API methods can be called on `MeteorSingleton.getInstance()` just as you would do on any other `Meteor` instance, as documented here with `mMeteor` 130 | 131 | * Registering a callback 132 | 133 | ```java 134 | // MeteorCallback callback; 135 | mMeteor.addCallback(callback); 136 | ``` 137 | 138 | * Unregistering a callback 139 | 140 | ```java 141 | mMeteor.removeCallbacks(); 142 | // or 143 | // // MeteorCallback callback; 144 | // mMeteor.removeCallback(callback); 145 | ``` 146 | 147 | * Available data types 148 | 149 | | JavaScript / JSON | Java / Android | 150 | | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 151 | | **`String`** (e.g. `"John"` or `'Jane'`) | **`String`** (e.g. `"John"` or `"Jane"`) | 152 | | **`Number`** (e.g. `42`) | **`byte`** (e.g. `(byte) 42`) | 153 | | | **`short`** (e.g. `(short) 42`) | 154 | | | **`int`** (e.g. `42`) | 155 | | | **`long`** (e.g. `42L`) | 156 | | | **`float`** (e.g. `3.14f`) | 157 | | | **`double`** (e.g. `3.14`) | 158 | | **`Boolean`** (e.g. `true`) | **`boolean`** (e.g. `true`) | 159 | | **`Array`** (e.g. `[ 7, "Hi", true ]`) | **`Object[]`** (e.g. `new Object[] { 7, "Hi", true }`) | 160 | | | **`List`** (e.g. `List list = new LinkedList(); list.add(7); list.add("Hi"); list.add(true);`) | 161 | | **`Object`** (e.g. `{ "amount": 100, "currency": "USD" }`) | **`Map`** (e.g. `Map map = new HashMap(); map.put("amount", 100); map.put("currency", "USD");`) | 162 | | | **`MyClass`** (e.g. `public class MyClass { public int amount; public String currency; } MyClass myObj = new MyClass(); myObj.amount = 100; myObj.currency = "USD";`) | 163 | | `null` | `null` | 164 | 165 | * Inserting data into a collection 166 | 167 | ```java 168 | Map values = new HashMap(); 169 | values.put("_id", "my-id"); 170 | values.put("some-key", "some-value"); 171 | 172 | mMeteor.insert("my-collection", values); 173 | // or 174 | // mMeteor.insert("my-collection", values, new ResultListener() { }); 175 | ``` 176 | 177 | * Updating data in a collection 178 | 179 | ```java 180 | Map query = new HashMap(); 181 | query.put("_id", "my-id"); 182 | 183 | Map values = new HashMap(); 184 | values.put("some-key", "some-value"); 185 | 186 | mMeteor.update("my-collection", query, values); 187 | // or 188 | // mMeteor.update("my-collection", query, values, options); 189 | // or 190 | // mMeteor.update("my-collection", query, values, options, new ResultListener() { }); 191 | ``` 192 | 193 | * Deleting data from a collection 194 | 195 | ```java 196 | mMeteor.remove("my-collection", "my-id"); 197 | // or 198 | // mMeteor.remove("my-collection", "my-id", new ResultListener() { }); 199 | ``` 200 | 201 | * Subscribing to data from the server 202 | 203 | ```java 204 | String subscriptionId = mMeteor.subscribe("my-subscription"); 205 | // or 206 | // String subscriptionId = mMeteor.subscribe("my-subscription", new Object[] { arg1, arg2 }); 207 | // or 208 | // String subscriptionId = mMeteor.subscribe("my-subscription", new Object[] { arg1, arg2 }, new SubscribeListener() { }); 209 | ``` 210 | 211 | * Unsubscribing from a previously established subscription 212 | 213 | ```java 214 | mMeteor.unsubscribe(subscriptionId); 215 | // or 216 | // mMeteor.unsubscribe(subscriptionId, new UnsubscribeListener() { }); 217 | ``` 218 | 219 | * Calling a custom method defined on the server 220 | 221 | ```java 222 | mMeteor.call("myMethod"); 223 | // or 224 | // mMeteor.call("myMethod", new Object[] { arg1, arg2 }); 225 | // or 226 | // mMeteor.call("myMethod", new ResultListener() { }); 227 | // or 228 | // mMeteor.call("myMethod", new Object[] { arg1, arg2 }, new ResultListener() { }); 229 | ``` 230 | 231 | * Disconnect from the server 232 | 233 | ```java 234 | mMeteor.disconnect(); 235 | ``` 236 | 237 | * Creating a new account (requires `accounts-password` package) 238 | 239 | ```java 240 | mMeteor.registerAndLogin("john", "john.doe@example.com", "password", new ResultListener() { }); 241 | // or 242 | // mMeteor.registerAndLogin("john", "john.doe@example.com", "password", profile, new ResultListener() { }); 243 | ``` 244 | 245 | * Signing in with an existing username (requires `accounts-password` package) 246 | 247 | ```java 248 | mMeteor.loginWithUsername("john", "password", new ResultListener() { }); 249 | ``` 250 | 251 | * Signing in with an existing email address (requires `accounts-password` package) 252 | 253 | ```java 254 | mMeteor.loginWithEmail("john.doe@example.com", "password", new ResultListener() { }); 255 | ``` 256 | 257 | * Check if the client is currently logged in (requires `accounts-password` package) 258 | 259 | ```java 260 | mMeteor.isLoggedIn(); 261 | ``` 262 | 263 | * Get the client's user ID (if currently logged in) (requires `accounts-password` package) 264 | 265 | ```java 266 | mMeteor.getUserId(); 267 | ``` 268 | 269 | * Logging out (requires `accounts-password` package) 270 | 271 | ```java 272 | mMeteor.logout(); 273 | // or 274 | // mMeteor.logout(new ResultListener() { }); 275 | ``` 276 | 277 | * Checking whether the client is connected 278 | 279 | ```java 280 | mMeteor.isConnected(); 281 | ``` 282 | 283 | * Manually attempt to re-connect (if necessary) 284 | 285 | ```java 286 | mMeteor.reconnect(); 287 | ``` 288 | 289 | ## Using databases to manage data 290 | 291 | ### Enabling a database 292 | 293 | Pass an instance of `Database` to the constructor. Right now, the only subclass provided as a built-in database is `InMemoryDatabase`. So the code for the constructor becomes: 294 | 295 | ```java 296 | mMeteor = new Meteor(this, "ws://example.meteor.com/websocket", new InMemoryDatabase()); 297 | ``` 298 | 299 | After that change, all data received from the server will automatically be parsed, updated and managed for you in the built-in database. That means no manual JSON parsing! 300 | 301 | So whenever you receive data notifications via `onDataAdded`, `onDataChanged` or `onDataRemoved`, that data has already been merged into the database and can be retrieved from there. In these callbacks, you can thus ignore the parameters containing JSON data and instead get the data from your database. 302 | 303 | ### Accessing the database 304 | 305 | ```java 306 | Database database = mMeteor.getDatabase(); 307 | ``` 308 | 309 | This method call and most of the following method calls can be chained for simplicity. 310 | 311 | ### Getting a collection from the database by name 312 | 313 | ```java 314 | // String collectionName = "myCollection"; 315 | Collection collection = mMeteor.getDatabase().getCollection(collectionName); 316 | ``` 317 | 318 | ### Retrieving the names of all collections from the database 319 | 320 | ```java 321 | String[] collectionNames = mMeteor.getDatabase().getCollectionNames(); 322 | ``` 323 | 324 | ### Fetching the number of collections from the database 325 | 326 | ```java 327 | int numCollections = mMeteor.getDatabase().count(); 328 | ``` 329 | 330 | ### Getting a document from a collection by ID 331 | 332 | ```java 333 | // String documentId = "wjQvNQ6sGjzLMDyiJ"; 334 | Document document = mMeteor.getDatabase().getCollection(collectionName).getDocument(documentId); 335 | ``` 336 | 337 | ### Retrieving the IDs of all documents from a collection 338 | 339 | ```java 340 | String[] documentIds = mMeteor.getDatabase().getCollection(collectionName).getDocumentIds(); 341 | ``` 342 | 343 | ### Fetching the number of documents from a collection 344 | 345 | ```java 346 | int numDocuments = mMeteor.getDatabase().getCollection(collectionName).count(); 347 | ``` 348 | 349 | ### Querying a collection for documents 350 | 351 | Any of the following method calls can be chained and combined in any way to select documents via complex queries. 352 | 353 | ```java 354 | // String fieldName = "age"; 355 | // int fieldValue = 62; 356 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereEqual(fieldName, fieldValue); 357 | ``` 358 | 359 | ```java 360 | // String fieldName = "active"; 361 | // int fieldValue = false; 362 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereNotEqual(fieldName, fieldValue); 363 | ``` 364 | 365 | ```java 366 | // String fieldName = "accountBalance"; 367 | // float fieldValue = 100000.00f; 368 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereLessThan(fieldName, fieldValue); 369 | ``` 370 | 371 | ```java 372 | // String fieldName = "numChildren"; 373 | // long fieldValue = 3L; 374 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereLessThanOrEqual(fieldName, fieldValue); 375 | ``` 376 | 377 | ```java 378 | // String fieldName = "revenue"; 379 | // double fieldValue = 0.00; 380 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereGreaterThan(fieldName, fieldValue); 381 | ``` 382 | 383 | ```java 384 | // String fieldName = "age"; 385 | // int fieldValue = 21; 386 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereGreaterThanOrEqual(fieldName, fieldValue); 387 | ``` 388 | 389 | ```java 390 | // String fieldName = "address"; 391 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereNull(fieldName); 392 | ``` 393 | 394 | ```java 395 | // String fieldName = "modifiedAt"; 396 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereNotNull(fieldName); 397 | ``` 398 | 399 | ```java 400 | // String fieldName = "age"; 401 | // Integer[] fieldValues = new Integer[] { 60, 70, 80 }; 402 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereIn(fieldName, fieldValues); 403 | ``` 404 | 405 | ```java 406 | // String fieldName = "languageCode"; 407 | // String[] fieldValues = new String[] { "zh", "es", "en", "hi", "ar" }; 408 | Query query = mMeteor.getDatabase().getCollection(collectionName).whereNotIn(fieldName, fieldValues); 409 | ``` 410 | 411 | Any query can be executed by a `find` or `findOne` call. The step of first creating the `Query` instance can be skipped if you chain the calls to execute the query immediately. 412 | 413 | ```java 414 | Document[] documents = mMeteor.getDatabase().getCollection(collectionName).find(); 415 | ``` 416 | 417 | ```java 418 | // int limit = 30; 419 | Document[] documents = mMeteor.getDatabase().getCollection(collectionName).find(limit); 420 | ``` 421 | 422 | ```java 423 | // int limit = 30; 424 | // int offset = 5; 425 | Document[] documents = mMeteor.getDatabase().getCollection(collectionName).find(limit, offset); 426 | ``` 427 | 428 | ```java 429 | Document document = mMeteor.getDatabase().getCollection(collectionName).findOne(); 430 | ``` 431 | 432 | Chained together, these calls may look as follows, for example: 433 | 434 | ```java 435 | Document document = mMeteor.getDatabase().getCollection("users").whereNotNull("lastLoginAt").whereGreaterThan("level", 3).findOne(); 436 | ``` 437 | 438 | ### Getting a field from a document by name 439 | 440 | ```java 441 | // String fieldName = "age"; 442 | Object field = mMeteor.getDatabase().getCollection(collectionName).getDocument(documentId).getField(fieldName); 443 | ``` 444 | 445 | ### Retrieving the names of all fields from a document 446 | 447 | ```java 448 | String[] fieldNames = mMeteor.getDatabase().getCollection(collectionName).getDocument(documentId).getFieldNames(); 449 | ``` 450 | 451 | ### Fetching the number of fields from a document 452 | 453 | ```java 454 | int numFields = mMeteor.getDatabase().getCollection(collectionName).getDocument(documentId).count(); 455 | ``` 456 | 457 | ## Contributing 458 | 459 | All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed. 460 | 461 | ## Dependencies 462 | 463 | * [nv-websocket-client](https://github.com/TakahikoKawasaki/nv-websocket-client) — [Takahiko Kawasaki](https://github.com/TakahikoKawasaki) — [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 464 | * [Jackson Core](https://github.com/FasterXML/jackson-core) — [FasterXML](https://github.com/FasterXML) — [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 465 | * [Jackson ObjectMapper](https://github.com/FasterXML/jackson-databind) — [FasterXML](https://github.com/FasterXML) — [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 466 | 467 | ## Further reading 468 | 469 | * [DDP — Specification](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md) 470 | * [Jackson — Documentation](http://wiki.fasterxml.com/JacksonDocumentation) 471 | 472 | ## Disclaimer 473 | 474 | This project is neither affiliated with nor endorsed by Meteor. 475 | 476 | ## License 477 | 478 | ``` 479 | Copyright (c) delight.im 480 | 481 | Licensed under the Apache License, Version 2.0 (the "License"); 482 | you may not use this file except in compliance with the License. 483 | You may obtain a copy of the License at 484 | 485 | http://www.apache.org/licenses/LICENSE-2.0 486 | 487 | Unless required by applicable law or agreed to in writing, software 488 | distributed under the License is distributed on an "AS IS" BASIS, 489 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 490 | See the License for the specific language governing permissions and 491 | limitations under the License. 492 | ``` 493 | -------------------------------------------------------------------------------- /Source/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Source/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # Custom 35 | .idea/ 36 | *.iml 37 | -------------------------------------------------------------------------------- /Source/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.2.3' 8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Source/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 14 22:25:00 CET 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /Source/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /Source/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /Source/library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | android { 5 | compileSdkVersion 19 6 | buildToolsVersion "25.0.1" 7 | 8 | defaultConfig { 9 | minSdkVersion 9 10 | targetSdkVersion 19 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | 20 | packagingOptions { 21 | exclude 'META-INF/LICENSE' 22 | exclude 'META-INF/NOTICE' 23 | exclude 'META-INF/ASL2.0' 24 | } 25 | } 26 | 27 | dependencies { 28 | compile files('libs/jackson-core-asl-1.9.7.jar') 29 | compile files('libs/jackson-mapper-asl-1.9.7.jar') 30 | compile 'com.neovisionaries:nv-websocket-client:1.31' 31 | } 32 | 33 | task sourcesJar(type: Jar) { 34 | from android.sourceSets.main.java.srcDirs 35 | classifier = 'sources' 36 | } 37 | 38 | task javadoc(type: Javadoc) { 39 | failOnError false 40 | source = android.sourceSets.main.java.sourceFiles 41 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 42 | classpath += configurations.compile 43 | } 44 | 45 | task javadocJar(type: Jar, dependsOn: javadoc) { 46 | classifier = 'javadoc' 47 | from javadoc.destinationDir 48 | } 49 | 50 | artifacts { 51 | archives sourcesJar 52 | archives javadocJar 53 | } 54 | -------------------------------------------------------------------------------- /Source/library/libs/jackson-core-asl-1.9.7.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/library/libs/jackson-core-asl-1.9.7.jar -------------------------------------------------------------------------------- /Source/library/libs/jackson-mapper-asl-1.9.7.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/library/libs/jackson-mapper-asl-1.9.7.jar -------------------------------------------------------------------------------- /Source/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/CallbackProxy.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import android.os.Handler; 20 | import android.os.Looper; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | 24 | /** Wrapper that executes all registered callbacks on the correct thread behind the scenes */ 25 | public class CallbackProxy implements MeteorCallback { 26 | 27 | private final List mCallbacks = new LinkedList(); 28 | private final Handler mUiHandler = new Handler(Looper.getMainLooper()); 29 | 30 | public CallbackProxy() { } 31 | 32 | public void addCallback(final MeteorCallback callback) { 33 | mCallbacks.add(callback); 34 | } 35 | 36 | public void removeCallback(final MeteorCallback callback) { 37 | mCallbacks.remove(callback); 38 | } 39 | 40 | public void removeCallbacks() { 41 | mCallbacks.clear(); 42 | } 43 | 44 | @Override 45 | public void onConnect(final boolean signedInAutomatically) { 46 | // iterate over all the registered callbacks 47 | for (final MeteorCallback callback : mCallbacks) { 48 | // if the callback exists 49 | if (callback != null) { 50 | // execute the callback on the main thread 51 | mUiHandler.post(new Runnable() { 52 | 53 | @Override 54 | public void run() { 55 | // run the proxied method with the same parameters 56 | callback.onConnect(signedInAutomatically); 57 | } 58 | 59 | }); 60 | } 61 | } 62 | } 63 | 64 | @Override 65 | public void onDisconnect() { 66 | // iterate over all the registered callbacks 67 | for (final MeteorCallback callback : mCallbacks) { 68 | // if the callback exists 69 | if (callback != null) { 70 | // execute the callback on the main thread 71 | mUiHandler.post(new Runnable() { 72 | 73 | @Override 74 | public void run() { 75 | // run the proxied method with the same parameters 76 | callback.onDisconnect(); 77 | } 78 | 79 | }); 80 | } 81 | } 82 | } 83 | 84 | @Override 85 | public void onDataAdded(final String collectionName, final String documentID, final String newValuesJson) { 86 | // iterate over all the registered callbacks 87 | for (final MeteorCallback callback : mCallbacks) { 88 | // if the callback exists 89 | if (callback != null) { 90 | // execute the callback on the main thread 91 | mUiHandler.post(new Runnable() { 92 | 93 | @Override 94 | public void run() { 95 | // run the proxied method with the same parameters 96 | callback.onDataAdded(collectionName, documentID, newValuesJson); 97 | } 98 | 99 | }); 100 | } 101 | } 102 | } 103 | 104 | @Override 105 | public void onDataChanged(final String collectionName, final String documentID, final String updatedValuesJson, final String removedValuesJson) { 106 | // iterate over all the registered callbacks 107 | for (final MeteorCallback callback : mCallbacks) { 108 | // if the callback exists 109 | if (callback != null) { 110 | // execute the callback on the main thread 111 | mUiHandler.post(new Runnable() { 112 | 113 | @Override 114 | public void run() { 115 | // run the proxied method with the same parameters 116 | callback.onDataChanged(collectionName, documentID, updatedValuesJson, removedValuesJson); 117 | } 118 | 119 | }); 120 | } 121 | } 122 | } 123 | 124 | @Override 125 | public void onDataRemoved(final String collectionName, final String documentID) { 126 | // iterate over all the registered callbacks 127 | for (final MeteorCallback callback : mCallbacks) { 128 | // if the callback exists 129 | if (callback != null) { 130 | // execute the callback on the main thread 131 | mUiHandler.post(new Runnable() { 132 | 133 | @Override 134 | public void run() { 135 | // run the proxied method with the same parameters 136 | callback.onDataRemoved(collectionName, documentID); 137 | } 138 | 139 | }); 140 | } 141 | } 142 | } 143 | 144 | @Override 145 | public void onException(final Exception e) { 146 | // iterate over all the registered callbacks 147 | for (final MeteorCallback callback : mCallbacks) { 148 | // if the callback exists 149 | if (callback != null) { 150 | // execute the callback on the main thread 151 | mUiHandler.post(new Runnable() { 152 | 153 | @Override 154 | public void run() { 155 | // run the proxied method with the same parameters 156 | callback.onException(e); 157 | } 158 | 159 | }); 160 | } 161 | } 162 | } 163 | 164 | public ResultListener forResultListener(final ResultListener callback) { 165 | return new ResultListener() { 166 | 167 | @Override 168 | public void onSuccess(final String result) { 169 | // if the callback exists 170 | if (callback != null) { 171 | // execute the callback on the main thread 172 | mUiHandler.post(new Runnable() { 173 | 174 | @Override 175 | public void run() { 176 | // run the proxied method with the same parameters 177 | callback.onSuccess(result); 178 | } 179 | 180 | }); 181 | } 182 | } 183 | 184 | @Override 185 | public void onError(final String error, final String reason, final String details) { 186 | // if the callback exists 187 | if (callback != null) { 188 | // execute the callback on the main thread 189 | mUiHandler.post(new Runnable() { 190 | 191 | @Override 192 | public void run() { 193 | // run the proxied method with the same parameters 194 | callback.onError(error, reason, details); 195 | } 196 | 197 | }); 198 | } 199 | } 200 | 201 | }; 202 | } 203 | 204 | public SubscribeListener forSubscribeListener(final SubscribeListener callback) { 205 | return new SubscribeListener() { 206 | 207 | @Override 208 | public void onSuccess() { 209 | // if the callback exists 210 | if (callback != null) { 211 | // execute the callback on the main thread 212 | mUiHandler.post(new Runnable() { 213 | 214 | @Override 215 | public void run() { 216 | // run the proxied method with the same parameters 217 | callback.onSuccess(); 218 | } 219 | 220 | }); 221 | } 222 | } 223 | 224 | @Override 225 | public void onError(final String error, final String reason, final String details) { 226 | // if the callback exists 227 | if (callback != null) { 228 | // execute the callback on the main thread 229 | mUiHandler.post(new Runnable() { 230 | 231 | @Override 232 | public void run() { 233 | // run the proxied method with the same parameters 234 | callback.onError(error, reason, details); 235 | } 236 | 237 | }); 238 | } 239 | } 240 | 241 | }; 242 | } 243 | 244 | public UnsubscribeListener forUnsubscribeListener(final UnsubscribeListener callback) { 245 | return new UnsubscribeListener() { 246 | 247 | @Override 248 | public void onSuccess() { 249 | // if the callback exists 250 | if (callback != null) { 251 | // execute the callback on the main thread 252 | mUiHandler.post(new Runnable() { 253 | 254 | @Override 255 | public void run() { 256 | // run the proxied method with the same parameters 257 | callback.onSuccess(); 258 | } 259 | 260 | }); 261 | } 262 | } 263 | 264 | }; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/DdpCallback.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** Callbacks for all database-related events received from a DDP server */ 20 | public interface DdpCallback { 21 | 22 | /** 23 | * Callback that is executed whenever a new document is added to a collection 24 | * 25 | * @param collectionName the name of the collection that the document is added to 26 | * @param documentID the ID of the document that is being added 27 | * @param newValuesJson the new fields of the document as a JSON string 28 | */ 29 | void onDataAdded(String collectionName, String documentID, String newValuesJson); 30 | 31 | /** 32 | * Callback that is executed whenever an existing document is changed in a collection 33 | * 34 | * @param collectionName the name of the collection that the document is changed in 35 | * @param documentID the ID of the document that is being changed 36 | * @param updatedValuesJson the modified fields of the document as a JSON string 37 | * @param removedValuesJson the deleted fields of the document as a JSON string 38 | */ 39 | void onDataChanged(String collectionName, String documentID, String updatedValuesJson, String removedValuesJson); 40 | 41 | /** 42 | * Callback that is executed whenever an existing document is removed from a collection 43 | * 44 | * @param collectionName the name of the collection that the document is removed from 45 | * @param documentID the ID of the document that is being removed 46 | */ 47 | void onDataRemoved(String collectionName, String documentID); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/Fields.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | public class Fields extends HashMap implements Map { } 23 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/Listener.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | public interface Listener { } 20 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/Meteor.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.db.DataStore; 20 | import im.delight.android.ddp.db.Database; 21 | import android.content.SharedPreferences; 22 | import android.content.Context; 23 | import com.neovisionaries.ws.client.WebSocket; 24 | import com.neovisionaries.ws.client.WebSocketAdapter; 25 | import com.neovisionaries.ws.client.WebSocketException; 26 | import com.neovisionaries.ws.client.WebSocketFactory; 27 | import com.neovisionaries.ws.client.WebSocketFrame; 28 | import com.neovisionaries.ws.client.WebSocketListener; 29 | import com.neovisionaries.ws.client.WebSocketState; 30 | import java.util.List; 31 | import java.util.concurrent.ConcurrentLinkedQueue; 32 | import java.util.Queue; 33 | import java.util.Iterator; 34 | import org.codehaus.jackson.map.ObjectMapper; 35 | import java.util.UUID; 36 | import java.util.Arrays; 37 | import java.io.IOException; 38 | import org.codehaus.jackson.JsonProcessingException; 39 | import org.codehaus.jackson.JsonNode; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | /** Client that connects to Meteor servers implementing the DDP protocol */ 44 | public class Meteor { 45 | 46 | private static final String TAG = "Meteor"; 47 | /** Supported versions of the DDP protocol in order of preference */ 48 | private static final String[] SUPPORTED_DDP_VERSIONS = { "1", "pre2", "pre1" }; 49 | /** The maximum number of attempts to re-connect to the server over WebSocket */ 50 | private static final int RECONNECT_ATTEMPTS_MAX = 5; 51 | /** Instance of Jackson library's ObjectMapper that converts between JSON and Java objects (POJOs) */ 52 | private static final ObjectMapper mObjectMapper = new ObjectMapper(); 53 | /** The WebSocket connection that will be used for the data transfer */ 54 | private WebSocket mWebSocket; 55 | /** The callback that handles messages and events received from the WebSocket connection */ 56 | private final WebSocketListener mWebSocketListener; 57 | /** Map that tracks all pending Listener instances */ 58 | private final Map mListeners; 59 | /** Messages that couldn't be dispatched yet and thus had to be queued */ 60 | private final Queue mQueuedMessages; 61 | private final Context mContext; 62 | /** Whether logging should be enabled or not */ 63 | private static boolean mLoggingEnabled; 64 | private String mServerUri; 65 | private String mDdpVersion; 66 | /** The number of unsuccessful attempts to re-connect in sequence */ 67 | private int mReconnectAttempts; 68 | /** The callbacks that will handle events and receive messages from this client */ 69 | protected final CallbackProxy mCallbackProxy = new CallbackProxy(); 70 | private String mSessionID; 71 | private boolean mConnected; 72 | private String mLoggedInUserId; 73 | private final DataStore mDataStore; 74 | 75 | /** 76 | * Returns a new instance for a client connecting to a server via DDP over websocket 77 | * 78 | * The server URI should usually be in the form of `ws://example.meteor.com/websocket` 79 | * 80 | * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) 81 | * @param serverUri the server URI to connect to 82 | */ 83 | public Meteor(final Context context, final String serverUri) { 84 | this(context, serverUri, (DataStore) null); 85 | } 86 | 87 | /** 88 | * Returns a new instance for a client connecting to a server via DDP over websocket 89 | * 90 | * The server URI should usually be in the form of `ws://example.meteor.com/websocket` 91 | * 92 | * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) 93 | * @param serverUri the server URI to connect to 94 | * @param dataStore the data store to write data to 95 | */ 96 | public Meteor(final Context context, final String serverUri, final DataStore dataStore) { 97 | this(context, serverUri, SUPPORTED_DDP_VERSIONS[0], dataStore); 98 | } 99 | 100 | /** 101 | * Returns a new instance for a client connecting to a server via DDP over websocket 102 | * 103 | * The server URI should usually be in the form of `ws://example.meteor.com/websocket` 104 | * 105 | * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) 106 | * @param serverUri the server URI to connect to 107 | * @param protocolVersion the desired DDP protocol version 108 | */ 109 | public Meteor(final Context context, final String serverUri, final String protocolVersion) { 110 | this(context, serverUri, protocolVersion, null); 111 | } 112 | 113 | /** 114 | * Returns a new instance for a client connecting to a server via DDP over websocket 115 | * 116 | * The server URI should usually be in the form of `ws://example.meteor.com/websocket` 117 | * 118 | * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) 119 | * @param serverUri the server URI to connect to 120 | * @param protocolVersion the desired DDP protocol version 121 | * @param dataStore the data store to write data to 122 | */ 123 | public Meteor(final Context context, final String serverUri, final String protocolVersion, final DataStore dataStore) { 124 | if (!isVersionSupported(protocolVersion)) { 125 | throw new IllegalArgumentException("DDP protocol version not supported: "+protocolVersion); 126 | } 127 | 128 | if (context == null) { 129 | throw new IllegalArgumentException("The context reference may not be null"); 130 | } 131 | 132 | // save the context reference 133 | mContext = context.getApplicationContext(); 134 | 135 | // save the data store reference 136 | mDataStore = dataStore; 137 | 138 | // create a new handler that processes the messages and events received from the WebSocket connection 139 | mWebSocketListener = new WebSocketAdapter() { 140 | 141 | @Override 142 | public void onConnected(final WebSocket websocket, final Map> headers) { 143 | log(TAG); 144 | log(" onOpen"); 145 | 146 | mConnected = true; 147 | mReconnectAttempts = 0; 148 | 149 | initConnection(mSessionID); 150 | } 151 | 152 | @Override 153 | public void onDisconnected(final WebSocket websocket, final WebSocketFrame serverCloseFrame, final WebSocketFrame clientCloseFrame, final boolean closedByServer) { 154 | log(TAG); 155 | log(" onClose"); 156 | 157 | final boolean lostConnection = mConnected; 158 | 159 | mConnected = false; 160 | 161 | if (lostConnection) { 162 | mReconnectAttempts++; 163 | 164 | if (mReconnectAttempts <= RECONNECT_ATTEMPTS_MAX) { 165 | // try to re-connect automatically 166 | reconnect(); 167 | } 168 | else { 169 | disconnect(); 170 | } 171 | } 172 | 173 | mCallbackProxy.onDisconnect(); 174 | } 175 | 176 | @Override 177 | public void onTextMessage(final WebSocket websocket, final String text) { 178 | log(TAG); 179 | log(" onTextMessage"); 180 | log(" payload == "+text); 181 | 182 | handleMessage(text); 183 | } 184 | 185 | @Override 186 | public void onStateChanged(final WebSocket websocket, final WebSocketState newState) {} 187 | 188 | @Override 189 | public void handleCallbackError(final WebSocket websocket, final Throwable cause) { 190 | mCallbackProxy.onException(new Exception(cause)); 191 | } 192 | 193 | @Override 194 | public void onError(final WebSocket websocket, final WebSocketException cause) { 195 | mCallbackProxy.onException(new Exception(cause)); 196 | } 197 | 198 | }; 199 | 200 | // create a map that holds the pending Listener instances 201 | mListeners = new HashMap(); 202 | 203 | // create a queue that holds undispatched messages waiting to be sent 204 | mQueuedMessages = new ConcurrentLinkedQueue(); 205 | 206 | // save the server URI 207 | mServerUri = serverUri; 208 | 209 | // try with the preferred DDP protocol version first 210 | mDdpVersion = protocolVersion; 211 | 212 | // count the number of failed attempts to re-connect 213 | mReconnectAttempts = 0; 214 | } 215 | 216 | /** Attempts to establish the connection to the server */ 217 | public void connect() { 218 | openConnection(false); 219 | } 220 | 221 | /** 222 | * Returns whether this client is connected or not 223 | * 224 | * @return whether this client is connected 225 | */ 226 | public boolean isConnected() { 227 | return mConnected; 228 | } 229 | 230 | /** Manually attempt to re-connect if necessary */ 231 | public void reconnect() { 232 | openConnection(true); 233 | } 234 | 235 | /** 236 | * Opens a connection to the server over websocket 237 | * 238 | * @param isReconnect whether this is a re-connect attempt or not 239 | */ 240 | private void openConnection(final boolean isReconnect) { 241 | if (isReconnect) { 242 | if (mConnected) { 243 | initConnection(mSessionID); 244 | return; 245 | } 246 | } 247 | 248 | // create a new WebSocket connection for the data transfer 249 | try { 250 | mWebSocket = new WebSocketFactory().setConnectionTimeout(30000).createSocket(mServerUri); 251 | } 252 | catch (final IOException e) { 253 | mCallbackProxy.onException(e); 254 | } 255 | 256 | mWebSocket.setMissingCloseFrameAllowed(true); 257 | mWebSocket.setPingInterval(25 * 1000); 258 | mWebSocket.addListener(mWebSocketListener); 259 | mWebSocket.connectAsynchronously(); 260 | } 261 | 262 | /** 263 | * Establish the connection to the server as requested by the DDP protocol (after the websocket has been opened) 264 | * 265 | * @param existingSessionID an existing session ID or `null` 266 | */ 267 | private void initConnection(final String existingSessionID) { 268 | final Map data = new HashMap(); 269 | 270 | data.put(Protocol.Field.MESSAGE, Protocol.Message.CONNECT); 271 | data.put(Protocol.Field.VERSION, mDdpVersion); 272 | data.put(Protocol.Field.SUPPORT, SUPPORTED_DDP_VERSIONS); 273 | 274 | if (existingSessionID != null) { 275 | data.put(Protocol.Field.SESSION, existingSessionID); 276 | } 277 | 278 | send(data); 279 | } 280 | 281 | /** Disconnect the client from the server */ 282 | public void disconnect() { 283 | mConnected = false; 284 | mListeners.clear(); 285 | mSessionID = null; 286 | 287 | if (mWebSocket != null) { 288 | try { 289 | mWebSocket.disconnect(); 290 | } 291 | catch (Exception e) { 292 | mCallbackProxy.onException(e); 293 | } 294 | } 295 | else { 296 | throw new IllegalStateException("You must have called the 'connect' method before you can disconnect again"); 297 | } 298 | } 299 | 300 | /** 301 | * Sends a Java object (POJO) over the websocket after serializing it with the Jackson library 302 | * 303 | * @param obj the Java object to send 304 | */ 305 | private void send(final Object obj) { 306 | // serialize the object to JSON 307 | final String jsonStr = toJson(obj); 308 | 309 | if (jsonStr == null) { 310 | throw new IllegalArgumentException("Object would be serialized to `null`"); 311 | } 312 | 313 | // send the JSON string 314 | send(jsonStr); 315 | } 316 | 317 | /** 318 | * Sends a string over the websocket 319 | * 320 | * @param message the string to send 321 | */ 322 | private void send(final String message) { 323 | log(TAG); 324 | log(" send"); 325 | log(" message == "+message); 326 | 327 | if (message == null) { 328 | throw new IllegalArgumentException("You cannot send `null` messages"); 329 | } 330 | 331 | if (mConnected) { 332 | log(" dispatching"); 333 | 334 | if (mWebSocket != null) { 335 | mWebSocket.sendText(message); 336 | } 337 | else { 338 | throw new IllegalStateException("You must have called the 'connect' method before you can send data"); 339 | } 340 | } 341 | else { 342 | log(" queueing"); 343 | mQueuedMessages.add(message); 344 | } 345 | } 346 | 347 | /** 348 | * Adds a callback that will handle events and receive messages from this client 349 | * 350 | * @param callback the callback instance 351 | */ 352 | public void addCallback(MeteorCallback callback) { 353 | mCallbackProxy.addCallback(callback); 354 | } 355 | 356 | /** 357 | * Removes a callback that was to handle events and receive messages from this client 358 | * 359 | * @param callback the callback instance 360 | */ 361 | public void removeCallback(MeteorCallback callback) { 362 | mCallbackProxy.removeCallback(callback); 363 | } 364 | 365 | /** Removes all callbacks that were to handle events and receive messages from this client */ 366 | public void removeCallbacks() { 367 | mCallbackProxy.removeCallbacks(); 368 | } 369 | 370 | /** 371 | * Serializes the given Java object (POJO) with the Jackson library 372 | * 373 | * @param obj the object to serialize 374 | * @return the serialized object in JSON format 375 | */ 376 | private String toJson(Object obj) { 377 | try { 378 | return mObjectMapper.writeValueAsString(obj); 379 | } 380 | catch (Exception e) { 381 | mCallbackProxy.onException(e); 382 | 383 | return null; 384 | } 385 | } 386 | 387 | private T fromJson(final String json, final Class targetType) { 388 | try { 389 | if (json != null) { 390 | final JsonNode jsonNode = mObjectMapper.readTree(json); 391 | 392 | return mObjectMapper.convertValue(jsonNode, targetType); 393 | } 394 | else { 395 | return null; 396 | } 397 | } 398 | catch (Exception e) { 399 | mCallbackProxy.onException(e); 400 | 401 | return null; 402 | } 403 | } 404 | 405 | /** 406 | * Called whenever a JSON payload has been received from the websocket 407 | * 408 | * @param payload the JSON payload to process 409 | */ 410 | private void handleMessage(final String payload) { 411 | final JsonNode data; 412 | 413 | try { 414 | data = mObjectMapper.readTree(payload); 415 | } 416 | catch (JsonProcessingException e) { 417 | mCallbackProxy.onException(e); 418 | 419 | return; 420 | } 421 | catch (IOException e) { 422 | mCallbackProxy.onException(e); 423 | 424 | return; 425 | } 426 | 427 | if (data != null) { 428 | if (data.has(Protocol.Field.MESSAGE)) { 429 | final String message = data.get(Protocol.Field.MESSAGE).getTextValue(); 430 | 431 | if (message.equals(Protocol.Message.CONNECTED)) { 432 | if (data.has(Protocol.Field.SESSION)) { 433 | mSessionID = data.get(Protocol.Field.SESSION).getTextValue(); 434 | } 435 | 436 | // initialize the new session 437 | initSession(); 438 | } 439 | else if (message.equals(Protocol.Message.FAILED)) { 440 | if (data.has(Protocol.Field.VERSION)) { 441 | // the server wants to use a different protocol version 442 | final String desiredVersion = data.get(Protocol.Field.VERSION).getTextValue(); 443 | 444 | // if the protocol version that was requested by the server is supported by this client 445 | if (isVersionSupported(desiredVersion)) { 446 | // remember which version has been requested 447 | mDdpVersion = desiredVersion; 448 | 449 | // the server should be closing the connection now and we will re-connect afterwards 450 | } 451 | else { 452 | throw new RuntimeException("Protocol version not supported: "+desiredVersion); 453 | } 454 | } 455 | } 456 | else if (message.equals(Protocol.Message.PING)) { 457 | final String id; 458 | 459 | if (data.has(Protocol.Field.ID)) { 460 | id = data.get(Protocol.Field.ID).getTextValue(); 461 | } 462 | else { 463 | id = null; 464 | } 465 | 466 | sendPong(id); 467 | } 468 | else if (message.equals(Protocol.Message.ADDED) || message.equals(Protocol.Message.ADDED_BEFORE)) { 469 | final String documentID; 470 | 471 | if (data.has(Protocol.Field.ID)) { 472 | documentID = data.get(Protocol.Field.ID).getTextValue(); 473 | } 474 | else { 475 | documentID = null; 476 | } 477 | 478 | final String collectionName; 479 | 480 | if (data.has(Protocol.Field.COLLECTION)) { 481 | collectionName = data.get(Protocol.Field.COLLECTION).getTextValue(); 482 | } 483 | else { 484 | collectionName = null; 485 | } 486 | 487 | final String newValuesJson; 488 | 489 | if (data.has(Protocol.Field.FIELDS)) { 490 | newValuesJson = data.get(Protocol.Field.FIELDS).toString(); 491 | } 492 | else { 493 | newValuesJson = null; 494 | } 495 | 496 | if (mDataStore != null) { 497 | mDataStore.onDataAdded(collectionName, documentID, fromJson(newValuesJson, Fields.class)); 498 | } 499 | 500 | mCallbackProxy.onDataAdded(collectionName, documentID, newValuesJson); 501 | } 502 | else if (message.equals(Protocol.Message.CHANGED)) { 503 | final String documentID; 504 | 505 | if (data.has(Protocol.Field.ID)) { 506 | documentID = data.get(Protocol.Field.ID).getTextValue(); 507 | } 508 | else { 509 | documentID = null; 510 | } 511 | 512 | final String collectionName; 513 | 514 | if (data.has(Protocol.Field.COLLECTION)) { 515 | collectionName = data.get(Protocol.Field.COLLECTION).getTextValue(); 516 | } 517 | else { 518 | collectionName = null; 519 | } 520 | 521 | final String updatedValuesJson; 522 | 523 | if (data.has(Protocol.Field.FIELDS)) { 524 | updatedValuesJson = data.get(Protocol.Field.FIELDS).toString(); 525 | } 526 | else { 527 | updatedValuesJson = null; 528 | } 529 | 530 | final String removedValuesJson; 531 | 532 | if (data.has(Protocol.Field.CLEARED)) { 533 | removedValuesJson = data.get(Protocol.Field.CLEARED).toString(); 534 | } 535 | else { 536 | removedValuesJson = null; 537 | } 538 | 539 | if (mDataStore != null) { 540 | mDataStore.onDataChanged(collectionName, documentID, fromJson(updatedValuesJson, Fields.class), fromJson(removedValuesJson, String[].class)); 541 | } 542 | 543 | mCallbackProxy.onDataChanged(collectionName, documentID, updatedValuesJson, removedValuesJson); 544 | } 545 | else if (message.equals(Protocol.Message.REMOVED)) { 546 | final String documentID; 547 | 548 | if (data.has(Protocol.Field.ID)) { 549 | documentID = data.get(Protocol.Field.ID).getTextValue(); 550 | } 551 | else { 552 | documentID = null; 553 | } 554 | 555 | final String collectionName; 556 | 557 | if (data.has(Protocol.Field.COLLECTION)) { 558 | collectionName = data.get(Protocol.Field.COLLECTION).getTextValue(); 559 | } 560 | else { 561 | collectionName = null; 562 | } 563 | 564 | if (mDataStore != null) { 565 | mDataStore.onDataRemoved(collectionName, documentID); 566 | } 567 | 568 | mCallbackProxy.onDataRemoved(collectionName, documentID); 569 | } 570 | else if (message.equals(Protocol.Message.RESULT)) { 571 | // check if we have to process any result data internally 572 | if (data.has(Protocol.Field.RESULT)) { 573 | final JsonNode resultData = data.get(Protocol.Field.RESULT); 574 | 575 | // if the result is from a previous login attempt 576 | if (isLoginResult(resultData)) { 577 | // extract the login token for subsequent automatic re-login 578 | final String loginToken = resultData.get(Protocol.Field.TOKEN).getTextValue(); 579 | saveLoginToken(loginToken); 580 | 581 | // extract the user's ID 582 | mLoggedInUserId = resultData.get(Protocol.Field.ID).getTextValue(); 583 | } 584 | } 585 | 586 | final String id; 587 | 588 | if (data.has(Protocol.Field.ID)) { 589 | id = data.get(Protocol.Field.ID).getTextValue(); 590 | } 591 | else { 592 | id = null; 593 | } 594 | 595 | final Listener listener = mListeners.get(id); 596 | 597 | if (listener instanceof ResultListener) { 598 | mListeners.remove(id); 599 | 600 | final String result; 601 | 602 | if (data.has(Protocol.Field.RESULT)) { 603 | result = data.get(Protocol.Field.RESULT).toString(); 604 | } 605 | else { 606 | result = null; 607 | } 608 | 609 | if (data.has(Protocol.Field.ERROR)) { 610 | final Protocol.Error error = Protocol.Error.fromJson(data.get(Protocol.Field.ERROR)); 611 | mCallbackProxy.forResultListener((ResultListener) listener).onError(error.getError(), error.getReason(), error.getDetails()); 612 | } 613 | else { 614 | mCallbackProxy.forResultListener((ResultListener) listener).onSuccess(result); 615 | } 616 | } 617 | } 618 | else if (message.equals(Protocol.Message.READY)) { 619 | if (data.has(Protocol.Field.SUBS)) { 620 | final Iterator elements = data.get(Protocol.Field.SUBS).getElements(); 621 | String subscriptionId; 622 | 623 | while (elements.hasNext()) { 624 | subscriptionId = elements.next().getTextValue(); 625 | 626 | final Listener listener = mListeners.get(subscriptionId); 627 | 628 | if (listener instanceof SubscribeListener) { 629 | mListeners.remove(subscriptionId); 630 | 631 | mCallbackProxy.forSubscribeListener((SubscribeListener) listener).onSuccess(); 632 | } 633 | } 634 | } 635 | } 636 | else if (message.equals(Protocol.Message.NOSUB)) { 637 | final String subscriptionId; 638 | 639 | if (data.has(Protocol.Field.ID)) { 640 | subscriptionId = data.get(Protocol.Field.ID).getTextValue(); 641 | } 642 | else { 643 | subscriptionId = null; 644 | } 645 | 646 | final Listener listener = mListeners.get(subscriptionId); 647 | 648 | if (listener instanceof SubscribeListener) { 649 | mListeners.remove(subscriptionId); 650 | 651 | if (data.has(Protocol.Field.ERROR)) { 652 | final Protocol.Error error = Protocol.Error.fromJson(data.get(Protocol.Field.ERROR)); 653 | mCallbackProxy.forSubscribeListener((SubscribeListener) listener).onError(error.getError(), error.getReason(), error.getDetails()); 654 | } 655 | else { 656 | mCallbackProxy.forSubscribeListener((SubscribeListener) listener).onError(null, null, null); 657 | } 658 | } 659 | else if (listener instanceof UnsubscribeListener) { 660 | mListeners.remove(subscriptionId); 661 | 662 | mCallbackProxy.forUnsubscribeListener((UnsubscribeListener) listener).onSuccess(); 663 | } 664 | } 665 | } 666 | } 667 | } 668 | 669 | /** 670 | * Returns whether the given JSON result is from a previous login attempt 671 | * 672 | * @param result the JSON result 673 | * @return whether the result is from a login attempt (`true`) or not (`false`) 674 | */ 675 | private static boolean isLoginResult(final JsonNode result) { 676 | return result.has(Protocol.Field.TOKEN) && result.has(Protocol.Field.ID); 677 | } 678 | 679 | /** 680 | * Returns whether the client is currently logged in as some user 681 | * 682 | * @return whether the client is logged in (`true`) or not (`false`) 683 | */ 684 | public boolean isLoggedIn() { 685 | return mLoggedInUserId != null; 686 | } 687 | 688 | /** 689 | * Returns the ID of the user who is currently logged in 690 | * 691 | * @return the ID or `null` 692 | */ 693 | public String getUserId() { 694 | return mLoggedInUserId; 695 | } 696 | 697 | /** 698 | * Returns whether the specified version of the DDP protocol is supported or not 699 | * 700 | * @param protocolVersion the DDP protocol version 701 | * @return whether the version is supported or not 702 | */ 703 | public static boolean isVersionSupported(final String protocolVersion) { 704 | return Arrays.asList(SUPPORTED_DDP_VERSIONS).contains(protocolVersion); 705 | } 706 | 707 | /** 708 | * Sends a `pong` over the websocket as a reply to an incoming `ping` 709 | * 710 | * @param id the ID extracted from the `ping` or `null` 711 | */ 712 | private void sendPong(final String id) { 713 | final Map data = new HashMap(); 714 | 715 | data.put(Protocol.Field.MESSAGE, Protocol.Message.PONG); 716 | 717 | if (id != null) { 718 | data.put(Protocol.Field.ID, id); 719 | } 720 | 721 | send(data); 722 | } 723 | 724 | /** 725 | * Sets whether logging of internal events and data flow should be enabled for this library 726 | * 727 | * @param enabled whether logging should be enabled (`true`) or not (`false`) 728 | */ 729 | public static void setLoggingEnabled(final boolean enabled) { 730 | mLoggingEnabled = enabled; 731 | } 732 | 733 | /** 734 | * Logs a message if logging has been enabled 735 | * 736 | * @param message the message to log 737 | */ 738 | public static void log(final String message) { 739 | if (mLoggingEnabled) { 740 | System.out.println(message); 741 | } 742 | } 743 | 744 | /** 745 | * Creates and returns a new unique ID 746 | * 747 | * @return the new unique ID 748 | */ 749 | public static String uniqueID() { 750 | return UUID.randomUUID().toString(); 751 | } 752 | 753 | /** 754 | * Insert given data into the specified collection 755 | * 756 | * @param collectionName the collection to insert the data into 757 | * @param data the data to insert 758 | */ 759 | public void insert(final String collectionName, final Map data) { 760 | insert(collectionName, data, null); 761 | } 762 | 763 | /** 764 | * Insert given data into the specified collection 765 | * 766 | * @param collectionName the collection to insert the data into 767 | * @param data the data to insert 768 | * @param listener the listener to call on success/error 769 | */ 770 | public void insert(final String collectionName, final Map data, final ResultListener listener) { 771 | call("/"+collectionName+"/insert", new Object[] { data }, listener); 772 | } 773 | 774 | /** 775 | * Insert given data into the specified collection 776 | * 777 | * @param collectionName the collection to insert the data into 778 | * @param query the query to select the document to update with 779 | * @param data the list of keys and values that should be set 780 | */ 781 | public void update(final String collectionName, final Map query, final Map data) { 782 | update(collectionName, query, data, emptyMap()); 783 | } 784 | 785 | /** 786 | * Insert given data into the specified collection 787 | * 788 | * @param collectionName the collection to insert the data into 789 | * @param query the query to select the document to update with 790 | * @param data the list of keys and values that should be set 791 | * @param options the list of option parameters 792 | */ 793 | public void update(final String collectionName, final Map query, final Map data, final Map options) { 794 | update(collectionName, query, data, options, null); 795 | } 796 | 797 | /** 798 | * Insert given data into the specified collection 799 | * 800 | * @param collectionName the collection to insert the data into 801 | * @param query the query to select the document to update with 802 | * @param data the list of keys and values that should be set 803 | * @param options the list of option parameters 804 | * @param listener the listener to call on success/error 805 | */ 806 | public void update(final String collectionName, final Map query, final Map data, final Map options, final ResultListener listener) { 807 | call("/"+collectionName+"/update", new Object[] { query, data, options }, listener); 808 | } 809 | 810 | /** 811 | * Insert given data into the specified collection 812 | * 813 | * @param collectionName the collection to insert the data into 814 | * @param documentID the ID of the document to remove 815 | */ 816 | public void remove(final String collectionName, final String documentID) { 817 | remove(collectionName, documentID, null); 818 | } 819 | 820 | /** 821 | * Insert given data into the specified collection 822 | * 823 | * @param collectionName the collection to insert the data into 824 | * @param documentId the ID of the document to remove 825 | * @param listener the listener to call on success/error 826 | */ 827 | public void remove(final String collectionName, final String documentId, final ResultListener listener) { 828 | final Map query = new HashMap(); 829 | query.put(MongoDb.Field.ID, documentId); 830 | 831 | call("/"+collectionName+"/remove", new Object[] { query }, listener); 832 | } 833 | 834 | /** 835 | * Sign in the user with the given username and password 836 | * 837 | * Please note that this requires the `accounts-password` package 838 | * 839 | * @param username the username to sign in with 840 | * @param password the password to sign in with 841 | * @param listener the listener to call on success/error 842 | */ 843 | public void loginWithUsername(final String username, final String password, final ResultListener listener) { 844 | login(username, null, password, listener); 845 | } 846 | 847 | /** 848 | * Sign in the user with the given email address and password 849 | * 850 | * Please note that this requires the `accounts-password` package 851 | * 852 | * @param email the email address to sign in with 853 | * @param password the password to sign in with 854 | * @param listener the listener to call on success/error 855 | */ 856 | public void loginWithEmail(final String email, final String password, final ResultListener listener) { 857 | login(null, email, password, listener); 858 | } 859 | 860 | /** 861 | * Sign in the user with the given username or email address and the specified password 862 | * 863 | * Please note that this requires the `accounts-password` package 864 | * 865 | * @param username the username to sign in with (either this or `email` is required) 866 | * @param email the email address to sign in with (either this or `username` is required) 867 | * @param password the password to sign in with 868 | * @param listener the listener to call on success/error 869 | */ 870 | private void login(final String username, final String email, final String password, final ResultListener listener) { 871 | final Map userData = new HashMap(); 872 | 873 | if (username != null) { 874 | userData.put("username", username); 875 | } 876 | else if (email != null) { 877 | userData.put("email", email); 878 | } 879 | else { 880 | throw new IllegalArgumentException("You must provide either a username or an email address"); 881 | } 882 | 883 | final Map authData = new HashMap(); 884 | authData.put("user", userData); 885 | authData.put("password", password); 886 | 887 | call("login", new Object[] { authData }, listener); 888 | } 889 | 890 | /** 891 | * Attempts to sign in with the given login token 892 | * 893 | * @param token the login token 894 | * @param listener the listener to call on success/error 895 | */ 896 | private void loginWithToken(final String token, final ResultListener listener) { 897 | final Map authData = new HashMap(); 898 | authData.put("resume", token); 899 | 900 | call("login", new Object[] { authData }, listener); 901 | } 902 | 903 | public void logout() { 904 | logout(null); 905 | } 906 | 907 | public void logout(final ResultListener listener) { 908 | call("logout", new Object[] { }, new ResultListener() { 909 | 910 | @Override 911 | public void onSuccess(final String result) { 912 | // remember that we're not logged in anymore 913 | mLoggedInUserId = null; 914 | 915 | // delete the last login token which is now invalid 916 | saveLoginToken(null); 917 | 918 | if (listener != null) { 919 | mCallbackProxy.forResultListener(listener).onSuccess(result); 920 | } 921 | } 922 | 923 | @Override 924 | public void onError(final String error, final String reason, final String details) { 925 | if (listener != null) { 926 | mCallbackProxy.forResultListener(listener).onError(error, reason, details); 927 | } 928 | } 929 | 930 | }); 931 | } 932 | 933 | /** 934 | * Registers a new user with the specified username, email address and password 935 | * 936 | * This method will automatically login as the new user on success 937 | * 938 | * Please note that this requires the `accounts-password` package 939 | * 940 | * @param username the username to register with (either this or `email` is required) 941 | * @param email the email address to register with (either this or `username` is required) 942 | * @param password the password to register with 943 | * @param listener the listener to call on success/error 944 | */ 945 | public void registerAndLogin(final String username, final String email, final String password, final ResultListener listener) { 946 | registerAndLogin(username, email, password, null, listener); 947 | } 948 | 949 | /** 950 | * Registers a new user with the specified username, email address and password 951 | * 952 | * This method will automatically login as the new user on success 953 | * 954 | * Please note that this requires the `accounts-password` package 955 | * 956 | * @param username the username to register with (either this or `email` is required) 957 | * @param email the email address to register with (either this or `username` is required) 958 | * @param password the password to register with 959 | * @param profile the user's profile data, typically including a `name` field 960 | * @param listener the listener to call on success/error 961 | */ 962 | public void registerAndLogin(final String username, final String email, final String password, final HashMap profile, final ResultListener listener) { 963 | if (username == null && email == null) { 964 | throw new IllegalArgumentException("You must provide either a username or an email address"); 965 | } 966 | 967 | final Map accountData = new HashMap(); 968 | 969 | if (username != null) { 970 | accountData.put("username", username); 971 | } 972 | 973 | if (email != null) { 974 | accountData.put("email", email); 975 | } 976 | 977 | accountData.put("password", password); 978 | 979 | if (profile != null) { 980 | accountData.put("profile", profile); 981 | } 982 | 983 | call("createUser", new Object[] { accountData }, listener); 984 | } 985 | 986 | /** 987 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 988 | * 989 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 990 | */ 991 | public void call(final String methodName) { 992 | call(methodName, null, null); 993 | } 994 | 995 | /** 996 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 997 | * 998 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 999 | * @param params the objects that should be passed to the method as parameters 1000 | */ 1001 | public void call(final String methodName, final Object[] params) { 1002 | call(methodName, params, null); 1003 | } 1004 | 1005 | /** 1006 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 1007 | * 1008 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 1009 | * @param listener the listener to trigger when the result has been received or `null` 1010 | */ 1011 | public void call(final String methodName, final ResultListener listener) { 1012 | call(methodName, null, listener); 1013 | } 1014 | 1015 | /** 1016 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 1017 | * 1018 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 1019 | * @param params the objects that should be passed to the method as parameters 1020 | * @param listener the listener to trigger when the result has been received or `null` 1021 | */ 1022 | public void call(final String methodName, final Object[] params, final ResultListener listener) { 1023 | callWithSeed(methodName, null, params, listener); 1024 | } 1025 | 1026 | /** 1027 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 1028 | * 1029 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 1030 | * @param randomSeed an arbitrary seed for pseudo-random generators or `null` 1031 | */ 1032 | public void callWithSeed(final String methodName, final String randomSeed) { 1033 | callWithSeed(methodName, randomSeed, null, null); 1034 | } 1035 | 1036 | /** 1037 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 1038 | * 1039 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 1040 | * @param randomSeed an arbitrary seed for pseudo-random generators or `null` 1041 | * @param params the objects that should be passed to the method as parameters 1042 | */ 1043 | public void callWithSeed(final String methodName, final String randomSeed, final Object[] params) { 1044 | callWithSeed(methodName, randomSeed, params, null); 1045 | } 1046 | 1047 | /** 1048 | * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) 1049 | * 1050 | * @param methodName the name of the method to call, e.g. `/someCollection.insert` 1051 | * @param randomSeed an arbitrary seed for pseudo-random generators or `null` 1052 | * @param params the objects that should be passed to the method as parameters 1053 | * @param listener the listener to trigger when the result has been received or `null` 1054 | */ 1055 | public void callWithSeed(final String methodName, final String randomSeed, final Object[] params, final ResultListener listener) { 1056 | // create a new unique ID for this request 1057 | final String callId = uniqueID(); 1058 | 1059 | // save a reference to the listener to be executed later 1060 | if (listener != null) { 1061 | mListeners.put(callId, listener); 1062 | } 1063 | 1064 | final Map data = new HashMap(); 1065 | 1066 | data.put(Protocol.Field.MESSAGE, Protocol.Message.METHOD); 1067 | data.put(Protocol.Field.METHOD, methodName); 1068 | data.put(Protocol.Field.ID, callId); 1069 | 1070 | if (params != null) { 1071 | data.put(Protocol.Field.PARAMS, params); 1072 | } 1073 | 1074 | if (randomSeed != null) { 1075 | data.put(Protocol.Field.RANDOM_SEED, randomSeed); 1076 | } 1077 | 1078 | send(data); 1079 | } 1080 | 1081 | /** 1082 | * Subscribes to a specific subscription from the server 1083 | * 1084 | * @param subscriptionName the name of the subscription 1085 | * @return the generated subscription ID (must be used when unsubscribing) 1086 | */ 1087 | public String subscribe(final String subscriptionName) { 1088 | return subscribe(subscriptionName, null); 1089 | } 1090 | 1091 | /** 1092 | * Subscribes to a specific subscription from the server 1093 | * 1094 | * @param subscriptionName the name of the subscription 1095 | * @param params the subscription parameters 1096 | * @return the generated subscription ID (must be used when unsubscribing) 1097 | */ 1098 | public String subscribe(final String subscriptionName, final Object[] params) { 1099 | return subscribe(subscriptionName, params, null); 1100 | } 1101 | 1102 | /** 1103 | * Subscribes to a specific subscription from the server 1104 | * 1105 | * @param subscriptionName the name of the subscription 1106 | * @param params the subscription parameters 1107 | * @param listener the listener to call on success/error 1108 | * @return the generated subscription ID (must be used when unsubscribing) 1109 | */ 1110 | public String subscribe(final String subscriptionName, final Object[] params, final SubscribeListener listener) { 1111 | // create a new unique ID for this request 1112 | final String subscriptionId = uniqueID(); 1113 | 1114 | // save a reference to the listener to be executed later 1115 | if (listener != null) { 1116 | mListeners.put(subscriptionId, listener); 1117 | } 1118 | 1119 | final Map data = new HashMap(); 1120 | 1121 | data.put(Protocol.Field.MESSAGE, Protocol.Message.SUBSCRIBE); 1122 | data.put(Protocol.Field.NAME, subscriptionName); 1123 | data.put(Protocol.Field.ID, subscriptionId); 1124 | 1125 | if (params != null) { 1126 | data.put(Protocol.Field.PARAMS, params); 1127 | } 1128 | 1129 | send(data); 1130 | 1131 | // return the generated subscription ID 1132 | return subscriptionId; 1133 | } 1134 | 1135 | /** 1136 | * Unsubscribes from the subscription with the specified name 1137 | * 1138 | * @param subscriptionId the ID of the subscription 1139 | */ 1140 | public void unsubscribe(final String subscriptionId) { 1141 | unsubscribe(subscriptionId, null); 1142 | } 1143 | 1144 | /** 1145 | * Unsubscribes from the subscription with the specified name 1146 | * 1147 | * @param subscriptionId the ID of the subscription 1148 | * @param listener the listener to call on success/error 1149 | */ 1150 | public void unsubscribe(final String subscriptionId, final UnsubscribeListener listener) { 1151 | // save a reference to the listener to be executed later 1152 | if (listener != null) { 1153 | mListeners.put(subscriptionId, listener); 1154 | } 1155 | 1156 | final Map data = new HashMap(); 1157 | data.put(Protocol.Field.MESSAGE, Protocol.Message.UNSUBSCRIBE); 1158 | data.put(Protocol.Field.ID, subscriptionId); 1159 | 1160 | send(data); 1161 | } 1162 | 1163 | /** 1164 | * Creates an empty map for use as default parameter 1165 | * 1166 | * @return an empty map 1167 | */ 1168 | private static Map emptyMap() { 1169 | return new HashMap(); 1170 | } 1171 | 1172 | /** 1173 | * Saves the given login token to the preferences 1174 | * 1175 | * @param token the login token to save 1176 | */ 1177 | private void saveLoginToken(final String token) { 1178 | final SharedPreferences.Editor editor = getSharedPreferences().edit(); 1179 | editor.putString(Preferences.Keys.LOGIN_TOKEN, token); 1180 | editor.apply(); 1181 | } 1182 | 1183 | /** 1184 | * Retrieves the last login token from the preferences 1185 | * 1186 | * @return the last login token or `null` 1187 | */ 1188 | private String getLoginToken() { 1189 | return getSharedPreferences().getString(Preferences.Keys.LOGIN_TOKEN, null); 1190 | } 1191 | 1192 | /** 1193 | * Returns a reference to the preferences for internal use 1194 | * 1195 | * @return the `SharedPreferences` instance 1196 | */ 1197 | private SharedPreferences getSharedPreferences() { 1198 | return mContext.getSharedPreferences(Preferences.FILE_NAME, Context.MODE_PRIVATE); 1199 | } 1200 | 1201 | private void initSession() { 1202 | // get the last login token 1203 | final String loginToken = getLoginToken(); 1204 | 1205 | // if we found a login token that might work 1206 | if (loginToken != null) { 1207 | // try to sign in with that token 1208 | loginWithToken(loginToken, new ResultListener() { 1209 | 1210 | @Override 1211 | public void onSuccess(final String result) { 1212 | announceSessionReady(true); 1213 | } 1214 | 1215 | @Override 1216 | public void onError(final String error, final String reason, final String details) { 1217 | // clear the user ID since automatic sign-in has failed 1218 | mLoggedInUserId = null; 1219 | 1220 | // discard the token which turned out to be invalid 1221 | saveLoginToken(null); 1222 | 1223 | announceSessionReady(false); 1224 | } 1225 | 1226 | }); 1227 | } 1228 | // if we didn't find any login token 1229 | else { 1230 | announceSessionReady(false); 1231 | } 1232 | } 1233 | 1234 | /** 1235 | * Announces that the new session is now ready to use 1236 | * 1237 | * @param signedInAutomatically whether we have already signed in automatically (`true`) or not (`false)` 1238 | */ 1239 | private void announceSessionReady(final boolean signedInAutomatically) { 1240 | // run the callback that waits for the connection to open 1241 | mCallbackProxy.onConnect(signedInAutomatically); 1242 | 1243 | // try to dispatch queued messages now 1244 | String queuedMessage = null; 1245 | while ((queuedMessage = mQueuedMessages.poll()) != null) { 1246 | send(queuedMessage); 1247 | } 1248 | } 1249 | 1250 | /** 1251 | * Returns the data store that was set in the constructor and that contains all data received from the server 1252 | * 1253 | * @return the data store or `null` 1254 | */ 1255 | public DataStore getDataStore() { 1256 | return mDataStore; 1257 | } 1258 | 1259 | /** 1260 | * Returns the database that was set in the constructor and that contains all data received from the server 1261 | * 1262 | * @return the database or `null` 1263 | */ 1264 | public Database getDatabase() { 1265 | if (mDataStore instanceof Database) { 1266 | return (Database) mDataStore; 1267 | } 1268 | else { 1269 | return null; 1270 | } 1271 | } 1272 | 1273 | } 1274 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/MeteorCallback.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** Callback for asynchronous events caused by a WebSocket connection or received from a DDP server */ 20 | public interface MeteorCallback extends DdpCallback { 21 | 22 | void onConnect(boolean signedInAutomatically); 23 | 24 | void onDisconnect(); 25 | 26 | void onException(Exception e); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/MeteorSingleton.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.db.DataStore; 20 | import android.content.Context; 21 | 22 | /** Provides a single access point to the `Meteor` class that can be used across `Activity` instances */ 23 | public class MeteorSingleton extends Meteor { 24 | 25 | private static MeteorSingleton mInstance; 26 | 27 | public synchronized static MeteorSingleton createInstance(final Context context, final String serverUri) { 28 | if (mInstance != null) { 29 | throw new IllegalStateException("An instance has already been created"); 30 | } 31 | 32 | mInstance = new MeteorSingleton(context, serverUri); 33 | 34 | return mInstance; 35 | } 36 | 37 | public synchronized static MeteorSingleton createInstance(final Context context, final String serverUri, final DataStore dataStore) { 38 | if (mInstance != null) { 39 | throw new IllegalStateException("An instance has already been created"); 40 | } 41 | 42 | mInstance = new MeteorSingleton(context, serverUri, dataStore); 43 | 44 | return mInstance; 45 | } 46 | 47 | public synchronized static MeteorSingleton createInstance(final Context context, final String serverUri, final String protocolVersion) { 48 | if (mInstance != null) { 49 | throw new IllegalStateException("An instance has already been created"); 50 | } 51 | 52 | mInstance = new MeteorSingleton(context, serverUri, protocolVersion); 53 | 54 | return mInstance; 55 | } 56 | 57 | public synchronized static MeteorSingleton createInstance(final Context context, final String serverUri, final String protocolVersion, final DataStore dataStore) { 58 | if (mInstance != null) { 59 | throw new IllegalStateException("An instance has already been created"); 60 | } 61 | 62 | mInstance = new MeteorSingleton(context, serverUri, protocolVersion, dataStore); 63 | 64 | return mInstance; 65 | } 66 | 67 | public synchronized static MeteorSingleton getInstance() { 68 | if (mInstance == null) { 69 | throw new IllegalStateException("Please call `createInstance(...)` first"); 70 | } 71 | 72 | return mInstance; 73 | } 74 | 75 | public synchronized static boolean hasInstance() { 76 | return mInstance != null; 77 | } 78 | 79 | public synchronized static void destroyInstance() { 80 | if (mInstance == null) { 81 | throw new IllegalStateException("Please call `createInstance(...)` first"); 82 | } 83 | 84 | mInstance.disconnect(); 85 | mInstance.removeCallbacks(); 86 | mInstance = null; 87 | } 88 | 89 | private MeteorSingleton(final Context context, final String serverUri) { 90 | super(context, serverUri); 91 | } 92 | 93 | private MeteorSingleton(final Context context, final String serverUri, final DataStore dataStore) { 94 | super(context, serverUri, dataStore); 95 | } 96 | 97 | private MeteorSingleton(final Context context, final String serverUri, final String protocolVersion) { 98 | super(context, serverUri, protocolVersion); 99 | } 100 | 101 | private MeteorSingleton(final Context context, final String serverUri, final String protocolVersion, final DataStore dataStore) { 102 | super(context, serverUri, protocolVersion, dataStore); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/MongoDb.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** Constants used in MongoDB (the database behind Meteor) */ 20 | public class MongoDb { 21 | 22 | /** Constants defining field names in documents */ 23 | public static class Field { 24 | 25 | public static final String ID = "_id"; 26 | 27 | @Deprecated 28 | public static final String VALUE = "_value"; 29 | 30 | @Deprecated 31 | public static final String PRIORITY = "_priority"; 32 | 33 | } 34 | 35 | /** Constants defining modifiers that can be used in requests */ 36 | public static class Modifier { 37 | 38 | @Deprecated 39 | public static final String SET = "$set"; 40 | 41 | } 42 | 43 | /** Constants definining options that may be sent along with requests */ 44 | public static class Option { 45 | 46 | @Deprecated 47 | public static final String UPSERT = "upsert"; 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/Preferences.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** Constants and utilities used for access to the preferences/settings */ 20 | public class Preferences { 21 | 22 | /** Name of the file where the preferences for this library will be stored */ 23 | public static final String FILE_NAME = "android_ddp"; 24 | 25 | private Preferences() { } 26 | 27 | public static class Keys { 28 | 29 | private Keys() { } 30 | 31 | /** Name of the preference where the current login token will be stored */ 32 | public static final String LOGIN_TOKEN = "login_token"; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/Protocol.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import org.codehaus.jackson.JsonNode; 20 | 21 | /** Constants used in the Distributed Data Protocol (DDP) */ 22 | public class Protocol { 23 | 24 | /** Constants defining message types in message objects' values */ 25 | public static class Message { 26 | 27 | public static final String ADDED = "added"; 28 | public static final String ADDED_BEFORE = "addedBefore"; 29 | public static final String CHANGED = "changed"; 30 | public static final String CONNECT = "connect"; 31 | public static final String CONNECTED = "connected"; 32 | public static final String FAILED = "failed"; 33 | public static final String METHOD = "method"; 34 | public static final String NOSUB = "nosub"; 35 | public static final String PING = "ping"; 36 | public static final String PONG = "pong"; 37 | public static final String READY = "ready"; 38 | public static final String REMOVED = "removed"; 39 | public static final String RESULT = "result"; 40 | public static final String SUBSCRIBE = "sub"; 41 | public static final String UNSUBSCRIBE = "unsub"; 42 | 43 | } 44 | 45 | /** Constants defining field names in message objects' keys */ 46 | public static class Field { 47 | 48 | public static final String CLEARED = "cleared"; 49 | public static final String COLLECTION = "collection"; 50 | public static final String DETAILS = "details"; 51 | public static final String ERROR = "error"; 52 | public static final String FIELDS = "fields"; 53 | public static final String ID = "id"; 54 | public static final String MESSAGE = "msg"; 55 | public static final String METHOD = "method"; 56 | public static final String NAME = "name"; 57 | public static final String PARAMS = "params"; 58 | public static final String RANDOM_SEED = "randomSeed"; 59 | public static final String REASON = "reason"; 60 | public static final String RESULT = "result"; 61 | public static final String SESSION = "session"; 62 | public static final String SUBS = "subs"; 63 | public static final String SUPPORT = "support"; 64 | public static final String VERSION = "version"; 65 | public static final String TOKEN = "token"; 66 | 67 | } 68 | 69 | /** Wrapper and utility class to store errors from the DDP protocol */ 70 | public static class Error { 71 | 72 | private final String mError; 73 | private final String mReason; 74 | private final String mDetails; 75 | 76 | private Error(final String error, final String reason, final String details) { 77 | mError = error; 78 | mReason = reason; 79 | mDetails = details; 80 | } 81 | 82 | public static Error fromJson(final JsonNode json) { 83 | final String error; 84 | if (json.has(Protocol.Field.ERROR)) { 85 | final JsonNode errorJson = json.get(Protocol.Field.ERROR); 86 | if (errorJson.isTextual()) { 87 | error = errorJson.getTextValue(); 88 | } 89 | else if (errorJson.isNumber()) { 90 | error = errorJson.getNumberValue().toString(); 91 | } 92 | else { 93 | throw new IllegalArgumentException("Unexpected data type of error.error"); 94 | } 95 | } 96 | else { 97 | error = null; 98 | } 99 | 100 | final String reason; 101 | if (json.has(Protocol.Field.REASON)) { 102 | reason = json.get(Protocol.Field.REASON).getTextValue(); 103 | } 104 | else { 105 | reason = null; 106 | } 107 | 108 | final String details; 109 | if (json.has(Protocol.Field.DETAILS)) { 110 | details = json.get(Protocol.Field.DETAILS).getTextValue(); 111 | } 112 | else { 113 | details = null; 114 | } 115 | 116 | return new Error(error, reason, details); 117 | } 118 | 119 | public String getError() { 120 | return mError; 121 | } 122 | 123 | public String getReason() { 124 | return mReason; 125 | } 126 | 127 | public String getDetails() { 128 | return mDetails; 129 | } 130 | 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/ResultListener.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | public interface ResultListener extends Listener { 20 | 21 | void onSuccess(String result); 22 | 23 | void onError(String error, String reason, String details); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/SubscribeListener.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | public interface SubscribeListener extends Listener { 20 | 21 | void onSuccess(); 22 | 23 | void onError(String error, String reason, String details); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/UnsubscribeListener.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | public interface UnsubscribeListener extends Listener { 20 | 21 | void onSuccess(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/Collection.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** A collection has a name and contains any number of documents, identified by their IDs */ 20 | public interface Collection extends Query { 21 | 22 | /** 23 | * Returns the name of the collection 24 | * 25 | * @return the name 26 | */ 27 | String getName(); 28 | 29 | /** 30 | * Returns the document with the specified ID from the collection 31 | * 32 | * @param id the ID of the document to return 33 | * @return the document object or `null` 34 | */ 35 | Document getDocument(String id); 36 | 37 | /** 38 | * Lists all documents from the collection by returning a set of their IDs 39 | * 40 | * @return an array containing the IDs of all documents 41 | */ 42 | String[] getDocumentIds(); 43 | 44 | /** 45 | * Returns the number of documents in the collection 46 | * 47 | * @return the count 48 | */ 49 | int count(); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/DataStore.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.Fields; 20 | 21 | /** Storage for data that exposes write access */ 22 | public interface DataStore { 23 | 24 | /** 25 | * Receives data whenever a new document is added to a collection 26 | * 27 | * @param collectionName the name of the collection that the document is added to 28 | * @param documentID the ID of the document that is being added 29 | * @param newValues the new fields of the document 30 | */ 31 | void onDataAdded(String collectionName, String documentID, Fields newValues); 32 | 33 | /** 34 | * Receives data whenever an existing document is changed in a collection 35 | * 36 | * @param collectionName the name of the collection that the document is changed in 37 | * @param documentID the ID of the document that is being changed 38 | * @param updatedValues the modified fields of the document 39 | * @param removedValues the deleted fields of the document 40 | */ 41 | void onDataChanged(String collectionName, String documentID, Fields updatedValues, String[] removedValues); 42 | 43 | /** 44 | * Receives data whenever an existing document is removed from a collection 45 | * 46 | * @param collectionName the name of the collection that the document is removed from 47 | * @param documentID the ID of the document that is being removed 48 | */ 49 | void onDataRemoved(String collectionName, String documentID); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/Database.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** Storage for data that exposes both read and write access */ 20 | public interface Database extends DataStore { 21 | 22 | /** 23 | * Returns the collection with the specified name from the database 24 | * 25 | * The collection may or may not actually exist 26 | * 27 | * If the collection does not exist, an empty collection is implicitly created 28 | * 29 | * @param name the name of the collection to return 30 | * @return a collection object (never `null`) 31 | */ 32 | Collection getCollection(String name); 33 | 34 | /** 35 | * Lists all collections from the database by returning a set of their names 36 | * 37 | * @return an array containing the names of all collections 38 | */ 39 | String[] getCollectionNames(); 40 | 41 | /** 42 | * Returns the number of collections in the database 43 | * 44 | * @return the count 45 | */ 46 | int count(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/Document.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** A document has an ID and contains any number of fields, identified by their names */ 20 | public interface Document { 21 | 22 | /** 23 | * Returns the ID of the document 24 | * 25 | * @return the ID 26 | */ 27 | String getId(); 28 | 29 | /** 30 | * Returns the field with the specified name from the document 31 | * 32 | * @param name the name of the field to return 33 | * @return the field data of any type (e.g. `String`, `Integer`, `Long`, `Boolean`) or `null` 34 | */ 35 | Object getField(String name); 36 | 37 | /** 38 | * Lists all fields from the document by returning their names 39 | * 40 | * @return an array containing the names of all fields 41 | */ 42 | String[] getFieldNames(); 43 | 44 | /** 45 | * Returns the number of fields in the document 46 | * 47 | * @return the count 48 | */ 49 | int count(); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/Query.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | /** A query can be executed to find a specified number of documents, after any number of requirements has been supplied before */ 20 | public interface Query { 21 | 22 | /** 23 | * Adds a filter to the query requiring the given field to have exactly the specified value 24 | * 25 | * @param fieldName the name of the field to check 26 | * @param fieldValue the value to check against 27 | * @return this instance for chaining 28 | */ 29 | Query whereEqual(String fieldName, Object fieldValue); 30 | 31 | /** 32 | * Adds a filter to the query requiring the given field to have a value other than the specified one 33 | * 34 | * @param fieldName the name of the field to check 35 | * @param fieldValue the value to check against 36 | * @return this instance for chaining 37 | */ 38 | Query whereNotEqual(String fieldName, Object fieldValue); 39 | 40 | /** 41 | * Adds a filter to the query requiring the given field to have a value less than the specified value 42 | * 43 | * @param fieldName the name of the field to check 44 | * @param fieldValue the value to check against 45 | * @return this instance for chaining 46 | */ 47 | Query whereLessThan(String fieldName, double fieldValue); 48 | 49 | /** 50 | * Adds a filter to the query requiring the given field to have a value less than or equal to the specified value 51 | * 52 | * @param fieldName the name of the field to check 53 | * @param fieldValue the value to check against 54 | * @return this instance for chaining 55 | */ 56 | Query whereLessThanOrEqual(String fieldName, double fieldValue); 57 | 58 | /** 59 | * Adds a filter to the query requiring the given field to have a value greater than the specified value 60 | * 61 | * @param fieldName the name of the field to check 62 | * @param fieldValue the value to check against 63 | * @return this instance for chaining 64 | */ 65 | Query whereGreaterThan(String fieldName, double fieldValue); 66 | 67 | /** 68 | * Adds a filter to the query requiring the given field to have a value greater than or equal to the specified value 69 | * 70 | * @param fieldName the name of the field to check 71 | * @param fieldValue the value to check against 72 | * @return this instance for chaining 73 | */ 74 | Query whereGreaterThanOrEqual(String fieldName, double fieldValue); 75 | 76 | /** 77 | * Adds a filter to the query requiring the given field to have `null` as its value (or no value) 78 | * 79 | * @param fieldName the name of the field to check 80 | * @return this instance for chaining 81 | */ 82 | Query whereNull(String fieldName); 83 | 84 | /** 85 | * Adds a filter to the query requiring the given field to have a value other than `null` 86 | * 87 | * @param fieldName the name of the field to check 88 | * @return this instance for chaining 89 | */ 90 | Query whereNotNull(String fieldName); 91 | 92 | /** 93 | * Adds a filter to the query requiring the given field to have one of the specified values 94 | * 95 | * @param fieldName the name of the field to check 96 | * @param fieldValues the values to check against 97 | * @return this instance for chaining 98 | */ 99 | Query whereIn(String fieldName, Object[] fieldValues); 100 | 101 | /** 102 | * Adds a filter to the query requiring the given field to have a value different from all the specified ones 103 | * 104 | * @param fieldName the name of the field to check 105 | * @param fieldValues the values to check against 106 | * @return this instance for chaining 107 | */ 108 | Query whereNotIn(String fieldName, Object[] fieldValues); 109 | 110 | /** 111 | * Executes the query and returns all matching entries 112 | * 113 | * @return an array (never `null`) containing zero or more matches 114 | */ 115 | Document[] find(); 116 | 117 | /** 118 | * Executes the query and returns at most the specified number of matching entries 119 | * 120 | * @param limit the maximum number of entries to return 121 | * @return an array (never `null`) containing zero or more matches 122 | */ 123 | Document[] find(int limit); 124 | 125 | /** 126 | * Executes the query and returns the matching entries in the specified range 127 | * 128 | * @param limit the maximum number of entries to return 129 | * @param offset the number of entries to skip at the beginning 130 | * @return an array (never `null`) containing zero or more matches 131 | */ 132 | Document[] find(int limit, int offset); 133 | 134 | /** 135 | * Executes the query and returns the first matching entry 136 | * 137 | * @return the first entry that matches the query or `null` 138 | */ 139 | Document findOne(); 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/memory/InMemoryCollection.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db.memory; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | import im.delight.android.ddp.db.Collection; 22 | import im.delight.android.ddp.db.Document; 23 | import im.delight.android.ddp.db.Query; 24 | 25 | /** Collection that is stored in memory */ 26 | public final class InMemoryCollection implements Collection { 27 | 28 | /** The name of the collection */ 29 | private final String mName; 30 | /** The map of documents backing the collection */ 31 | private final DocumentsMap mDocuments; 32 | 33 | /** 34 | * Creates a new collection that is stored in memory 35 | * 36 | * @param name the name of the collection to create 37 | */ 38 | protected InMemoryCollection(final String name) { 39 | mName = name; 40 | mDocuments = new DocumentsMap(); 41 | } 42 | 43 | @Override 44 | public String getName() { 45 | return mName; 46 | } 47 | 48 | @Override 49 | public Document getDocument(final String id) { 50 | return mDocuments.get(id); 51 | } 52 | 53 | @Override 54 | public String[] getDocumentIds() { 55 | return mDocuments.keySet().toArray(new String[mDocuments.size()]); 56 | } 57 | 58 | @Override 59 | public int count() { 60 | return mDocuments.size(); 61 | } 62 | 63 | @Override 64 | public Query whereEqual(final String fieldName, final Object fieldValue) { 65 | return new InMemoryQuery(mDocuments).whereEqual(fieldName, fieldValue); 66 | } 67 | 68 | @Override 69 | public Query whereNotEqual(final String fieldName, final Object fieldValue) { 70 | return new InMemoryQuery(mDocuments).whereNotEqual(fieldName, fieldValue); 71 | } 72 | 73 | @Override 74 | public Query whereLessThan(final String fieldName, final double fieldValue) { 75 | return new InMemoryQuery(mDocuments).whereLessThan(fieldName, fieldValue); 76 | } 77 | 78 | @Override 79 | public Query whereLessThanOrEqual(final String fieldName, final double fieldValue) { 80 | return new InMemoryQuery(mDocuments).whereLessThanOrEqual(fieldName, fieldValue); 81 | } 82 | 83 | @Override 84 | public Query whereGreaterThan(final String fieldName, final double fieldValue) { 85 | return new InMemoryQuery(mDocuments).whereGreaterThan(fieldName, fieldValue); 86 | } 87 | 88 | @Override 89 | public Query whereGreaterThanOrEqual(final String fieldName, final double fieldValue) { 90 | return new InMemoryQuery(mDocuments).whereGreaterThanOrEqual(fieldName, fieldValue); 91 | } 92 | 93 | @Override 94 | public Query whereNull(final String fieldName) { 95 | return new InMemoryQuery(mDocuments).whereNull(fieldName); 96 | } 97 | 98 | @Override 99 | public Query whereNotNull(final String fieldName) { 100 | return new InMemoryQuery(mDocuments).whereNotNull(fieldName); 101 | } 102 | 103 | @Override 104 | public Query whereIn(String fieldName, Object[] fieldValues) { 105 | return new InMemoryQuery(mDocuments).whereIn(fieldName, fieldValues); 106 | } 107 | 108 | @Override 109 | public Query whereNotIn(String fieldName, Object[] fieldValues) { 110 | return new InMemoryQuery(mDocuments).whereNotIn(fieldName, fieldValues); 111 | } 112 | 113 | @Override 114 | public Document[] find() { 115 | return new InMemoryQuery(mDocuments).find(); 116 | } 117 | 118 | @Override 119 | public Document[] find(int limit) { 120 | return new InMemoryQuery(mDocuments).find(limit); 121 | } 122 | 123 | @Override 124 | public Document[] find(int limit, int offset) { 125 | return new InMemoryQuery(mDocuments).find(limit, offset); 126 | } 127 | 128 | @Override 129 | public Document findOne() { 130 | return new InMemoryQuery(mDocuments).findOne(); 131 | } 132 | 133 | /** 134 | * Returns the raw map of documents backing this collection 135 | * 136 | * @return the raw map of documents 137 | */ 138 | protected DocumentsMap getDocumentsMap() { 139 | return mDocuments; 140 | } 141 | 142 | @Override 143 | public String toString() { 144 | return mDocuments.toString(); 145 | } 146 | 147 | /** Data type for the map backing the collection */ 148 | protected static class DocumentsMap extends LinkedHashMap { 149 | 150 | public DocumentsMap() { 151 | super(); 152 | } 153 | 154 | public DocumentsMap(final Map map) { 155 | super(map); 156 | } 157 | 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/memory/InMemoryDatabase.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db.memory; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.Meteor; 20 | import im.delight.android.ddp.db.Collection; 21 | import im.delight.android.ddp.db.Database; 22 | import im.delight.android.ddp.Fields; 23 | import java.util.HashMap; 24 | 25 | /** Database that is stored in memory */ 26 | public final class InMemoryDatabase implements Database { 27 | 28 | private static final String TAG = "InMemoryDatabase"; 29 | /** The collections contained in the database */ 30 | private final CollectionsMap mCollections; 31 | 32 | /** Creates a new database that is stored in memory */ 33 | public InMemoryDatabase() { 34 | mCollections = new CollectionsMap(); 35 | } 36 | 37 | @Override 38 | public Collection getCollection(final String name) { 39 | if (mCollections.containsKey(name)) { 40 | return mCollections.get(name); 41 | } 42 | else { 43 | return new InMemoryCollection(name); 44 | } 45 | } 46 | 47 | @Override 48 | public String[] getCollectionNames() { 49 | return mCollections.keySet().toArray(new String[mCollections.size()]); 50 | } 51 | 52 | @Override 53 | public int count() { 54 | return mCollections.size(); 55 | } 56 | 57 | @Override 58 | public void onDataAdded(final String collectionName, final String documentId, final Fields newValues) { 59 | if (!mCollections.containsKey(collectionName)) { 60 | mCollections.put(collectionName, new InMemoryCollection(collectionName)); 61 | } 62 | 63 | final InMemoryCollection.DocumentsMap collectionData = mCollections.get(collectionName).getDocumentsMap(); 64 | 65 | if (newValues != null) { 66 | collectionData.put(documentId, new InMemoryDocument(documentId, newValues)); 67 | } 68 | } 69 | 70 | @Override 71 | public void onDataChanged(final String collectionName, final String documentId, final Fields updatedValues, final String[] removedValues) { 72 | if (mCollections.containsKey(collectionName)) { 73 | final InMemoryCollection.DocumentsMap collectionData = mCollections.get(collectionName).getDocumentsMap(); 74 | final Fields documentData = collectionData.get(documentId).getFields(); 75 | 76 | if (updatedValues != null) { 77 | documentData.putAll(updatedValues); 78 | } 79 | 80 | if (removedValues != null) { 81 | for (String removedKey : removedValues) { 82 | documentData.remove(removedKey); 83 | } 84 | } 85 | } 86 | else { 87 | Meteor.log(TAG); 88 | Meteor.log(" Cannot find document `"+documentId+"` to update in collection `"+collectionName+"`"); 89 | 90 | onDataAdded(collectionName, documentId, updatedValues); 91 | } 92 | } 93 | 94 | @Override 95 | public void onDataRemoved(final String collectionName, final String documentId) { 96 | if (mCollections.containsKey(collectionName)) { 97 | mCollections.get(collectionName).getDocumentsMap().remove(documentId); 98 | } 99 | else { 100 | Meteor.log(TAG); 101 | Meteor.log(" Cannot find document `"+documentId+"` to delete in collection `"+collectionName+"`"); 102 | } 103 | } 104 | 105 | @Override 106 | public String toString() { 107 | return mCollections.toString(); 108 | } 109 | 110 | /** Data type for the map backing the database */ 111 | private static class CollectionsMap extends HashMap { } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/memory/InMemoryDocument.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db.memory; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.Fields; 20 | import im.delight.android.ddp.db.Document; 21 | 22 | /** Document that is stored in memory */ 23 | public final class InMemoryDocument implements Document { 24 | 25 | /** The ID of the document */ 26 | private final String mId; 27 | /** The fields of the document */ 28 | private final Fields mFields; 29 | 30 | /** 31 | * Creates a new document that is stored in memory 32 | * 33 | * @param id the ID of the document to create 34 | */ 35 | protected InMemoryDocument(final String id) { 36 | this(id, new Fields()); 37 | } 38 | 39 | /** 40 | * Creates a new document that is stored in memory 41 | * 42 | * @param id the ID of the document to create 43 | * @param fields the initial fields for the document to create 44 | */ 45 | protected InMemoryDocument(final String id, final Fields fields) { 46 | mId = id; 47 | mFields = fields; 48 | } 49 | 50 | @Override 51 | public String getId() { 52 | return mId; 53 | } 54 | 55 | @Override 56 | public Object getField(final String name) { 57 | return mFields.get(name); 58 | } 59 | 60 | @Override 61 | public String[] getFieldNames() { 62 | return mFields.keySet().toArray(new String[mFields.size()]); 63 | } 64 | 65 | @Override 66 | public int count() { 67 | return mFields.size(); 68 | } 69 | 70 | /** 71 | * Returns the raw map of fields backing this document 72 | * 73 | * @return the raw map of fields 74 | */ 75 | protected Fields getFields() { 76 | return mFields; 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return mFields.toString(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Source/library/src/main/java/im/delight/android/ddp/db/memory/InMemoryQuery.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.db.memory; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.db.Document; 20 | import im.delight.android.ddp.db.Query; 21 | import java.util.Iterator; 22 | import java.util.NoSuchElementException; 23 | 24 | /** Query that operates on a collection stored in memory */ 25 | public final class InMemoryQuery implements Query { 26 | 27 | private static final double PICO_DOUBLE = 0.000000000001; 28 | private final InMemoryCollection.DocumentsMap mDocuments; 29 | 30 | /** 31 | * Creates a new query that operates on a collection stored in memory 32 | * 33 | * @param documents the map of documents that this new query should operate on 34 | */ 35 | protected InMemoryQuery(final InMemoryCollection.DocumentsMap documents) { 36 | // create a shallow copy of the map containing the documents 37 | mDocuments = new InMemoryCollection.DocumentsMap(documents); 38 | } 39 | 40 | @Override 41 | public Query whereEqual(final String fieldName, final Object fieldValue) { 42 | if (fieldValue == null) { 43 | return whereNull(fieldName); 44 | } 45 | 46 | final Iterator iterator = mDocuments.values().iterator(); 47 | 48 | InMemoryDocument entry; 49 | while (iterator.hasNext()) { 50 | entry = iterator.next(); 51 | 52 | if (entry.getField(fieldName) == null || !entry.getField(fieldName).equals(fieldValue)) { 53 | iterator.remove(); 54 | } 55 | } 56 | 57 | return this; 58 | } 59 | 60 | @Override 61 | public Query whereNotEqual(final String fieldName, final Object fieldValue) { 62 | if (fieldValue == null) { 63 | return whereNotNull(fieldName); 64 | } 65 | 66 | final Iterator iterator = mDocuments.values().iterator(); 67 | 68 | InMemoryDocument entry; 69 | while (iterator.hasNext()) { 70 | entry = iterator.next(); 71 | 72 | if (entry.getField(fieldName) != null && entry.getField(fieldName).equals(fieldValue)) { 73 | iterator.remove(); 74 | } 75 | } 76 | 77 | return this; 78 | } 79 | 80 | @Override 81 | public Query whereLessThan(final String fieldName, final double fieldValue) { 82 | final Iterator iterator = mDocuments.values().iterator(); 83 | 84 | InMemoryDocument entry; 85 | while (iterator.hasNext()) { 86 | entry = iterator.next(); 87 | 88 | if (coerceNumber(entry.getField(fieldName)) >= fieldValue) { 89 | iterator.remove(); 90 | } 91 | } 92 | 93 | return this; 94 | } 95 | 96 | @Override 97 | public Query whereLessThanOrEqual(final String fieldName, final double fieldValue) { 98 | return whereLessThan(fieldName, fieldValue + PICO_DOUBLE); 99 | } 100 | 101 | @Override 102 | public Query whereGreaterThan(final String fieldName, final double fieldValue) { 103 | final Iterator iterator = mDocuments.values().iterator(); 104 | 105 | InMemoryDocument entry; 106 | while (iterator.hasNext()) { 107 | entry = iterator.next(); 108 | 109 | if (coerceNumber(entry.getField(fieldName)) <= fieldValue) { 110 | iterator.remove(); 111 | } 112 | } 113 | 114 | return this; 115 | } 116 | 117 | @Override 118 | public Query whereGreaterThanOrEqual(final String fieldName, final double fieldValue) { 119 | return whereGreaterThan(fieldName, fieldValue - PICO_DOUBLE); 120 | } 121 | 122 | @Override 123 | public Query whereNull(final String fieldName) { 124 | final Iterator iterator = mDocuments.values().iterator(); 125 | 126 | InMemoryDocument entry; 127 | while (iterator.hasNext()) { 128 | entry = iterator.next(); 129 | 130 | if (entry.getField(fieldName) != null) { 131 | iterator.remove(); 132 | } 133 | } 134 | 135 | return this; 136 | } 137 | 138 | @Override 139 | public Query whereNotNull(final String fieldName) { 140 | final Iterator iterator = mDocuments.values().iterator(); 141 | 142 | InMemoryDocument entry; 143 | while (iterator.hasNext()) { 144 | entry = iterator.next(); 145 | 146 | if (entry.getField(fieldName) == null) { 147 | iterator.remove(); 148 | } 149 | } 150 | 151 | return this; 152 | } 153 | 154 | @Override 155 | public Query whereIn(String fieldName, Object[] fieldValues) { 156 | if (fieldValues == null || fieldValues.length == 0) { 157 | return whereNull(fieldName); 158 | } 159 | 160 | final Iterator iterator = mDocuments.values().iterator(); 161 | 162 | InMemoryDocument entry; 163 | while (iterator.hasNext()) { 164 | entry = iterator.next(); 165 | 166 | boolean found = false; 167 | 168 | if (entry.getField(fieldName) != null) { 169 | for (Object fieldValue : fieldValues) { 170 | if (entry.getField(fieldName).equals(fieldValue)) { 171 | found = true; 172 | 173 | break; 174 | } 175 | } 176 | } 177 | 178 | if (!found) { 179 | iterator.remove(); 180 | } 181 | } 182 | 183 | return this; 184 | } 185 | 186 | @Override 187 | public Query whereNotIn(String fieldName, Object[] fieldValues) { 188 | if (fieldValues == null || fieldValues.length == 0) { 189 | return whereNotNull(fieldName); 190 | } 191 | 192 | final Iterator iterator = mDocuments.values().iterator(); 193 | 194 | InMemoryDocument entry; 195 | while (iterator.hasNext()) { 196 | entry = iterator.next(); 197 | 198 | boolean found = false; 199 | 200 | if (entry.getField(fieldName) != null) { 201 | for (Object fieldValue : fieldValues) { 202 | if (entry.getField(fieldName).equals(fieldValue)) { 203 | found = true; 204 | 205 | break; 206 | } 207 | } 208 | } 209 | 210 | if (found) { 211 | iterator.remove(); 212 | } 213 | } 214 | 215 | return this; 216 | } 217 | 218 | @Override 219 | public Document[] find() { 220 | return mDocuments.values().toArray(new Document[mDocuments.size()]); 221 | } 222 | 223 | @Override 224 | public Document[] find(final int limit) { 225 | if (limit <= 0) { 226 | throw new IllegalArgumentException("The limit (`"+limit+"`) must be greater than `0`"); 227 | } 228 | 229 | return find(limit, 0); 230 | } 231 | 232 | @Override 233 | public Document[] find(final int limit, final int offset) { 234 | if (limit <= 0) { 235 | throw new IllegalArgumentException("The limit is `"+limit+"` but it must be greater than `0`"); 236 | } 237 | 238 | if (offset < 0) { 239 | throw new IllegalArgumentException("The offset is `"+offset+"` but it must be greater than or equal to `0`"); 240 | } 241 | 242 | final int numResults = Math.min(mDocuments.size() - offset, limit); 243 | 244 | if (numResults <= 0) { 245 | return new Document[0]; 246 | } 247 | 248 | final Document[] results = new Document[numResults]; 249 | 250 | final Iterator iterator = mDocuments.values().iterator(); 251 | 252 | // until the initial offset has been reached 253 | for (int i = 0; i < offset; i++) { 254 | // if more entries are available 255 | if (iterator.hasNext()) { 256 | // discard the next entry 257 | iterator.next(); 258 | } 259 | // if another entry was expected but there was none 260 | else { 261 | // return an error 262 | throw new IllegalStateException("Expected `"+numResults+"` entries but there were only `"+i+"`"); 263 | } 264 | } 265 | 266 | // until the number of elements to be returned has been reached 267 | for (int i = 0; i < numResults; i++) { 268 | // if more entries are available 269 | if (iterator.hasNext()) { 270 | // get the next entry and put it into the result list 271 | results[i] = iterator.next(); 272 | } 273 | // if another entry was expected but there was none 274 | else { 275 | // return an error 276 | throw new IllegalStateException("Expected `"+numResults+"` entries but there were only `"+(offset + i)+"`"); 277 | } 278 | } 279 | 280 | return results; 281 | } 282 | 283 | @Override 284 | public Document findOne() { 285 | // if there are entries available 286 | try { 287 | // return the first entry 288 | return mDocuments.values().iterator().next(); 289 | } 290 | catch (NoSuchElementException e) { 291 | return null; 292 | } 293 | } 294 | 295 | /** 296 | * Forces the given object, no matter what type, to be a (primitive) number 297 | * 298 | * @param value the object to convert to a number 299 | * @return the number coerced from the specified object 300 | */ 301 | private static double coerceNumber(final Object value) { 302 | if (value == null) { 303 | return 0; 304 | } 305 | 306 | Number number; 307 | 308 | if (value instanceof Byte) { 309 | number = (Byte) value; 310 | } 311 | else if (value instanceof Short) { 312 | number = (Short) value; 313 | } 314 | else if (value instanceof Integer) { 315 | number = (Integer) value; 316 | } 317 | else if (value instanceof Long) { 318 | number = (Long) value; 319 | } 320 | else if (value instanceof Float) { 321 | number = (Float) value; 322 | } 323 | else if (value instanceof Double) { 324 | number = (Double) value; 325 | } 326 | else if (value instanceof String) { 327 | try { 328 | number = Double.parseDouble((String) value); 329 | } 330 | catch (NumberFormatException e) { 331 | return 0; 332 | } 333 | } 334 | else { 335 | return 0; 336 | } 337 | 338 | return number.doubleValue(); 339 | } 340 | 341 | @Override 342 | public String toString() { 343 | return mDocuments.toString(); 344 | } 345 | 346 | } 347 | -------------------------------------------------------------------------------- /Source/sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "25.0.1" 6 | 7 | defaultConfig { 8 | applicationId "im.delight.android.ddp.examples" 9 | minSdkVersion 9 10 | targetSdkVersion 19 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile project(':library') 23 | } 24 | -------------------------------------------------------------------------------- /Source/sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/sample/src/main/java/im/delight/android/ddp/examples/MainActivity.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.ddp.examples; 2 | 3 | /* 4 | * Copyright (c) delight.im 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 | 19 | import im.delight.android.ddp.ResultListener; 20 | import java.util.Map; 21 | import java.util.HashMap; 22 | import im.delight.android.ddp.Meteor; 23 | import im.delight.android.ddp.MeteorCallback; 24 | import im.delight.android.ddp.db.memory.InMemoryDatabase; 25 | import android.app.Activity; 26 | import android.os.Bundle; 27 | 28 | public class MainActivity extends Activity implements MeteorCallback { 29 | 30 | private Meteor mMeteor; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | 37 | // enable logging of internal events for the library 38 | Meteor.setLoggingEnabled(true); 39 | 40 | // create a new instance 41 | mMeteor = new Meteor(this, "ws://www.meteor.com/websocket", new InMemoryDatabase()); 42 | 43 | // register the callback that will handle events and receive messages 44 | mMeteor.addCallback(this); 45 | 46 | // establish the connection 47 | mMeteor.connect(); 48 | } 49 | 50 | @Override 51 | public void onConnect(final boolean signedInAutomatically) { 52 | System.out.println("Connected"); 53 | System.out.println("Is logged in: "+mMeteor.isLoggedIn()); 54 | System.out.println("User ID: "+mMeteor.getUserId()); 55 | 56 | if (signedInAutomatically) { 57 | System.out.println("Successfully logged in automatically"); 58 | } 59 | else { 60 | // sign up for a new account 61 | mMeteor.registerAndLogin("john-doe", "john.doe@example.com", "password1", new ResultListener() { 62 | 63 | @Override 64 | public void onSuccess(String result) { 65 | System.out.println("Successfully registered: "+result); 66 | } 67 | 68 | @Override 69 | public void onError(String error, String reason, String details) { 70 | System.out.println("Could not register: "+error+" / "+reason+" / "+details); 71 | } 72 | 73 | }); 74 | 75 | // sign in to the server 76 | mMeteor.loginWithUsername("john-doe", "password1", new ResultListener() { 77 | 78 | @Override 79 | public void onSuccess(String result) { 80 | System.out.println("Successfully logged in: "+result); 81 | 82 | System.out.println("Is logged in: "+mMeteor.isLoggedIn()); 83 | System.out.println("User ID: "+mMeteor.getUserId()); 84 | } 85 | 86 | @Override 87 | public void onError(String error, String reason, String details) { 88 | System.out.println("Could not log in: "+error+" / "+reason+" / "+details); 89 | } 90 | 91 | }); 92 | } 93 | 94 | // subscribe to data from the server 95 | String subscriptionId = mMeteor.subscribe("meetups"); 96 | 97 | // unsubscribe from data again (usually done later or not at all) 98 | mMeteor.unsubscribe(subscriptionId); 99 | 100 | // insert data into a collection 101 | Map insertValues = new HashMap(); 102 | insertValues.put("_id", "my-key"); 103 | insertValues.put("some-number", 3); 104 | mMeteor.insert("my-collection", insertValues); 105 | 106 | // update data in a collection 107 | Map updateQuery = new HashMap(); 108 | updateQuery.put("_id", "my-key"); 109 | Map updateValues = new HashMap(); 110 | updateValues.put("_id", "my-key"); 111 | updateValues.put("some-number", 5); 112 | mMeteor.update("my-collection", updateQuery, updateValues); 113 | 114 | // remove data from a collection 115 | mMeteor.remove("my-collection", "my-key"); 116 | 117 | // call an arbitrary method 118 | mMeteor.call("myMethod"); 119 | } 120 | 121 | @Override 122 | public void onDisconnect() { 123 | System.out.println("Disconnected"); 124 | } 125 | 126 | @Override 127 | public void onDataAdded(String collectionName, String documentID, String fieldsJson) { 128 | System.out.println("Data added to <"+collectionName+"> in document <"+documentID+">"); 129 | System.out.println(" Added: "+fieldsJson); 130 | } 131 | 132 | @Override 133 | public void onDataChanged(String collectionName, String documentID, String updatedValuesJson, String removedValuesJson) { 134 | System.out.println("Data changed in <"+collectionName+"> in document <"+documentID+">"); 135 | System.out.println(" Updated: "+updatedValuesJson); 136 | System.out.println(" Removed: "+removedValuesJson); 137 | } 138 | 139 | @Override 140 | public void onDataRemoved(String collectionName, String documentID) { 141 | System.out.println("Data removed from <"+collectionName+"> in document <"+documentID+">"); 142 | } 143 | 144 | @Override 145 | public void onException(Exception e) { 146 | System.out.println("Exception"); 147 | if (e != null) { 148 | e.printStackTrace(); 149 | } 150 | } 151 | 152 | @Override 153 | public void onDestroy() { 154 | mMeteor.disconnect(); 155 | mMeteor.removeCallback(this); 156 | // or 157 | // mMeteor.removeCallbacks(); 158 | 159 | // ... 160 | 161 | super.onDestroy(); 162 | } 163 | 164 | /*private void testDatabase() { 165 | // first Meteor#handleMessage has to be made public temporarily 166 | 167 | // mock some data that is being added 168 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"people\",\"id\":\"al\",\"fields\":{\"name\":\"Alice\",\"age\":25,\"gender\":\"f\",\"location\":{\"country\":\"Japan\",\"region\":\"Kansai\"}}}"); 169 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"people\",\"id\":\"bo\",\"fields\":{\"name\":\"Bob\",\"age\":27,\"gender\":\"m\",\"location\":{\"country\":\"Spain\",\"region\":\"Andalusia\"}}}"); 170 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"people\",\"id\":\"ca\",\"fields\":{\"name\":\"Carol\",\"age\":29,\"gender\":null,\"location\":null}}"); 171 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"people\",\"id\":\"ev\",\"fields\":{\"name\":\"Eve\",\"age\":31,\"gender\":\"f\",\"location\":{\"country\":\"Australia\",\"region\":null}}}"); 172 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"settings\",\"id\":\"5h2iJyPMZmDTaSwGC\",\"fields\":{\"appId\":\"92Hn8HKvatWDPP22u\",\"endpoint\":\"http://www.example.com/\",\"clientDelay\":10000,\"enableSomething\":true}}"); 173 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"companies\",\"id\":\"31c53bca49616e773567920d\",\"fields\":{\"owner\":null,\"isInProgress\":true,\"Description\":\"Acme Inc. is a company\",\"Company\":\"Acme Inc.\",\"Location\":\"Some city, Some country\",\"Region\":\"SomeContinent/SomeOtherContinent\",\"Logo\":\"/assets/i/companies/default-logo.png\",\"Type\":\"Things\",\"Website\":\"http://www.example.com/\",\"prime\":false}}"); 174 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"versions\",\"id\":\"JyPMZ49616e7735\",\"fields\":{\"version\":\"ae976571be8a6999984eae9da24fc5d948ca80ac\",\"assets\":{\"allCss\":[{\"url\":\"/main.css\"}]}}}"); 175 | mMeteor.handleMessage("{\"msg\":\"added\",\"collection\":\"events\",\"id\":\"2H7ZDva9nhL4F42im\",\"fields\":{\"coords\":{\"type\":\"Point\",\"coordinates\":[1.23,-2.345]},\"events\":[{\"eventId\":\"946221490\",\"eventName\":\"Meteor 101\",\"eventUrl\":\"http://www.example.com/946221490\",\"eventTime\":1000000000000,\"eventUTCOffset\":-3600000}],\"groupId\":2018074068,\"groupName\":\"Meteor 101 A\",\"groupUrlname\":\"Meteor-101-A\"}}"); 176 | 177 | // mock some data that is being changed 178 | mMeteor.handleMessage("{\"msg\":\"changed\",\"collection\":\"people\",\"id\":\"ev\",\"fields\":{\"age\":23,\"location\":{\"region\":\"New South Wales\"}},\"cleared\":[\"gender\"]}"); 179 | 180 | // mock some data that is being removed 181 | mMeteor.handleMessage("{\"msg\":\"removed\",\"collection\":\"people\",\"id\":\"ca\"}"); 182 | 183 | // get a reference to the database 184 | final Database database = mMeteor.getDatabase(); 185 | 186 | // wait a second 187 | new Handler().postDelayed(new Runnable() { 188 | 189 | @Override 190 | public void run() { 191 | // and then check what's in there 192 | 193 | // test the database operations 194 | System.out.println("Database#count() = " + database.count()); 195 | System.out.println("Database#getCollectionNames() = " + Arrays.toString(database.getCollectionNames())); 196 | System.out.println("Database#getCollection(\"customers\") = " + database.getCollection("customers")); 197 | System.out.println("Database#getCollection(\"customers\").count() = " + database.getCollection("customers").count()); 198 | System.out.println("Database#getCollection(\"people\") = " + database.getCollection("people")); 199 | 200 | // print a divider 201 | System.out.println("----------"); 202 | 203 | // get a reference to a collection 204 | final Collection collection = database.getCollection("people"); 205 | 206 | // test the collection operations 207 | System.out.println("Collection#getName() = " + collection.getName()); 208 | System.out.println("Collection#count() = " + collection.count()); 209 | System.out.println("Collection#getDocumentIds() = " + Arrays.toString(collection.getDocumentIds())); 210 | System.out.println("Collection#getDocument(\"jo\") = " + collection.getDocument("jo")); 211 | System.out.println("Collection#getDocument(\"al\") = " + collection.getDocument("al")); 212 | System.out.println("Collection#getDocument(\"ca\") = " + collection.getDocument("ca")); 213 | System.out.println("Collection#getDocument(\"ev\") = " + collection.getDocument("ev")); 214 | 215 | // print a divider 216 | System.out.println("----------"); 217 | 218 | // get a reference to a document 219 | final Document document = collection.getDocument("al"); 220 | 221 | // test the document operations 222 | System.out.println("Document#getId() = " + document.getId()); 223 | System.out.println("Document#count() = " + document.count()); 224 | System.out.println("Document#getFieldNames() = " + Arrays.toString(document.getFieldNames())); 225 | System.out.println("Document#getField(\"age\") = " + document.getField("age")); 226 | 227 | // print a divider 228 | System.out.println("----------"); 229 | 230 | // test the query builder operations 231 | System.out.println("Collection#findOne() = " + collection.findOne()); 232 | System.out.println("Collection#find(1) = " + Arrays.toString(collection.find(1))); 233 | System.out.println("Collection#find(2) = " + Arrays.toString(collection.find(2))); 234 | System.out.println("Collection#find(5) = " + Arrays.toString(collection.find(5))); 235 | System.out.println("Collection#find(1, 1) = " + Arrays.toString(collection.find(1, 1))); 236 | System.out.println("Collection#find(1, 2) = " + Arrays.toString(collection.find(1, 2))); 237 | System.out.println("Collection#find(2, 1) = " + Arrays.toString(collection.find(2, 1))); 238 | System.out.println("Collection#find(2, 2) = " + Arrays.toString(collection.find(2, 2))); 239 | System.out.println("Collection#find() = " + Arrays.toString(collection.find())); 240 | System.out.println("Collection#whereEqual(\"name\", \"Eve\").find() = " + Arrays.toString(collection.whereEqual("name", "Eve").find())); 241 | System.out.println("Collection#whereNotEqual(\"name\", \"Eve\").find() = " + Arrays.toString(collection.whereNotEqual("name", "Eve").find())); 242 | System.out.println("Collection#whereLessThan(\"age\", 27).find() = " + Arrays.toString(collection.whereLessThan("age", 27).find())); 243 | System.out.println("Collection#whereLessThanOrEqual(\"age\", 27).find() = " + Arrays.toString(collection.whereLessThanOrEqual("age", 27).find())); 244 | System.out.println("Collection#whereLessThan(\"age\", 25).find() = " + Arrays.toString(collection.whereLessThan("age", 25).find())); 245 | System.out.println("Collection#whereGreaterThan(\"age\", 23).find() = " + Arrays.toString(collection.whereGreaterThan("age", 23).find())); 246 | System.out.println("Collection#whereGreaterThanOrEqual(\"age\", 23).find() = " + Arrays.toString(collection.whereGreaterThanOrEqual("age", 23).find())); 247 | System.out.println("Collection#whereGreaterThan(\"age\", 25).find() = " + Arrays.toString(collection.whereGreaterThan("age", 25).find())); 248 | System.out.println("Collection#whereNull(\"location\").find() = " + Arrays.toString(collection.whereNull("location").find())); 249 | System.out.println("Collection#whereNotNull(\"location\").find() = " + Arrays.toString(collection.whereNotNull("location").find())); 250 | System.out.println("Collection#whereNotNull(\"gender\").whereLessThan(\"age\", 26).find() = " + Arrays.toString(collection.whereNotNull("gender").whereLessThan("age", 26).find())); 251 | } 252 | 253 | }, 1000); 254 | }*/ 255 | 256 | } 257 | -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/Android-DDP/4ae26e3728b5d2956ec670a0667da20f2b40b430/Source/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Source/sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /Source/sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Android-DDP 4 | Android DDP 5 | 6 | -------------------------------------------------------------------------------- /Source/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | include ':sample' 3 | --------------------------------------------------------------------------------