edu.stanford.mobisocial.dungbeetle.MessagingManagerThread.java Source code

Java tutorial

Introduction

Here is the source code for edu.stanford.mobisocial.dungbeetle.MessagingManagerThread.java

Source

/*
 * Copyright (C) 2011 The Stanford MobiSocial Laboratory
 *
 * This file is part of Musubi, a mobile social network.
 *
 *  This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package edu.stanford.mobisocial.dungbeetle;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.SoftReference;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import mobisocial.socialkit.EncodedObj;
import mobisocial.socialkit.PreparedObj;
import mobisocial.socialkit.SignedObj;
import mobisocial.socialkit.User;
import mobisocial.socialkit.musubi.DbObj;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Process;
import android.util.Log;
import android.util.Pair;
import edu.stanford.mobisocial.bumblebee.ConnectionStatus;
import edu.stanford.mobisocial.bumblebee.ConnectionStatusListener;
import edu.stanford.mobisocial.bumblebee.IncomingMessage;
import edu.stanford.mobisocial.bumblebee.MessageListener;
import edu.stanford.mobisocial.bumblebee.MessengerService;
import edu.stanford.mobisocial.bumblebee.OutgoingMessage;
import edu.stanford.mobisocial.bumblebee.RabbitMQMessengerService;
import edu.stanford.mobisocial.bumblebee.StateListener;
import edu.stanford.mobisocial.bumblebee.TransportIdentityProvider;
import edu.stanford.mobisocial.dungbeetle.feed.DbObjects;
import edu.stanford.mobisocial.dungbeetle.feed.iface.DbEntryHandler;
import edu.stanford.mobisocial.dungbeetle.feed.iface.UnprocessedMessageHandler;
import edu.stanford.mobisocial.dungbeetle.feed.presence.DropMessagesPresence.MessageDropHandler;
import edu.stanford.mobisocial.dungbeetle.feed.presence.Push2TalkPresence;
import edu.stanford.mobisocial.dungbeetle.feed.presence.TVModePresence;
import edu.stanford.mobisocial.dungbeetle.model.Contact;
import edu.stanford.mobisocial.dungbeetle.model.DbObject;
import edu.stanford.mobisocial.dungbeetle.model.Feed;
import edu.stanford.mobisocial.dungbeetle.model.Subscriber;
import edu.stanford.mobisocial.dungbeetle.obj.handler.IteratorObjHandler;
import edu.stanford.mobisocial.dungbeetle.obj.handler.NotificationObjHandler;
import edu.stanford.mobisocial.dungbeetle.obj.handler.ProfileScanningObjHandler;
import edu.stanford.mobisocial.dungbeetle.util.Maybe;
import edu.stanford.mobisocial.dungbeetle.util.Util;

/**
 * The main thread for sending and receiving messages from the network.
 * Also see the Bumblebee project for code relating to network connectivity,
 * identity management, and message encryption.
 */
public class MessagingManagerThread extends Thread {
    public static final String TAG = "MessagingManagerThread";
    public static final boolean DBG = true;
    private Context mContext;
    private MessengerService mMessenger;
    private ObjectContentObserver mOco;
    private DBHelper mHelper;
    private IdentityProvider mIdent;
    private final MessageDropHandler mMessageDropHandler;

    public MessagingManagerThread(final Context context) {
        mContext = context;
        mHelper = DBHelper.getGlobal(context);
        mIdent = new DBIdentityProvider(mHelper);
        mMessageDropHandler = new MessageDropHandler();

        ConnectionStatus status = new ConnectionStatus() {
            public boolean isConnected() {
                ConnectivityManager cm = (ConnectivityManager) context
                        .getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo info = cm.getActiveNetworkInfo();
                return info != null && info.isConnected();
            }
        };
        mMessenger = new RabbitMQMessengerService(wrapIdent(mIdent), status);
        mMessenger.addStateListener(new StateListener() {
            public void onReady() {
                Log.i(TAG, "Connected to message transport!");
            }

            public void onNotReady() {
                Log.i(TAG, "Message transport not available.");
            }
        });
        mMessenger.addMessageListener(new MessageListener() {
            public void onMessage(IncomingMessage incoming) {
                if (DBG)
                    Log.i(TAG, "Got incoming message " + incoming);
                handleIncomingMessage(incoming);
            }
        });
        mMessenger.addConnectionStatusListener(new ConnectionStatusListener() {

            @Override
            public void onStatus(String msg, Exception e) {
                StringWriter err = new StringWriter();
                PrintWriter p = new PrintWriter(err);
                if (e != null) {
                    p.println(e.toString());
                    p.println(e.getMessage());
                    e.printStackTrace(p);
                }
                Log.e(TAG, "Connection Status: " + msg + "\n" + err.toString());
            }
        });

        mOco = new ObjectContentObserver(new Handler(mContext.getMainLooper()));
        mContext.getContentResolver()
                .registerContentObserver(Uri.parse(DungBeetleContentProvider.CONTENT_URI + "/feeds"), true, mOco);
        mContext.getContentResolver()
                .registerContentObserver(Uri.parse(DungBeetleContentProvider.CONTENT_URI + "/out"), true, mOco);
    }

