Java tutorial
package im.delight.android.ddp; /* * Copyright (c) delight.im <info@delight.im> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.content.Context; import android.content.SharedPreferences; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Queue; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import im.delight.android.ddp.db.DataStore; import im.delight.android.ddp.db.Database; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; /** Client that connects to Meteor servers implementing the DDP protocol */ public class Meteor { private static final String TAG = "Meteor"; /** Supported versions of the DDP protocol in order of preference */ private static final String[] SUPPORTED_DDP_VERSIONS = { "1", "pre2", "pre1" }; /** The maximum number of attempts to re-connect to the server over WebSocket */ private static final int RECONNECT_ATTEMPTS_MAX = 5; /** * Instance of Gson that converts between JSON and Java objects (POJOs) */ private static final Gson mGson = new Gson(); /** * Whether logging should be enabled or not */ private static boolean mLoggingEnabled; /** * The callbacks that will handle events and receive messages from this client */ protected final CallbackProxy mCallbackProxy = new CallbackProxy(); /** The callback that handles messages and events received from the WebSocket connection */ private final WebSocketListener mWebSocketListener; /** Map that tracks all pending Listener instances */ private final Map<String, Listener> mListeners; /** Messages that couldn't be dispatched yet and thus had to be queued */ private final Queue<String> mQueuedMessages; private final Context mContext; private final DataStore mDataStore; /** The WebSocket connection that will be used for the data transfer */ private WebSocket mWebSocket; private String mServerUri; private String mDdpVersion; /** The number of unsuccessful attempts to re-connect in sequence */ private int mReconnectAttempts; private String mSessionID; private boolean mConnected; private String mLoggedInUserId; /** * Returns a new instance for a client connecting to a server via DDP over websocket * * The server URI should usually be in the form of `ws://example.meteor.com/websocket` * * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) * @param serverUri the server URI to connect to */ public Meteor(final Context context, final String serverUri) { this(context, serverUri, (DataStore) null); } /** * Returns a new instance for a client connecting to a server via DDP over websocket * * The server URI should usually be in the form of `ws://example.meteor.com/websocket` * * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) * @param serverUri the server URI to connect to * @param dataStore the data store to write data to */ public Meteor(final Context context, final String serverUri, final DataStore dataStore) { this(context, serverUri, SUPPORTED_DDP_VERSIONS[0], dataStore); } /** * Returns a new instance for a client connecting to a server via DDP over websocket * * The server URI should usually be in the form of `ws://example.meteor.com/websocket` * * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) * @param serverUri the server URI to connect to * @param protocolVersion the desired DDP protocol version */ public Meteor(final Context context, final String serverUri, final String protocolVersion) { this(context, serverUri, protocolVersion, null); } /** * Returns a new instance for a client connecting to a server via DDP over websocket * * The server URI should usually be in the form of `ws://example.meteor.com/websocket` * * @param context a `Context` reference (e.g. an `Activity` or `Service` instance) * @param serverUri the server URI to connect to * @param protocolVersion the desired DDP protocol version * @param dataStore the data store to write data to */ public Meteor(final Context context, final String serverUri, final String protocolVersion, final DataStore dataStore) { if (!isVersionSupported(protocolVersion)) { throw new IllegalArgumentException("DDP protocol version not supported: " + protocolVersion); } if (context == null) { throw new IllegalArgumentException("The context reference may not be null"); } // save the context reference mContext = context.getApplicationContext(); // save the data store reference mDataStore = dataStore; mWebSocketListener = new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); log(TAG); log(" onOpen"); mConnected = true; mReconnectAttempts = 0; initConnection(mSessionID); } @Override public void onMessage(WebSocket webSocket, String text) { super.onMessage(webSocket, text); log(TAG); log(" onTextMessage"); log(" payload == " + text); handleMessage(text); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { super.onClosing(webSocket, code, reason); } @Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); log(TAG); log(" onClose"); final boolean lostConnection = mConnected; mConnected = false; if (lostConnection) { mReconnectAttempts++; if (mReconnectAttempts <= RECONNECT_ATTEMPTS_MAX) { // try to re-connect automatically reconnect(); } else { disconnect(); } } mCallbackProxy.onDisconnect(); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); mCallbackProxy.onException(new Exception(t)); } }; // create a map that holds the pending Listener instances mListeners = new HashMap<String, Listener>(); // create a queue that holds undispatched messages waiting to be sent mQueuedMessages = new ConcurrentLinkedQueue<String>(); // save the server URI mServerUri = serverUri; // try with the preferred DDP protocol version first mDdpVersion = protocolVersion; // count the number of failed attempts to re-connect mReconnectAttempts = 0; } /** * Returns whether the given JSON result is from a previous login attempt * * @param result the JSON result * @return whether the result is from a login attempt (`true`) or not (`false`) */ private static boolean isLoginResult(final JsonObject result) { return result.has(Protocol.Field.TOKEN) && result.has(Protocol.Field.ID); } /** * Returns whether the specified version of the DDP protocol is supported or not * * @param protocolVersion the DDP protocol version * @return whether the version is supported or not */ public static boolean isVersionSupported(final String protocolVersion) { return Arrays.asList(SUPPORTED_DDP_VERSIONS).contains(protocolVersion); } /** * Sets whether logging of internal events and data flow should be enabled for this library * * @param enabled whether logging should be enabled (`true`) or not (`false`) */ public static void setLoggingEnabled(final boolean enabled) { mLoggingEnabled = enabled; } /** * Logs a message if logging has been enabled * * @param message the message to log */ public static void log(final String message) { if (mLoggingEnabled) { System.out.println(message); } } /** * Creates and returns a new unique ID * * @return the new unique ID */ public static String uniqueID() { return UUID.randomUUID().toString(); } /** * Creates an empty map for use as default parameter * * @return an empty map */ private static Map<String, Object> emptyMap() { return new HashMap<String, Object>(); } /** Attempts to establish the connection to the server */ public void connect() { openConnection(false); } /** * Returns whether this client is connected or not * * @return whether this client is connected */ public boolean isConnected() { return mConnected; } /** Manually attempt to re-connect if necessary */ public void reconnect() { openConnection(true); } /** * Opens a connection to the server over websocket * * @param isReconnect whether this is a re-connect attempt or not */ private void openConnection(final boolean isReconnect) { if (isReconnect) { if (mConnected) { initConnection(mSessionID); return; } } OkHttpClient client = new OkHttpClient.Builder().readTimeout(30000, TimeUnit.MILLISECONDS) .pingInterval(25 * 1000, TimeUnit.MICROSECONDS).build(); Request request = new Request.Builder().url(mServerUri).build(); mWebSocket = client.newWebSocket(request, mWebSocketListener); } /** * Establish the connection to the server as requested by the DDP protocol (after the websocket has been opened) * * @param existingSessionID an existing session ID or `null` */ private void initConnection(final String existingSessionID) { final Map<String, Object> data = new HashMap<String, Object>(); data.put(Protocol.Field.MESSAGE, Protocol.Message.CONNECT); data.put(Protocol.Field.VERSION, mDdpVersion); data.put(Protocol.Field.SUPPORT, SUPPORTED_DDP_VERSIONS); if (existingSessionID != null) { data.put(Protocol.Field.SESSION, existingSessionID); } send(data); } /** Disconnect the client from the server */ public void disconnect() { mConnected = false; mListeners.clear(); mSessionID = null; if (mWebSocket != null) { mWebSocket.cancel(); } else { throw new IllegalStateException( "You must have called the 'connect' method before you can disconnect again"); } } /** * Sends a Java object (POJO) over the websocket after serializing it with the Jackson library * * @param obj the Java object to send */ private void send(final Object obj) { // serialize the object to JSON final String jsonStr = toJson(obj); if (jsonStr == null) { throw new IllegalArgumentException("Object would be serialized to `null`"); } // send the JSON string send(jsonStr); } /** * Sends a string over the websocket * * @param message the string to send */ private void send(final String message) { log(TAG); log(" send"); log(" message == " + message); if (message == null) { throw new IllegalArgumentException("You cannot send `null` messages"); } if (mConnected) { log(" dispatching"); if (mWebSocket != null) { mWebSocket.send(message); } else { throw new IllegalStateException( "You must have called the 'connect' method before you can send data"); } } else { log(" queueing"); mQueuedMessages.add(message); } } /** * Adds a callback that will handle events and receive messages from this client * * @param callback the callback instance */ public void addCallback(MeteorCallback callback) { mCallbackProxy.addCallback(callback); } /** * Removes a callback that was to handle events and receive messages from this client * * @param callback the callback instance */ public void removeCallback(MeteorCallback callback) { mCallbackProxy.removeCallback(callback); } /** Removes all callbacks that were to handle events and receive messages from this client */ public void removeCallbacks() { mCallbackProxy.removeCallbacks(); } /** * Serializes the given Java object (POJO) with the Jackson library * * @param obj the object to serialize * @return the serialized object in JSON format */ private String toJson(Object obj) { try { return mGson.toJson(obj); } catch (Exception e) { mCallbackProxy.onException(e); return null; } } private <T> T fromJson(final String json, final Class<T> targetType) { try { if (json != null) { return mGson.fromJson(json, targetType); } else { return null; } } catch (Exception e) { mCallbackProxy.onException(e); return null; } } /** * Called whenever a JSON payload has been received from the websocket * * @param payload the JSON payload to process */ private void handleMessage(final String payload) { final JsonObject data = mGson.fromJson(payload, JsonObject.class); if (data != null) { if (data.has(Protocol.Field.MESSAGE)) { final String message = data.get(Protocol.Field.MESSAGE).getAsString(); if (message.equals(Protocol.Message.CONNECTED)) { if (data.has(Protocol.Field.SESSION)) { mSessionID = data.get(Protocol.Field.SESSION).getAsString(); } // initialize the new session initSession(); } else if (message.equals(Protocol.Message.FAILED)) { if (data.has(Protocol.Field.VERSION)) { // the server wants to use a different protocol version final String desiredVersion = data.get(Protocol.Field.VERSION).getAsString(); // if the protocol version that was requested by the server is supported by this client if (isVersionSupported(desiredVersion)) { // remember which version has been requested mDdpVersion = desiredVersion; // the server should be closing the connection now and we will re-connect afterwards } else { throw new RuntimeException("Protocol version not supported: " + desiredVersion); } } } else if (message.equals(Protocol.Message.PING)) { final String id; if (data.has(Protocol.Field.ID)) { id = data.get(Protocol.Field.ID).getAsString(); } else { id = null; } sendPong(id); } else if (message.equals(Protocol.Message.ADDED) || message.equals(Protocol.Message.ADDED_BEFORE)) { final String documentID; if (data.has(Protocol.Field.ID)) { documentID = data.get(Protocol.Field.ID).getAsString(); } else { documentID = null; } final String collectionName; if (data.has(Protocol.Field.COLLECTION)) { collectionName = data.get(Protocol.Field.COLLECTION).getAsString(); } else { collectionName = null; } final String newValuesJson; if (data.has(Protocol.Field.FIELDS)) { newValuesJson = data.get(Protocol.Field.FIELDS).toString(); } else { newValuesJson = null; } if (mDataStore != null) { mDataStore.onDataAdded(collectionName, documentID, fromJson(newValuesJson, Fields.class)); } mCallbackProxy.onDataAdded(collectionName, documentID, newValuesJson); } else if (message.equals(Protocol.Message.CHANGED)) { final String documentID; if (data.has(Protocol.Field.ID)) { documentID = data.get(Protocol.Field.ID).getAsString(); } else { documentID = null; } final String collectionName; if (data.has(Protocol.Field.COLLECTION)) { collectionName = data.get(Protocol.Field.COLLECTION).getAsString(); } else { collectionName = null; } final String updatedValuesJson; if (data.has(Protocol.Field.FIELDS)) { updatedValuesJson = data.get(Protocol.Field.FIELDS).toString(); } else { updatedValuesJson = null; } final String removedValuesJson; if (data.has(Protocol.Field.CLEARED)) { removedValuesJson = data.get(Protocol.Field.CLEARED).toString(); } else { removedValuesJson = null; } if (mDataStore != null) { mDataStore.onDataChanged(collectionName, documentID, fromJson(updatedValuesJson, Fields.class), fromJson(removedValuesJson, String[].class)); } mCallbackProxy.onDataChanged(collectionName, documentID, updatedValuesJson, removedValuesJson); } else if (message.equals(Protocol.Message.REMOVED)) { final String documentID; if (data.has(Protocol.Field.ID)) { documentID = data.get(Protocol.Field.ID).getAsString(); } else { documentID = null; } final String collectionName; if (data.has(Protocol.Field.COLLECTION)) { collectionName = data.get(Protocol.Field.COLLECTION).getAsString(); } else { collectionName = null; } if (mDataStore != null) { mDataStore.onDataRemoved(collectionName, documentID); } mCallbackProxy.onDataRemoved(collectionName, documentID); } else if (message.equals(Protocol.Message.RESULT)) { // check if we have to process any result data internally if (data.has(Protocol.Field.RESULT)) { final JsonObject resultData = data.getAsJsonObject(Protocol.Field.RESULT); // if the result is from a previous login attempt if (isLoginResult(resultData)) { // extract the login token for subsequent automatic re-login final String loginToken = resultData.get(Protocol.Field.TOKEN).getAsString(); saveLoginToken(loginToken); // extract the user's ID mLoggedInUserId = resultData.get(Protocol.Field.ID).getAsString(); } } final String id; if (data.has(Protocol.Field.ID)) { id = data.get(Protocol.Field.ID).getAsString(); } else { id = null; } final Listener listener = mListeners.get(id); if (listener instanceof ResultListener) { mListeners.remove(id); final String result; if (data.has(Protocol.Field.RESULT)) { result = data.get(Protocol.Field.RESULT).toString(); } else { result = null; } if (data.has(Protocol.Field.ERROR)) { final Protocol.Error error = Protocol.Error .fromJson(data.getAsJsonObject(Protocol.Field.ERROR)); mCallbackProxy.forResultListener((ResultListener) listener).onError(error.getError(), error.getReason(), error.getDetails()); } else { mCallbackProxy.forResultListener((ResultListener) listener).onSuccess(result); } } } else if (message.equals(Protocol.Message.READY)) { if (data.has(Protocol.Field.SUBS)) { final JsonArray subs = data.getAsJsonArray(Protocol.Field.SUBS); for (JsonElement sub : subs) { String subscriptionId = sub.getAsString(); final Listener listener = mListeners.get(subscriptionId); if (listener instanceof SubscribeListener) { mListeners.remove(subscriptionId); mCallbackProxy.forSubscribeListener((SubscribeListener) listener).onSuccess(); } } } } else if (message.equals(Protocol.Message.NOSUB)) { final String subscriptionId; if (data.has(Protocol.Field.ID)) { subscriptionId = data.get(Protocol.Field.ID).getAsString(); } else { subscriptionId = null; } final Listener listener = mListeners.get(subscriptionId); if (listener instanceof SubscribeListener) { mListeners.remove(subscriptionId); if (data.has(Protocol.Field.ERROR)) { final Protocol.Error error = Protocol.Error .fromJson(data.getAsJsonObject(Protocol.Field.ERROR)); mCallbackProxy.forSubscribeListener((SubscribeListener) listener) .onError(error.getError(), error.getReason(), error.getDetails()); } else { mCallbackProxy.forSubscribeListener((SubscribeListener) listener).onError(null, null, null); } } else if (listener instanceof UnsubscribeListener) { mListeners.remove(subscriptionId); mCallbackProxy.forUnsubscribeListener((UnsubscribeListener) listener).onSuccess(); } } } } } /** * Returns whether the client is currently logged in as some user * * @return whether the client is logged in (`true`) or not (`false`) */ public boolean isLoggedIn() { return mLoggedInUserId != null; } /** * Returns the ID of the user who is currently logged in * * @return the ID or `null` */ public String getUserId() { return mLoggedInUserId; } /** * Sends a `pong` over the websocket as a reply to an incoming `ping` * * @param id the ID extracted from the `ping` or `null` */ private void sendPong(final String id) { final Map<String, Object> data = new HashMap<String, Object>(); data.put(Protocol.Field.MESSAGE, Protocol.Message.PONG); if (id != null) { data.put(Protocol.Field.ID, id); } send(data); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param data the data to insert */ public void insert(final String collectionName, final Map<String, Object> data) { insert(collectionName, data, null); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param data the data to insert * @param listener the listener to call on success/error */ public void insert(final String collectionName, final Map<String, Object> data, final ResultListener listener) { call("/" + collectionName + "/insert", new Object[] { data }, listener); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param query the query to select the document to update with * @param data the list of keys and values that should be set */ public void update(final String collectionName, final Map<String, Object> query, final Map<String, Object> data) { update(collectionName, query, data, emptyMap()); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param query the query to select the document to update with * @param data the list of keys and values that should be set * @param options the list of option parameters */ public void update(final String collectionName, final Map<String, Object> query, final Map<String, Object> data, final Map<String, Object> options) { update(collectionName, query, data, options, null); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param query the query to select the document to update with * @param data the list of keys and values that should be set * @param options the list of option parameters * @param listener the listener to call on success/error */ public void update(final String collectionName, final Map<String, Object> query, final Map<String, Object> data, final Map<String, Object> options, final ResultListener listener) { call("/" + collectionName + "/update", new Object[] { query, data, options }, listener); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param documentID the ID of the document to remove */ public void remove(final String collectionName, final String documentID) { remove(collectionName, documentID, null); } /** * Insert given data into the specified collection * * @param collectionName the collection to insert the data into * @param documentId the ID of the document to remove * @param listener the listener to call on success/error */ public void remove(final String collectionName, final String documentId, final ResultListener listener) { final Map<String, Object> query = new HashMap<String, Object>(); query.put(MongoDb.Field.ID, documentId); call("/" + collectionName + "/remove", new Object[] { query }, listener); } /** * Sign in the user with the given username and password * * Please note that this requires the `accounts-password` package * * @param username the username to sign in with * @param password the password to sign in with * @param listener the listener to call on success/error */ public void loginWithUsername(final String username, final String password, final ResultListener listener) { login(username, null, password, listener); } /** * Sign in the user with the given email address and password * * Please note that this requires the `accounts-password` package * * @param email the email address to sign in with * @param password the password to sign in with * @param listener the listener to call on success/error */ public void loginWithEmail(final String email, final String password, final ResultListener listener) { login(null, email, password, listener); } /** * Sign in the user with the given username or email address and the specified password * * Please note that this requires the `accounts-password` package * * @param username the username to sign in with (either this or `email` is required) * @param email the email address to sign in with (either this or `username` is required) * @param password the password to sign in with * @param listener the listener to call on success/error */ private void login(final String username, final String email, final String password, final ResultListener listener) { final Map<String, Object> userData = new HashMap<String, Object>(); if (username != null) { userData.put("username", username); } else if (email != null) { userData.put("email", email); } else { throw new IllegalArgumentException("You must provide either a username or an email address"); } final Map<String, Object> authData = new HashMap<String, Object>(); authData.put("user", userData); authData.put("password", password); call("login", new Object[] { authData }, listener); } /** * Attempts to sign in with the given login token * * @param token the login token * @param listener the listener to call on success/error */ private void loginWithToken(final String token, final ResultListener listener) { final Map<String, Object> authData = new HashMap<String, Object>(); authData.put("resume", token); call("login", new Object[] { authData }, listener); } public void logout() { logout(null); } public void logout(final ResultListener listener) { call("logout", new Object[] {}, new ResultListener() { @Override public void onSuccess(final String result) { // remember that we're not logged in anymore mLoggedInUserId = null; // delete the last login token which is now invalid saveLoginToken(null); if (listener != null) { mCallbackProxy.forResultListener(listener).onSuccess(result); } } @Override public void onError(final String error, final String reason, final String details) { if (listener != null) { mCallbackProxy.forResultListener(listener).onError(error, reason, details); } } }); } /** * Registers a new user with the specified username, email address and password * * This method will automatically login as the new user on success * * Please note that this requires the `accounts-password` package * * @param username the username to register with (either this or `email` is required) * @param email the email address to register with (either this or `username` is required) * @param password the password to register with * @param listener the listener to call on success/error */ public void registerAndLogin(final String username, final String email, final String password, final ResultListener listener) { registerAndLogin(username, email, password, null, listener); } /** * Registers a new user with the specified username, email address and password * * This method will automatically login as the new user on success * * Please note that this requires the `accounts-password` package * * @param username the username to register with (either this or `email` is required) * @param email the email address to register with (either this or `username` is required) * @param password the password to register with * @param profile the user's profile data, typically including a `name` field * @param listener the listener to call on success/error */ public void registerAndLogin(final String username, final String email, final String password, final HashMap<String, Object> profile, final ResultListener listener) { if (username == null && email == null) { throw new IllegalArgumentException("You must provide either a username or an email address"); } final Map<String, Object> accountData = new HashMap<String, Object>(); if (username != null) { accountData.put("username", username); } if (email != null) { accountData.put("email", email); } accountData.put("password", password); if (profile != null) { accountData.put("profile", profile); } call("createUser", new Object[] { accountData }, listener); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` */ public void call(final String methodName) { call(methodName, null, null); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param params the objects that should be passed to the method as parameters */ public void call(final String methodName, final Object[] params) { call(methodName, params, null); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param listener the listener to trigger when the result has been received or `null` */ public void call(final String methodName, final ResultListener listener) { call(methodName, null, listener); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param params the objects that should be passed to the method as parameters * @param listener the listener to trigger when the result has been received or `null` */ public void call(final String methodName, final Object[] params, final ResultListener listener) { callWithSeed(methodName, null, params, listener); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param randomSeed an arbitrary seed for pseudo-random generators or `null` */ public void callWithSeed(final String methodName, final String randomSeed) { callWithSeed(methodName, randomSeed, null, null); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param randomSeed an arbitrary seed for pseudo-random generators or `null` * @param params the objects that should be passed to the method as parameters */ public void callWithSeed(final String methodName, final String randomSeed, final Object[] params) { callWithSeed(methodName, randomSeed, params, null); } /** * Executes a remote procedure call (any Java objects (POJOs) will be serialized to JSON by the Jackson library) * * @param methodName the name of the method to call, e.g. `/someCollection.insert` * @param randomSeed an arbitrary seed for pseudo-random generators or `null` * @param params the objects that should be passed to the method as parameters * @param listener the listener to trigger when the result has been received or `null` */ public void callWithSeed(final String methodName, final String randomSeed, final Object[] params, final ResultListener listener) { // create a new unique ID for this request final String callId = uniqueID(); // save a reference to the listener to be executed later if (listener != null) { mListeners.put(callId, listener); } final Map<String, Object> data = new HashMap<String, Object>(); data.put(Protocol.Field.MESSAGE, Protocol.Message.METHOD); data.put(Protocol.Field.METHOD, methodName); data.put(Protocol.Field.ID, callId); if (params != null) { data.put(Protocol.Field.PARAMS, params); } if (randomSeed != null) { data.put(Protocol.Field.RANDOM_SEED, randomSeed); } send(data); } /** * Subscribes to a specific subscription from the server * * @param subscriptionName the name of the subscription * @return the generated subscription ID (must be used when unsubscribing) */ public String subscribe(final String subscriptionName) { return subscribe(subscriptionName, null); } /** * Subscribes to a specific subscription from the server * * @param subscriptionName the name of the subscription * @param params the subscription parameters * @return the generated subscription ID (must be used when unsubscribing) */ public String subscribe(final String subscriptionName, final Object[] params) { return subscribe(subscriptionName, params, null); } /** * Subscribes to a specific subscription from the server * * @param subscriptionName the name of the subscription * @param params the subscription parameters * @param listener the listener to call on success/error * @return the generated subscription ID (must be used when unsubscribing) */ public String subscribe(final String subscriptionName, final Object[] params, final SubscribeListener listener) { // create a new unique ID for this request final String subscriptionId = uniqueID(); // save a reference to the listener to be executed later if (listener != null) { mListeners.put(subscriptionId, listener); } final Map<String, Object> data = new HashMap<String, Object>(); data.put(Protocol.Field.MESSAGE, Protocol.Message.SUBSCRIBE); data.put(Protocol.Field.NAME, subscriptionName); data.put(Protocol.Field.ID, subscriptionId); if (params != null) { data.put(Protocol.Field.PARAMS, params); } send(data); // return the generated subscription ID return subscriptionId; } /** * Unsubscribes from the subscription with the specified name * * @param subscriptionId the ID of the subscription */ public void unsubscribe(final String subscriptionId) { unsubscribe(subscriptionId, null); } /** * Unsubscribes from the subscription with the specified name * * @param subscriptionId the ID of the subscription * @param listener the listener to call on success/error */ public void unsubscribe(final String subscriptionId, final UnsubscribeListener listener) { // save a reference to the listener to be executed later if (listener != null) { mListeners.put(subscriptionId, listener); } final Map<String, Object> data = new HashMap<String, Object>(); data.put(Protocol.Field.MESSAGE, Protocol.Message.UNSUBSCRIBE); data.put(Protocol.Field.ID, subscriptionId); send(data); } /** * Saves the given login token to the preferences * * @param token the login token to save */ private void saveLoginToken(final String token) { final SharedPreferences.Editor editor = getSharedPreferences().edit(); editor.putString(Preferences.Keys.LOGIN_TOKEN, token); editor.apply(); } /** * Retrieves the last login token from the preferences * * @return the last login token or `null` */ private String getLoginToken() { return getSharedPreferences().getString(Preferences.Keys.LOGIN_TOKEN, null); } /** * Returns a reference to the preferences for internal use * * @return the `SharedPreferences` instance */ private SharedPreferences getSharedPreferences() { return mContext.getSharedPreferences(Preferences.FILE_NAME, Context.MODE_PRIVATE); } private void initSession() { // get the last login token final String loginToken = getLoginToken(); // if we found a login token that might work if (loginToken != null) { // try to sign in with that token loginWithToken(loginToken, new ResultListener() { @Override public void onSuccess(final String result) { announceSessionReady(true); } @Override public void onError(final String error, final String reason, final String details) { // clear the user ID since automatic sign-in has failed mLoggedInUserId = null; // discard the token which turned out to be invalid saveLoginToken(null); announceSessionReady(false); } }); } // if we didn't find any login token else { announceSessionReady(false); } } /** * Announces that the new session is now ready to use * * @param signedInAutomatically whether we have already signed in automatically (`true`) or not (`false)` */ private void announceSessionReady(final boolean signedInAutomatically) { // run the callback that waits for the connection to open mCallbackProxy.onConnect(signedInAutomatically); // try to dispatch queued messages now for (String queuedMessage : mQueuedMessages) { send(queuedMessage); } } /** * Returns the data store that was set in the constructor and that contains all data received from the server * * @return the data store or `null` */ public DataStore getDataStore() { return mDataStore; } /** * Returns the database that was set in the constructor and that contains all data received from the server * * @return the database or `null` */ public Database getDatabase() { if (mDataStore instanceof Database) { return (Database) mDataStore; } else { return null; } } }