├── .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 extends String, ? extends InMemoryDocument> 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 |
--------------------------------------------------------------------------------