    // FYI: Invoked on connection reader thread
    private void handleIncomingMessage(final IncomingMessage incoming) {
        final SignedObj contents = incoming.contents();
        final long hash = contents.getHash();
        if (contents.getSender() == null) {
            Log.e(TAG, "Null sender for " + contents.getType() + ", " + contents.getJson());
            return;
        }
        final String personId = contents.getSender().getId();
        // final String personId = incoming.from();

        /**
         * TODO: This needs to be updated with the POSI standards to accept a
         * SignedObj.
         */

        if (DBG)
            Log.i(TAG, "Localized contents: " + contents);
        try {
            JSONObject in_obj = contents.getJson();
            String feedName = contents.getFeedName();
            String type = contents.getType();
            Uri feedPreUri = Feed.uriForName(feedName);
            if (mMessageDropHandler.preFiltersObj(mContext, feedPreUri)) {
                return;
            }

            if (mHelper.queryAlreadyReceived(hash)) {
                if (DBG)
                    Log.i(TAG, "Message already received: " + hash);
                return;
            }

            Maybe<Contact> contact = mHelper.contactForPersonId(personId);
            final DbEntryHandler objHandler = DbObjects.getObjHandler(in_obj);
            byte[] extracted_data = null;
            if (objHandler instanceof UnprocessedMessageHandler) {
                Pair<JSONObject, byte[]> r = ((UnprocessedMessageHandler) objHandler).handleUnprocessed(mContext,
                        in_obj);
                if (r != null) {
                    in_obj = r.first;
                    extracted_data = r.second;
                }
            }
            final JSONObject obj = in_obj;
            final byte[] raw = extracted_data;

            /**
             *  TODO STFAN BJDODSON KANAKB
             *
             *  See FriendAcceptObj.handleUnprocessed as template, code is something like:
             *
             *  if (!mPublicKeyDirectory.verify(in_obj.getString("email"), in_obj.getPublicKey())) {
             *    Log.w("Spammer trying to claim public key for email address");
             *    return;
             *  }
             *  if (inAddressBook(email)) {
             *     // auto-accept and notify of new friend
             *  } else {
             *     // notification to accept friend
             *  }
             */

            if (!contact.isKnown()) {
                Log.i(TAG, "Message from unknown contact. " + contents);
                return;
            }

            /**
             * Run handlers over all received objects:
             */

            long objId;
            final Contact realContact = contact.get();
            long contactId = realContact.id;
            if (DBG)
                Log.d(TAG, "Msg from " + contactId + " ( " + realContact.name + ")");
            // Insert into the database. (TODO: Handler, both android.os and
            // musubi.core)

            if (!objHandler.handleObjFromNetwork(mContext, realContact, obj)) {
                return;
            }

            Integer intKey = null;
            if (obj.has(DbObjects.JSON_INT_KEY)) {
                intKey = obj.getInt(DbObjects.JSON_INT_KEY);
                obj.remove(DbObjects.JSON_INT_KEY);
            }
            objId = mHelper.addObjectByJson(contact.otherwise(Contact.NA()).id, obj, hash, raw, intKey);
            Uri feedUri;
            if (feedName.equals("friend")) {
                feedUri = Feed.uriForName("friend/" + contactId);
            } else {
                feedUri = Feed.uriForName(feedName);
            }
            mContext.getContentResolver().notifyChange(feedUri, null);
            if (feedName.equals("direct") || feedName.equals("friend")) {
                long time = obj.optLong(DbObject.TIMESTAMP);
                Helpers.updateLastPresence(mContext, realContact, time);
                objHandler.handleDirectMessage(mContext, realContact, obj);
            }

            /**
             * Run handlers over all received objects:
             */

            // TODO: framework code.
            DbObj signedObj = App.instance().getMusubi().objForId(objId);
            getFromNetworkHandlers().handleObj(mContext, DbObjects.forType(type), signedObj);
            objHandler.afterDbInsertion(mContext, signedObj);
        } catch (Exception e) {
            Log.e(TAG, "Error handling incoming message.", e);
        }
    }

