com.keysolutions.ddpclient.android.DDPStateSingleton.java Source code

Java tutorial

Introduction

Here is the source code for com.keysolutions.ddpclient.android.DDPStateSingleton.java

Source

/*
* (c)Copyright 2013-2014 Ken Yee, KEY Enterprise Solutions 
*
* 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.
*/

package com.keysolutions.ddpclient.android;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.ConcurrentHashMap;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.google.gson.Gson;
import com.keysolutions.ddpclient.DDPClient;
import com.keysolutions.ddpclient.DDPClient.CONNSTATE;
import com.keysolutions.ddpclient.DDPClient.DdpMessageField;
import com.keysolutions.ddpclient.DDPClient.DdpMessageType;
import com.keysolutions.ddpclient.DDPListener;

/**
 * Base/common handling of DDP state with default handling of collection data as maps
 * Override data update methods to store data into SQLite or other data store
 * @author kenyee
 */
public class DDPStateSingleton extends MeteorAuthCommands implements Observer, DDPStateBroadcasts, DDPStateStorage {
    private static final String TAG = "DDPStateSingleton";
    public final static boolean TRACE = false; // for debugging

    // Broadcast Intent info
    public static final String MESSAGE_ERROR = "ddpclient.ERROR";
    public static final String MESSAGE_CONNECTION = "ddpclient.CONNECTIONSTATE";
    public static final String MESSAGE_METHODRESUlT = "ddpclient.METHODRESULT";
    public static final String MESSAGE_LOGINERROR = "ddpclient.LOGINERROR";
    public static final String MESSAGE_SUBUPDATED = "ddpclient.SUBUPDATED";
    public static final String MESSAGE_EXTRA_MSG = "ddpclient.REASON";
    public static final String MESSAGE_EXTRA_STATE = "ddpclient.STATE";
    public static final String MESSAGE_EXTRA_RESULT = "ddpclient.RESULT";
    public static final String MESSAGE_EXTRA_METHODID = "ddpclient.METHODID";
    public static final String MESSAGE_EXTRA_USERID = "ddpclient.USERID";
    public static final String MESSAGE_EXTRA_USERTOKEN = "ddpclient.USERTOKEN";
    public static final String MESSAGE_EXTRA_SUBNAME = "ddpclient.SUBNAME";
    public static final String MESSAGE_EXTRA_CHANGETYPE = "ddpclient.CHANGETYPE";
    public static final String MESSAGE_EXTRA_CHANGEID = "ddpclient.CHANGEID";
    // method ID for login
    public static final String METHODID_LOGIN = "login1";
    // resumetoken pref key
    public static final String PREF_DDPINFO = "ddp.info";
    public static final String PREF_RESUMETOKEN = "resume.token";

    /** instance of this class because it's a singleton */
    protected static DDPStateSingleton mInstance;

    /**
     * connection info for your Meteor server
     * Override to point to your server - this is kenyees default server
     */
    //protected static final String sMeteorServer = "demoparties.meteor.com";   // original implementation that didn't allow to overwrite the server
    protected static String mMeteorServer = "demoparties.meteor.com"; // BB-MOD default server

    /**
     * connection info for your Meteor server
     * Override to point to your server's port
     */
    // protected static final Integer sMeteorPort = 80;       // original implementation that didn't allow to overwrite it
    protected static Integer mMeteorPort = 80;

    /** reference to lower level DDP websocket client */
    protected DDPClient mDDP;

    /** reference to Android application context */
    private Context mContext;

    /** current DDP state */
    public enum DDPSTATE {
        NotLoggedIn, Connected, LoggedIn, Closed,
    };

    /** used to track DDP state */
    private DDPSTATE mDDPState;

    /** stores resume token on login */
    private String mResumeToken;

    /** stores user ID on login */
    private String mUserId;

    /**BB-MOD holds the ready state of subscriptions (subscriptionName, true or false) - so that the subscriber can know when to continue with the initiation */
    ConcurrentHashMap subscriptionsAreReady = new ConcurrentHashMap();

    /** internal storage for collections */
    // { collectionName,
    //      { docId, {
    //                  {fieldName1, fieldValue1},
    //                  {fieldName2, fieldValue2},
    //                }
    //      }
    // }
    private final Map<String, Map<String, Map<String, Object>>> mCollections = new ConcurrentHashMap<String, Map<String, Map<String, Object>>>();;

    /** Google GSON object for parsing JSON */
    protected final Gson mGSON = new Gson();

    /*BB-MOD OVERLOADING INIT INSTANCE so that you can call it without server and port
    // and it will connecta utomatically to the default server and port
    public static void initInstance(Context context) {
    Log.v("DDPStateSingleton", "initInstance - " + "context = [" + context + "]");
    if (mInstance == null) {
        mInstance = new DDPStateSingleton(context, "", 80);
    }
    }
        
    public static void initInstance(Context context, String meteorServer) {
    Log.v("DDPStateSingleton", "initInstance - " + "context = [" + context + "], meteorServer = [" + meteorServer + "]");
    if (mInstance == null) {
        mInstance = new DDPStateSingleton(context, meteorServer, 80);
    }
    }
    */

    public static void initInstance(Context context, String meteorServer, Integer meteorPort) {
        Log.v("DDPStateSingleton", "initInstance - " + "context = [" + context + "], meteorServer = ["
                + meteorServer + "], meteorPort = [" + meteorPort + "]");
        // only called by MyApplication
        if (mInstance == null) {
            // Create the instance
            mInstance = new DDPStateSingleton(context, meteorServer, meteorPort);
        }
    }

    /**
     * Gets an instance of this class
     * @return only instance of this class since it's a singleton
     */
    public static DDPStateSingleton getInstance() {
        return mInstance;
    }

    /**
     * Constructor for class (hidden because this is a singleton)
     * @param context Android application context
     * @param meteorServer String containing the meteor server ie: demoparties.meteor.com
     * @param meteorPort Integer of the meteor server port ie: 80
     */
    public DDPStateSingleton(Context context, String meteorServer, Integer meteorPort) {
        if (TRACE)
            Log.v("DDPStateSingleton", "DDPStateSingleton - " + "context = [" + context + "], meteorServer = ["
                    + meteorServer + "], meteorPort = [" + meteorPort + "]");
        this.mContext = context;
        this.mMeteorServer = meteorServer;
        this.mMeteorPort = meteorPort;
        //if (meteorServer != null) this.setServer(meteorServer);
        //if (meteorPort   != null) this.setServerPort(meteorPort);

        createDDPClient();
        // disable IPv6 if we're running on Android emulator because websocket
        // library doesn't work with it
        if ("google_sdk".equals(Build.PRODUCT)) {
            java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");
            java.lang.System.setProperty("java.net.preferIPv4Stack", "true");
        }
        // kick off a connection before UI starts up
        isConnected();
    }

    /**
     * Creates a new DDP websocket client (needed for reconnect because we can't reuse it)
     */
    public void createDDPClient() {
        if (TRACE)
            Log.i(TAG, "createDDPClient with server: " + getServer() + "  port: " + getServerPort());
        try {
            //mDDP = new DDPClient(sMeteorServer, sMeteorPort);
            mDDP = new DDPClient(getServer(), getServerPort());
            //            Log.i(TAG, "createDDPClient mDDP: " + mDDP);
        } catch (URISyntaxException e) {
            Log.e(TAG, "Invalid Websocket URL connecting to " + getServer() + ":" + getServerPort());
        }
        getDDP().addObserver(this);
        mDDPState = DDPSTATE.NotLoggedIn;
    }

    /**
     * Gets Java DDP Client object
     * @return DDP Client
     */
    //protected DDPClient getDDP() {
    // BB-MOD to allow to call mDDP methods like disconnect - TODO verify that you don't need this and change connectIfNeeded
    public DDPClient getDDP() {
        return mDDP;
    }

    /**
     * gets the Meteor Server
     * @return Meteor Server
     */
    public String getServer() {
        return mMeteorServer;
    }