    private IteratorObjHandler mFromNetworkHandlers;

    public IteratorObjHandler getFromNetworkHandlers() {
        if (mFromNetworkHandlers == null) {
            mFromNetworkHandlers = new IteratorObjHandler();
            mFromNetworkHandlers.addHandler(TVModePresence.getInstance());
            mFromNetworkHandlers.addHandler(Push2TalkPresence.getInstance());
            mFromNetworkHandlers.addHandler(new ProfileScanningObjHandler());
            mFromNetworkHandlers.addHandler(new NotificationObjHandler(mHelper));
        }
        return mFromNetworkHandlers;
    }

    @Override
    public void run() {
        ProfileScanningObjHandler profileScanningObjHandler = new ProfileScanningObjHandler();
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Set<Long> notSendingObjects = new HashSet<Long>();
        if (DBG)
            Log.i(TAG, "Running...");
        mMessenger.init();
        long max_sent = -1;
        while (!interrupted()) {
            mOco.waitForChange();
            mOco.clearChanged();
            Cursor objs = mHelper.queryUnsentObjects(max_sent);
            try {
                Log.i(TAG, "Sending " + objs.getCount() + " objects...");
                if (objs.moveToFirst())
                    do {
                        Long objId = objs.getLong(objs.getColumnIndexOrThrow(DbObject._ID));
                        String jsonSrc = objs.getString(objs.getColumnIndexOrThrow(DbObject.JSON));

                        max_sent = objId.longValue();
                        JSONObject json = null;
                        if (jsonSrc != null) {
                            try {
                                json = new JSONObject(jsonSrc);
                            } catch (JSONException e) {
                                Log.e(TAG, "bad json", e);
                            }
                        } else {
                            json = new JSONObject();
                        }

                        if (json != null) {
                            /*
                             * if you update latest feed here then there is a
                             * race condition between when you put a message
                             * into your db, when you actually have a connection
                             * to send the message (which is here) when other
                             * people send you messages the processing gets all
                             * out of order, so instead we update latest
                             * immediately when you add messages into your db
                             * inside DBHelper.java addToFeed();
                             */
                            // mFeedModifiedObjHandler.handleObj(mContext,
                            // feedUri, objId);

                            // TODO: Don't be fooled! This is not truly an
                            // EncodedObj
                            // and does not yet have a hash.
                            DbObj signedObj = App.instance().getMusubi().objForId(objId);
                            if (signedObj == null) {
                                Log.e(TAG, "Error, object " + objId + " not found in database");
                                notSendingObjects.add(objId);
                                continue;
                            }
                            DbEntryHandler h = DbObjects.getObjHandler(json);
                            h.afterDbInsertion(mContext, signedObj);

                            // TODO: Constraint error thrown for now b/c local
                            // user not in contacts
                            profileScanningObjHandler.handleObj(mContext, h, signedObj);
                        }

                        OutgoingMessage m = new OutgoingMsg(objs);
                        if (m.contents().getRecipients().isEmpty()) {
                            Log.w(TAG, "No addressees for direct message " + objId);
                            notSendingObjects.add(objId);
                        } else {
                            mMessenger.sendMessage(m);
                        }
                    } while (objs.moveToNext());
                if (notSendingObjects.size() > 0) {
                    if (DBG)
                        Log.d(TAG, "Marking " + notSendingObjects.size() + " objects sent");
                    mHelper.markObjectsAsSent(notSendingObjects);
                    notSendingObjects.clear();
                }
            } catch (Exception e) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
                    Log.wtf(TAG, "error running notify loop", e);
                } else {
                    Log.e(TAG, "error running notify loop", e);
                }
            } finally {
                objs.close();
            }
        }
        mHelper.close();
    }

    private List<Long> getFeedSubscribers(Uri feedUri) {
        if (feedUri == null) {
            throw new NullPointerException("Feed cannot be null");
        }
        String feedName = feedUri.getLastPathSegment();
        switch (Feed.typeOf(feedUri)) {
        case FRIEND:
            String personId = Feed.friendIdForFeed(feedUri);
            if (personId == null) {
                return new ArrayList<Long>(0);
            }
            String table = Contact.TABLE;
            String[] columns = new String[] { Contact._ID };
            String selection = Contact.PERSON_ID + " = ?";
            String[] selectionArgs = new String[] { personId };
            String groupBy = null;
            String having = null;
            String orderBy = null;
            Cursor c = mHelper.getReadableDatabase().query(table, columns, selection, selectionArgs, groupBy,
                    having, orderBy);
            if (c == null || !c.moveToFirst()) {
                Log.w(TAG, "Could not find user for id " + personId);
                return new ArrayList<Long>(0);
            }
            return Collections.singletonList(c.getLong(0));
        case APP:
            // Currently, we send app messages to all users, which are registered
            // as subscribers to the "friend" feed. The subscribers model needs to
            // be reworked, and further the "app" feed needs further thinking.
            // Messages should be lossy, and encryption should not require keys
            // for each recipient.
            feedName = "friend";
            // No break:
        case GROUP:
            Cursor subs = mHelper.querySubscribers(feedName);
            List<Long> recipientIds = new ArrayList<Long>(subs.getCount());
            subs.moveToFirst();
            while (!subs.isAfterLast()) {
                long id = subs.getLong(subs.getColumnIndexOrThrow(Subscriber.CONTACT_ID));
                recipientIds.add(id);
                subs.moveToNext();
            }
            subs.close();
            return recipientIds;
        default:
            Log.w(TAG, "unmatched feed type for " + feedUri);
            return new ArrayList<Long>();
        }
    }

    private class OutgoingMsg implements OutgoingMessage {
        protected SoftReference<EncodedObj> mEncoded;
        protected PreparedObj mBody;
        protected long mObjectId;
        protected byte[] mRaw;
        protected boolean mDeleteOnCommit;

        protected OutgoingMsg(Cursor objs) {
            mObjectId = objs.getLong(0 /* DbObject._ID */);
            DbEntryHandler objHandler = DbObjects.forType(objs.getString(2));
            mDeleteOnCommit = objHandler.discardOutboundObj();

            String feedName = objs.getString(objs.getColumnIndexOrThrow(DbObject.FEED_NAME));
            Uri feedUri = Feed.uriForName(feedName);
            String to = objs.getString(objs.getColumnIndexOrThrow(DbObject.DESTINATION));

            if (DBG)
                Log.d(TAG, "Sending to: " + feedName + ", " + to);
            List<Long> recipientIds;
            if (to != null) {
                recipientIds = Util.splitLongsToList(to, ",");
            } else {
                recipientIds = getFeedSubscribers(feedUri);
            }

            List<User> recipients = mHelper.getPKUsersForIds(recipientIds);
            String type = objs.getString(objs.getColumnIndexOrThrow(DbObject.TYPE));
            String appId = objs.getString(objs.getColumnIndexOrThrow(DbObject.APP_ID));
            String jsonSrc = objs.getString(objs.getColumnIndexOrThrow(DbObject.JSON));
            mRaw = objs.getBlob(objs.getColumnIndexOrThrow(DbObject.RAW));
            Integer intKey = null;
            int col = objs.getColumnIndexOrThrow(DbObject.KEY_INT);
            if (!objs.isNull(col)) {
                intKey = objs.getInt(col);
            }
            JSONObject json = null;
            try {
                json = new JSONObject(jsonSrc);
                User sender = App.instance().getMusubi().userForLocalDevice(feedUri);
                mBody = new DbPreparedObj(sender, recipients, appId, type, json, mRaw, intKey);
            } catch (JSONException e) {
                Log.e(TAG, "Bad json in db", e);
            }
        }

        @Override
        public long getLocalUniqueId() {
            return mObjectId;
        }

        public PreparedObj contents() {
            return mBody;
        }

        public String toString() {
            return "[Message with body: " + mBody + "]";
        }

        public void onCommitted() {
            mEncoded.clear();
            mHelper.getWritableDatabase().beginTransaction();
            try {
                mHelper.markObjectAsSent(mObjectId);
                mHelper.clearEncoded(mObjectId);
                if (mDeleteOnCommit)
                    mHelper.deleteObj(mObjectId);
                mHelper.getWritableDatabase().setTransactionSuccessful();
            } finally {
                mHelper.getWritableDatabase().endTransaction();
            }
        }

        @Override
        public void onEncoded(EncodedObj encoded) {
            mEncoded = new SoftReference<EncodedObj>(encoded);
            Log.d(TAG, "Setting encoded with hash " + encoded.getHash());
            mHelper.setEncoded(mObjectId, encoded);
            mRaw = null;
        }

        @Override
        public EncodedObj getEncoded() {
            EncodedObj cached = mEncoded != null ? mEncoded.get() : null;
            if (cached != null) {
                Log.d(TAG, "fetching memcached encoding " + cached.getHash());
                return cached;
            }
            cached = mHelper.getEncoded(mObjectId);
            mEncoded = new SoftReference<EncodedObj>(cached);
            return cached;
        }
    }

    private TransportIdentityProvider wrapIdent(final IdentityProvider ident) {
        return new TransportIdentityProvider() {
            public RSAPublicKey userPublicKey() {
                return ident.userPublicKey();
            }

            public RSAPrivateKey userPrivateKey() {
                return ident.userPrivateKey();
            }

            public String userPersonId() {
                return ident.userPersonId();
            }

            public RSAPublicKey publicKeyForPersonId(String id) {
                return ident.publicKeyForPersonId(id);
            }

            public String personIdForPublicKey(RSAPublicKey key) {
                return ident.personIdForPublicKey(key);
            }

            @Override
            public User userForPersonId(String id) {
                Uri feedUri = Feed.uriForName(Feed.FEED_NAME_GLOBAL);
                User u = App.instance().getMusubi().userForGlobalId(feedUri, id);
                return u;
            }
        };
    }

    class ObjectContentObserver extends ContentObserver {
        public boolean changed;

        public ObjectContentObserver(Handler h) {
            super(h);
            changed = true;
        }

        @Override
        public synchronized void onChange(boolean self) {
            changed = true;
            notify();
        }

        public synchronized void waitForChange() {
            if (changed)
                return;
            try {
                wait();
                changed = false;
            } catch (InterruptedException e) {
            }
        }

        public synchronized void clearChanged() {
            changed = false;
        }
    };

    static class DbPreparedObj implements PreparedObj {
        final String mType;
        final String mAppId;
        final String mFeedName;
        final JSONObject mJson;
        final List<User> mRecipients;
        final User mSender;
        final byte[] mRaw;
        final Integer mIntKey;

        public DbPreparedObj(User sender, List<User> recipients, String appId, String type, JSONObject json,
                byte[] raw, Integer intKey) {
            mFeedName = json.optString(DbObjects.FEED_NAME);
            mRecipients = recipients;
            mAppId = appId;
            mSender = sender;
            mType = type;
            mRaw = raw;
            mIntKey = intKey;
            mJson = json;
        }

        @Override
        public Integer getInt() {
            return mIntKey;
        }

        @Override
        public JSONObject getJson() {
            return mJson;
        }

        @Override
        public byte[] getRaw() {
            return mRaw;
        }

        @Override
        public String getType() {
            return mType;
        }

        @Override
        public String getAppId() {
            return mAppId;
        }

        @Override
        public User getSender() {
            return mSender;
        }

        @Override
        public List<User> getRecipients() {
            return mRecipients;
        }

        @Override
        public long getSequenceNumber() {
            try {
                return mJson.getLong(DbObjects.SEQUENCE_ID);
            } catch (Exception e) {
                return -1;
            }
        }

        @Override
        public String getFeedName() {
            return mFeedName;
        }

        @Override
        public long getTimestamp() {
            try {
                return mJson.getLong(DbObjects.TIMESTAMP);
            } catch (Exception e) {
                return -1;
            }
        }
    }
}