    /**
     * sets the Meteor Server
     * it also restart the connection if there is one already connected
     * @param meteorServer
     */
    public void setServer(String meteorServer) {
        if (TRACE)
            Log.v("DDPStateSingleton", "setServer - " + "meteorServer = [" + meteorServer + "]");
        if (this.isConnected()) {
            getDDP().disconnect();
            //TODO - verify that the the current connection is closed via logging
            //Log.v("DDPStateSingleton", "setServer - just disconnected server state:" + getDDP().getState());
        }
        mMeteorServer = meteorServer;
        // autorestart the connection
        connectIfNeeded();

    }

    /**
     * sets the Meteor Port - default is 80
     * @return Integer value of the meteor port
     */
    public Integer getServerPort() {
        return mMeteorPort;
    }

    /**
     *
      * @param meteorPort
     */
    public void setServerPort(Integer meteorPort) {
        this.mMeteorPort = meteorPort;
    }

    /**
     * Gets state of DDP connection (including login info which isn't part of DDPClient state)
     * @return DDP connection state
     */
    public DDPSTATE getState() {
        if (TRACE)
            Log.v("DDPStateSingleton", "getState");
        //Log.v("DDPStateSingleton", "getState: " + getDDP().getState());
        // used by the UI to figure out what the current DDP connection state is
        if (getDDP().getState() == CONNSTATE.Closed) {
            mDDPState = DDPSTATE.Closed;
        }
        return mDDPState;
    }

    /**
     * Requests DDP connect if needed
     * @return true if connect was issue, otherwise false
     */
    public boolean connectIfNeeded() {
        if (TRACE)
            Log.w("DDPStateSingleton", "connectIfNeeded");
        if (getDDP().getState() == CONNSTATE.Disconnected) {
            Log.i(TAG, "connectIfNeeded - currently disconnected > connect");
            // make connection to Meteor server
            getDDP().connect();
            return true;

        } else if (getDDP().getState() == CONNSTATE.Closed) {
            Log.i(TAG, "connectIfNeeded - currently closed connection > Re-connect");
            // try to reconnect w/ a new connection
            createDDPClient();
            getDDP().connect();
        }
        return false;
    }

    /**
     * Whether we're connected to server
     * @return true if connected, false otherwise
     */
    public boolean isConnected() {
        if (TRACE)
            Log.v("DDPStateSingleton", "testing if isConnected");
        //Log.v("DDPStateSingleton", "isConnected - state:" + getDDP().getState());
        return ((getDDP().getState() != CONNSTATE.Disconnected) && (getDDP().getState() != CONNSTATE.Closed));
    }

    /**
     * Whether we're logged into server
     * @return true if connected, false otherwise
     */
    public boolean isLoggedIn() {
        if (TRACE)
            Log.v("DDPStateSingleton", "isLoggedIn - " + "");
        return mDDPState == DDPSTATE.LoggedIn;
    }

    /**
     * Logs out from server and removes the resume token
     */
    public void logout() {
        if (TRACE)
            Log.v("DDPStateSingleton", "logout");
        saveResumeToken(null);
        mDDPState = DDPSTATE.NotLoggedIn;
        mUserId = null;
        //REVIEW: do we need to cut websocket connection?
        broadcastConnectionState(mDDPState);
    }

    /**
     * Saves resume token to Android's internal app storage
     * @param token resume token
     */
    public void saveResumeToken(String token) {
        if (TRACE)
            Log.v("DDPStateSingleton", "saveResumeToken - " + "token = [" + token + "]");
        SharedPreferences settings = mContext.getSharedPreferences(PREF_DDPINFO, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        //REVIEW: should token be encrypted?
        editor.putString(PREF_RESUMETOKEN, token);
        editor.commit();
        Log.v("DDPStateSingleton", "saveResumeToken - settings" + settings + "  editor: " + editor);
    }

    /**
     * Gets resume token from Android's internal app storage
     * @return resume token or null if not found
     */
    public String getResumeToken() {
        if (TRACE)
            Log.v("DDPStateSingleton", "getResumeToken");
        // bb - the settings have already registered the token "Ep4E5iJ6jjkNB4wog"
        SharedPreferences settings = mContext.getSharedPreferences(PREF_DDPINFO, Context.MODE_PRIVATE);
        if (TRACE)
            Log.v("DDPStateSingleton", "getResumeToken - settings" + settings + " PREF_RESUMETOKEN:"
                    + settings.getString(PREF_RESUMETOKEN, null));
        return settings.getString(PREF_RESUMETOKEN, null);
    }

    /**
     * Handles callback from login command
     * @param jsonFields fields from result
     */
    @SuppressWarnings("unchecked")
    public void handleLoginResult(Map<String, Object> jsonFields) {
        if (TRACE)
            Log.v("DDPStateSingleton", "handleLoginResult - " + "jsonFields = [" + jsonFields + "]");
        if (jsonFields.containsKey("result")) {
            Map<String, Object> result = (Map<String, Object>) jsonFields.get(DdpMessageField.RESULT);
            mResumeToken = (String) result.get("token");
            saveResumeToken(mResumeToken);
            mUserId = (String) result.get("id");
            mDDPState = DDPSTATE.LoggedIn;
            broadcastConnectionState(mDDPState);
        } else if (jsonFields.containsKey("error")) {
            Map<String, Object> error = (Map<String, Object>) jsonFields.get(DdpMessageField.ERROR);
            broadcastDDPError((String) error.get("message"));
        }
    }

    /**
     * Subscribes to specified subscription (note: this can be different from collection)
     * @param subscriptionName name of subscription
     * @param params parameters for subscription function (e.g., doc ID, etc.)
     */
    public void subscribe(final String subscriptionName, Object[] params) {
        if (TRACE)
            Log.v("DDPStateSingleton",
                    "subscribe to " + "subscriptionName = [" + subscriptionName + "], params = [" + params + "]");

        // BB-Mod add the subscription to the HashMap with its initial value "false" which says that it is not ready
        subscriptionsAreReady.put(subscriptionName, false);

        // subscribe to a Meteor collection with given params
        // test error handling for invalid subscription
        getDDP().subscribe(subscriptionName, params, new DDPListener() {
            @Override
            public void onReady(String id) {
                Log.i("DDPStateSingleton", "subscribe > new DDPListener.onReady - subscription: " + subscriptionName
                        + "  -  id: " + id);
                //BB-MOD store that the subscription is ready && check the status
                subscriptionsAreReady.replace(subscriptionName, true);
                areAllSubscriptionsReady();

                // broadcast that subscription has been updated
                broadcastSubscriptionChanged(subscriptionName, DdpMessageType.READY, null);
            }

            @Override
            public void onResult(Map<String, Object> jsonFields) {
                //Log.v("DDPStateSingleton", "subscribe > new DDPListener.onResult - " + "jsonFields = [" + jsonFields + "]");
                String msgtype = (String) jsonFields.get(DDPClient.DdpMessageField.MSG);
                if (msgtype == null) {
                    // ignore {"server_id":"GqrKrbcSeDfTYDkzQ"} web socket msgs
                    return;
                } else if (msgtype.equals(DdpMessageType.ERROR)) {
                    broadcastDDPError((String) jsonFields.get(DdpMessageField.ERRORMSG));
                }
            }
        });
    }

    /**
     * BB-MOD
     * searches if all subscriptions have sent the "ready" messages
     * if( changeType.equals(DDPClient.DdpMessageType.READY) && (ddp.areAllSubscriptionsReady() == true) ) // everything is ready
     * @return boolean - true = all subscriptions are Ready - false = some subscriptions aren't ready
     */
    public boolean areAllSubscriptionsReady() {
        // if there is one which is not ready the contains value returns true, so we reverse it with !
        boolean allReady = !(subscriptionsAreReady.containsValue(false));
        //Log.v("DDPStateSingleton", "areAllSubscriptionsReady: " + allReady);
        return allReady;
    }

    /**
     * Used to notify event system of connection events.
     * Default behavior uses Android's LocalBroadcastManager.
     * Override if you want to use a different eventbus.
     * @param ddpstate current DDP state
     */
    public void broadcastConnectionState(DDPSTATE ddpstate) {
        if (TRACE)
            Log.i(TAG, "broadcastConnectionState  ddpstate: " + ddpstate + " - ddpstate.ordinal(): "
                    + ddpstate.ordinal());
        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(MESSAGE_CONNECTION);
        broadcastIntent.putExtra(MESSAGE_EXTRA_STATE, ddpstate.ordinal());
        broadcastIntent.putExtra(MESSAGE_EXTRA_USERID, mUserId);
        broadcastIntent.putExtra(MESSAGE_EXTRA_USERTOKEN, mResumeToken);
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcastIntent);
    }

    /**
     * Used to notify event system of error events.
     * Default behavior uses Android's LocalBroadcastManager.
     * Override if you want to use a different eventbus.
     * @param errorMsg error message
     */
    public void broadcastDDPError(String errorMsg) {
        if (TRACE)
            Log.v("DDPStateSingleton", "broadcastDDPError - " + "errorMsg = [" + errorMsg + "]");
        // let core know there was an error subscribing to a collection
        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(MESSAGE_ERROR);
        broadcastIntent.putExtra(MESSAGE_EXTRA_MSG, errorMsg);
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcastIntent);
    }

    /**
     * Used to notify event system of subscription change events.
     * Default behavior uses Android's LocalBroadcastManager.
     * Override if you want to use a different eventbus.
     * @param subscriptionName subscription name (this can be different from collection name)
     * @param changetype "change", "add" or "remove"
     * @param docId document ID of change/remove or null if add
     */
    public void broadcastSubscriptionChanged(String subscriptionName, String changetype, String docId) {
        if (TRACE)
            Log.v("DDPStateSingleton", "broadcastSubscriptionChanged - " + "subscriptionName = [" + subscriptionName
                    + "], changetype = [" + changetype + "], docId = [" + docId + "]");
        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(MESSAGE_SUBUPDATED);
        broadcastIntent.putExtra(MESSAGE_EXTRA_SUBNAME, subscriptionName);
        broadcastIntent.putExtra(MESSAGE_EXTRA_CHANGETYPE, changetype);
        broadcastIntent.putExtra(MESSAGE_EXTRA_CHANGEID, docId);
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcastIntent);
    }

    /**
     * handles callbacks from DDP client websocket callbacks
     */
    @SuppressWarnings("unchecked")
    @Override
    public void update(Observable client, Object msg) {
        // Show clearly in the console a new cycle of update from the server
        if (TRACE) {
            //Log.v("DDPStateSingleton", "-----------------------------------");
            //Log.v("DDPStateSingleton", "          new DDP update           ");
            //Log.v("DDPStateSingleton", "-----------------------------------");
            Log.i("DDPStateSingleton", "--------- DDP UPDATE - " + /* "client = [" + client + "],*/ "msg = " + msg);
        }

        // handle subscription updates and other messages that aren't associated with a specific command
        if (msg instanceof Map<?, ?>) {
            Map<String, Object> jsonFields = (Map<String, Object>) msg;
            // handle msg types for DDP server->client msgs:
            // https://github.com/meteor/meteor/blob/master/packages/livedata/DDP.md
            String msgtype = (String) jsonFields.get(DDPClient.DdpMessageField.MSG);
            String collName = (String) jsonFields.get(DdpMessageField.COLLECTION);
            String docId = (String) jsonFields.get(DdpMessageField.ID);
            Log.i("DDPStateSingleton",
                    "update - msgtype: " + msgtype + "   collName: " + collName + "  docId: " + docId);

            if (msgtype == null) {
                // ignore {"server_id":"GqrKrbcSeDfTYDkzQ"} web socket msgs
                return;

            } else if (msgtype.equals(DdpMessageType.ERROR)) {
                //Log.w("DDPStateSingleton", "update - ERROR" );
                broadcastDDPError((String) jsonFields.get(DdpMessageField.ERRORMSG));

            } else if (msgtype.equals(DdpMessageType.CONNECTED)) {
                Log.v("DDPStateSingleton", "update - CONNECTED mDDPState:" + mDDPState);
                mDDPState = DDPSTATE.Connected; //BB-MOD was commented but is currently set to NotLoggedIn
                broadcastConnectionState(mDDPState);

            } else if (msgtype.equals(DdpMessageType.ADDED)) {
                //                Log.v("DDPStateSingleton", "update - ADDED" );
                //String collName = (String) jsonFields.get(DdpMessageField.COLLECTION);
                //String docId = (String) jsonFields.get(DdpMessageField.ID);
                addDoc(jsonFields, collName, docId);
                // broadcast that subscription has been updated
                broadcastSubscriptionChanged(collName, DdpMessageType.ADDED, docId);

            } else if (msgtype.equals(DdpMessageType.REMOVED)) {
                //                Log.v("DDPStateSingleton", "update - REMOVED" );
                // String collName = (String) jsonFields.get(DdpMessageField.COLLECTION);
                //String docId = (String) jsonFields.get(DdpMessageField.ID);
                if (removeDoc(collName, docId)) {
                    // broadcast that subscription has been updated
                    broadcastSubscriptionChanged(collName, DdpMessageType.REMOVED, docId);
                }

            } else if (msgtype.equals(DdpMessageType.CHANGED)) {
                //                Log.v("DDPStateSingleton", "update - CHANGED" );
                // handle document updates
                //String collName = (String) jsonFields.get(DdpMessageField.COLLECTION);
                //String docId = (String) jsonFields.get(DdpMessageField.ID);
                if (updateDoc(jsonFields, collName, docId)) {
                    // broadcast that subscription has been updated
                    broadcastSubscriptionChanged(collName, DdpMessageType.CHANGED, docId);
                }
            }
        }
    }

    /**
     * Handles updating a document in a collection.
     * Override if you want to use your own collection data store.
     * @param jsonFields fields for document
     * @param collName collection name
     * @param docId documement ID for update
     * @return true if changed; false if document not found
     */
    @SuppressWarnings("unchecked")
    public boolean updateDoc(Map<String, Object> jsonFields, String collName, String docId) {
        //        Log.v("DDPStateSingleton", "updateDoc - " + "jsonFields = [" + jsonFields + "], collName = [" + collName + "], docId = [" + docId + "]");
        if (mCollections.containsKey(collName)) {
            Map<String, Map<String, Object>> collection = mCollections.get(collName);
            Map<String, Object> doc = (Map<String, Object>) collection.get(docId);
            if (doc != null) {
                // take care of field updates
                Map<String, Object> fields = (Map<String, Object>) jsonFields.get(DdpMessageField.FIELDS);

                if (fields != null) {
                    for (Map.Entry<String, Object> field : fields.entrySet()) {
                        String fieldname = field.getKey();
                        doc.put(fieldname, field.getValue());
                    }
                }
                // take care of clearing fields
                List<String> clearfields = ((List<String>) jsonFields.get(DdpMessageField.CLEARED));
                if (clearfields != null) {
                    for (String fieldname : clearfields) {
                        if (doc.containsKey(fieldname)) {
                            doc.remove(fieldname);
                        }
                    }
                }
                return true;
            }
        } else {
            Log.w(TAG, "Received invalid changed msg for collection " + collName);
        }
        return false;
    }

    /**
     * Handles deleting a document in a collection.
     * Override if you want to use your own collection data store.
     * @param collName collection name
     * @param docId document ID
     * @return true if doc was deleted, false otherwise
     */
    public boolean removeDoc(String collName, String docId) {
        //        Log.v("DDPStateSingleton", "removeDoc - " + "collName = [" + collName + "], docId = [" + docId + "]");
        if (mCollections.containsKey(collName)) {
            // remove IDs from collection
            Map<String, Map<String, Object>> collection = mCollections.get(collName);
            //Log.v(TAG, "Removed doc: " + docId);
            collection.remove(docId);
            return true;
        } else {
            //Log.w(TAG, "Received invalid removed msg for collection " + collName);
            return false;
        }
    }

    /**
     * Handles adding a document to collection.
     * Override if you want to use your own collection data store.
     * @param jsonFields fields for document
     * @param collName collection name
     * @param docId document ID
     */
    @SuppressWarnings("unchecked")
    public void addDoc(Map<String, Object> jsonFields, String collName, String docId) {
        //        Log.v("DDPStateSingleton", "addDoc - " + "jsonFields = [" + jsonFields + "], collName = [" + collName + "], docId = [" + docId + "]");
        if (!mCollections.containsKey(collName)) {
            // add new collection
            //Log.v(TAG, "Added collection " + collName);
            mCollections.put(collName, new ConcurrentHashMap<String, Map<String, Object>>());
        }
        Map<String, Map<String, Object>> collection = mCollections.get(collName);
        collection.put(docId, (Map<String, Object>) jsonFields.get(DdpMessageField.FIELDS));
        //Log.v(TAG, "Added docid " + docId + " to collection " + collName);
    }

    /**
     * Gets local Meteor collection
     * @param collectionName collection name
     * @return collection as a Map or null if not found
     */
    public Map<String, Map<String, Object>> getCollection(String collectionName) {
        // return specified collection Map which is indexed by document ID
        //Log.v("DDPStateSingleton", "getCollection - " + "collectionName = [" + collectionName + "]  mCollections: " +mCollections + "  mCollections.get("+collectionName+"):"+mCollections.get(collectionName));
        return mCollections.get(collectionName);
    }

    /**
     * Gets a document out of a collection
     * @param collectionName collection name
     * @param docId document ID
     * @return null if not found or a collection of the document's fields
     */
    public Map<String, Object> getDocument(String collectionName, String docId) {
        //Log.v("DDPStateSingleton", "getDocument - " + "collectionName = [" + collectionName + "], docId = [" + docId + "]");
        Map<String, Map<String, Object>> docs = DDPStateSingleton.getInstance().getCollection(collectionName);
        if (docs != null) {
            return docs.get(docId);
        }
        return null;
    }

    /**
     * Gets current Meteor user ID
     * @return user ID or null if not logged in
     */
    public String getUserId() {
        //Log.v("DDPStateSingleton", "getUserId mUserId: " + mUserId);
        return mUserId;
    }

    /**
     * Gets email address for user
     * @param userId user ID
     * @return email address for that user (lookup via users collection)
     */
    @SuppressWarnings("unchecked")
    public String getUserEmail(String userId) {
        //Log.v("DDPStateSingleton", "getUserEmail - " + "userId = [" + userId + "]");
        if (userId == null) {
            return null;
        }
        // map userId to email
        // NOTE: this gets convoluted if they use OAuth logins because the email
        // field is in the service!
        Map<String, Object> user = getDocument("users", userId);
        String email = userId;
        if (user != null) {
            ArrayList<Map<String, String>> emails = (ArrayList<Map<String, String>>) user.get("emails");
            if ((emails != null) && (emails.size() > 0)) {
                // get first email address
                Map<String, String> emailFields = emails.get(0);
                email = emailFields.get("address");
            }
        }
        return email;
    }

    //////////////////////////////////   BB-MOD Exposes to everybody the possibility to call a Meteor method like Meteor.call

    /**
     * Copied from DDPClient
     * Call a meteor method with the supplied parameters
     *
     * @param method name of corresponding Meteor method
     * @param params arguments to be passed to the Meteor method
     * @param resultListener DDP command listener for this method call
     *
     * TODO - example with the listener
     * MyDDP.getInstance().meteorCall('MeteorServerMethodName', new Object[]{param1, param2,...})
     */
    public int meteorCall(String method, Object[] params, DDPListener resultListener) {
        //        Log.v("DDPStateSingleton", "meteorCall - " + "method = [" + method + "], params = [" + params + "], resultListener = [" + resultListener + "]");
        return getDDP().call(method, params, resultListener);
    }

    public int meteorCall(String method, Object[] params) {
        return meteorCall(method, params, null);
    }

}