/* * Copyright 2012 The Stanford MobiSocial Laboratory * * 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 mobisocial.musubi.service; import gnu.trove.procedure.TLongProcedure; import gnu.trove.set.hash.TLongHashSet; import java.util.Arrays; import java.util.Date; import mobisocial.musubi.App; import mobisocial.musubi.R; import mobisocial.musubi.encoding.DiscardMessage; import mobisocial.musubi.encoding.IncomingMessage; import mobisocial.musubi.encoding.MessageDecoder; import mobisocial.musubi.encoding.NeedsKey; import mobisocial.musubi.encoding.ObjEncoder; import mobisocial.musubi.encoding.ObjFormat; import mobisocial.musubi.encoding.TransportDataProvider; import mobisocial.musubi.identity.IdentityProvider; import mobisocial.musubi.identity.IdentityProviderException; import mobisocial.musubi.model.MApp; import mobisocial.musubi.model.MDevice; import mobisocial.musubi.model.MEncodedMessage; import mobisocial.musubi.model.MEncryptionUserKey; import mobisocial.musubi.model.MFeed; import mobisocial.musubi.model.MFeed.FeedType; import mobisocial.musubi.model.MFeedMember; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MMyAccount; import mobisocial.musubi.model.MObject; import mobisocial.musubi.model.helpers.AppManager; import mobisocial.musubi.model.helpers.DeviceManager; import mobisocial.musubi.model.helpers.EncodedMessageManager; import mobisocial.musubi.model.helpers.FeedManager; import mobisocial.musubi.model.helpers.IdentitiesManager; import mobisocial.musubi.model.helpers.MessageTransportManager; import mobisocial.musubi.model.helpers.MyAccountManager; import mobisocial.musubi.model.helpers.ObjectManager; import mobisocial.musubi.model.helpers.UserKeyManager; import mobisocial.musubi.objects.ProfileObj; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.MusubiContentProvider.Provided; import mobisocial.musubi.provider.TestSettingsProvider; import mobisocial.musubi.util.IdentityCache; import mobisocial.musubi.util.Util; import mobisocial.socialkit.Obj; import org.javatuples.Pair; import org.json.JSONException; import org.json.JSONObject; import android.accounts.Account; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.provider.ContactsContract; import android.util.Log; /** * Scans for inbound encoded objects that need to be decoded. * * @see MusubiService * @see MessageEncodeProcessor * @see ObjPipelineProcessor */ public class MessageDecodeProcessor extends ContentObserver { private static boolean DBG = true; private final String TAG = getClass().getSimpleName(); private MessageDecoder mMessageDecoder; private final Context mContext; private final SQLiteOpenHelper mDatabaseSource; private final AppManager mAppManager; private final ObjectManager mObjectManager; private final IdentitiesManager mIdentityManager; private final EncodedMessageManager mEncodedMessageManager; private final MyAccountManager mAccountManager; private final FeedManager mFeedManager; private final DeviceManager mDeviceManager; private final KeyUpdateHandler mKeyUpdateHandler; private boolean mSynchronousKeyFetch = false; private IdentityCache mContactThumbnailCache; final IdentityProvider mIdentityProvider; HandlerThread mThread; public static MessageDecodeProcessor newInstance(Context context, SQLiteOpenHelper dbh, KeyUpdateHandler keyUpdateService, IdentityProvider identityProvider) { HandlerThread thread = new HandlerThread("MessageDecodeThread"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); return new MessageDecodeProcessor(context, dbh, thread, keyUpdateService, identityProvider); } private MessageDecodeProcessor(Context context, SQLiteOpenHelper dbh, HandlerThread thread, KeyUpdateHandler keyUpdateService, IdentityProvider identityProvider) { super(new Handler(thread.getLooper())); mThread = thread; mContext = context; mDatabaseSource = dbh; mAppManager = new AppManager(mDatabaseSource); mFeedManager = new FeedManager(mDatabaseSource); mAccountManager = new MyAccountManager(mDatabaseSource); mEncodedMessageManager = new EncodedMessageManager(mDatabaseSource); mIdentityManager = new IdentitiesManager(mDatabaseSource); mObjectManager = new ObjectManager(mDatabaseSource); mDeviceManager = new DeviceManager(mDatabaseSource); mContactThumbnailCache = App.getContactCache(context); mIdentityProvider = identityProvider; TestSettingsProvider.Settings settings = App.getTestSettings(context); if(settings != null) { mSynchronousKeyFetch = settings.mSynchronousKeyFetchInMessageEncodeDecode; } mKeyUpdateHandler = keyUpdateService; //do initialization that hits the database in the background thread new Handler(mThread.getLooper()).post(new Runnable() { @Override public void run() { long myDevice = mDeviceManager.getLocalDeviceName(); TransportDataProvider tdp = new MessageTransportManager( mDatabaseSource, mIdentityProvider.getEncryptionScheme(), mIdentityProvider.getSignatureScheme(), myDevice); mMessageDecoder = new MessageDecoder(tdp); } }); } @Override public void onChange(boolean selfChange) { if (DBG) Log.d(TAG, "MessageDecodeProcessor noticed change"); SQLiteDatabase db = mDatabaseSource.getWritableDatabase(); long[] ids = objsToDecode(); if (ids.length == 0) { return; } TLongHashSet dirtyFeeds = new TLongHashSet(); MessageDecoderProcedure decoder = new MessageDecoderProcedure(db, dirtyFeeds); for (long id : ids) { decoder.execute(id); } final ContentResolver resolver = mContext.getContentResolver(); if (decoder.mSomethingChanged) { resolver.notifyChange(MusubiService.APP_OBJ_READY, this); requestAddressBookSync(); } if (decoder.mRunProfilePush) { resolver.notifyChange(MusubiService.PROFILE_SYNC_REQUESTED, this); } if (dirtyFeeds.size() > 0) { resolver.notifyChange(MusubiContentProvider.uriForDir(Provided.FEEDS), null); dirtyFeeds.forEach(new TLongProcedure() { @Override public boolean execute(long id) { resolver.notifyChange(MusubiContentProvider.uriForItem(Provided.FEEDS, id), null); return true; } }); } } private void requestAddressBookSync() { String accountName = mContext.getString(R.string.account_name); String accountType = mContext.getString(R.string.account_type); Account account = new Account(accountName, accountType); ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle()); } boolean handleProfileUpdate(MIdentity owner, ObjFormat object) { if (!ProfileObj.TYPE.equals(object.type)) { return false; } if (object.jsonSrc == null) { Log.e(TAG, "received profile without content"); return true; } try { boolean updateRequired = false; boolean syncRequested = false; boolean redrawRequired = false; JSONObject json = new JSONObject(object.jsonSrc); // a profile request may come from someone who we have messaged // we are not on their whitelist, so they won't send us a profile // but they will send us a message asking us to send a profile to them if(json.has(ProfileObj.VERSION)) { long version = json.getLong(ProfileObj.VERSION); if (owner.receivedProfileVersion_ < version) { if (object.raw != null) { owner.musubiThumbnail_ = object.raw; mIdentityManager.updateMusubiThumbnail(owner); mContactThumbnailCache.invalidate(owner.id_); if(owner.owned_) { for(MIdentity me : mIdentityManager.getOwnedIdentities()) { //TODO: wasteful me.musubiThumbnail_ = owner.musubiThumbnail_; me.receivedProfileVersion_ = owner.receivedProfileVersion_; mIdentityManager.updateMusubiThumbnail(me); mContactThumbnailCache.invalidate(me.id_); mIdentityManager.updateIdentity(me); } } } if(json.has(ProfileObj.NAME)) { owner.musubiName_ = json.getString(ProfileObj.NAME); if(owner.owned_) { for(MIdentity me : mIdentityManager.getOwnedIdentities()) { //TODO: wasteful me.musubiName_ = owner.musubiName_; me.receivedProfileVersion_ = owner.receivedProfileVersion_; mIdentityManager.updateIdentity(me); } } } if(json.has(ProfileObj.PRINCIPAL)) { String principal = json.getString(ProfileObj.PRINCIPAL); //if they tell us their email address, etc. write it down. if(Arrays.equals(owner.principalHash_, Util.sha256(principal.getBytes()))) { owner.principal_ = principal; } } owner.receivedProfileVersion_ = version; updateRequired = true; redrawRequired = true; } } if (json.has(ProfileObj.REPLY) && json.getBoolean(ProfileObj.REPLY)) { // If identity is in the whitelist, flag as needing profile sync. if (mIdentityManager.isWhitelisted(owner) || mFeedManager.isInAllowedFeed(owner)) { owner.sentProfileVersion_ = 1; // Force an update without reply // (assumes "my profile" has been set at least once.) updateRequired = true; syncRequested = true; } } if (updateRequired) { mIdentityManager.updateIdentity(owner); } if(redrawRequired) { mContext.getContentResolver().notifyChange(MusubiService.PRIMARY_CONTENT_CHANGED, this); } if (syncRequested) { mContext.getContentResolver().notifyChange(MusubiService.PROFILE_SYNC_REQUESTED, this); } } catch (JSONException e) { Log.e(TAG, "Failed to decode profile", e); return true; } return true; } long[] objsToDecode() { return mEncodedMessageManager.getNonDecodedInboundIds(); } class ExpandMembersProcedure implements TLongProcedure { boolean mRunProfilePush = false; MFeed mFeed; MIdentity[] mPersonas; MMyAccount[] mProvisional; MMyAccount[] mWhitelist; public ExpandMembersProcedure(MMyAccount[] provisional_account, MMyAccount[] whitelist_account, MFeed feed, MIdentity[] personas) { mFeed = feed; mPersonas = personas; mProvisional = provisional_account; mWhitelist = whitelist_account; } @Override public boolean execute(long id) { mFeedManager.ensureFeedMember(mFeed.id_, id); //send a profile request if we don't have one from them yet MIdentity recipient = mIdentityManager.getIdentityForId(id); if(recipient.receivedProfileVersion_ == 0) { //we don't really want N profiles, but we may or may not be //friends, so its best to ask with any relevant identities to //maximize the chance we can know who the sender is for(MIdentity persona : mPersonas) { sendProfileRequest(persona, recipient); } } if(mFeed.accepted_) { for(int i = 0; i < mPersonas.length; ++i) { mRunProfilePush |= mFeedManager.addToWhitelistsIfNecessary(mProvisional[i], mWhitelist[i], mPersonas[i], recipient); } } return true; } } Pair<Boolean, Boolean> expandMembership(final SQLiteDatabase db, MIdentity[] personas, final MFeed feed, MIdentity[] recipients) { TLongHashSet participants = new TLongHashSet(recipients.length); for (MIdentity ident : recipients) { participants.add(ident.id_); } final String table = MFeedMember.TABLE; String[] columns = new String[] { MFeedMember.COL_IDENTITY_ID }; String selection = MFeedMember.COL_FEED_ID + " = ?"; String[] selectionArgs = new String[] { Long.toString(feed.id_) }; String groupBy = null, having = null, orderBy = null; Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy); while (c.moveToNext()) { Long dbid = c.getLong(0); participants.remove(dbid); } MMyAccount[] provisional_account = new MMyAccount[personas.length]; MMyAccount[] whitelist_account = new MMyAccount[personas.length]; for(int i = 0; i < personas.length; ++i) { provisional_account[i] = mAccountManager.getProvisionalWhitelistForIdentity(personas[i].id_); whitelist_account[i] = mAccountManager.getWhitelistForIdentity(personas[i].id_); } ExpandMembersProcedure expand = new ExpandMembersProcedure(provisional_account, whitelist_account, feed, personas); if (participants.size() > 0) { participants.forEach(expand); } return Pair.with(participants.size() > 0, expand.mRunProfilePush); } private void sendProfileRequest(MIdentity from, MIdentity ident) { Obj profile_request = ProfilePushProcessor.getProfileRequestObj(); MFeed feed = mFeedManager.createOneShotFeed(from, ident); Uri feedUri = MusubiContentProvider.uriForItem(Provided.FEEDS, feed.id_); App.getMusubi(mContext).getFeed(feedUri).postObj(profile_request); } public class MessageDecoderProcedure implements TLongProcedure { SQLiteDatabase mDB; boolean mSomethingChanged = false; TLongHashSet mDirtyFeeds; private boolean mRunProfilePush; public MessageDecoderProcedure(SQLiteDatabase db, TLongHashSet dirtyFeeds) { mDB = db; mDirtyFeeds = dirtyFeeds; } public boolean execute(long id) { // Get the encoded data for processing MEncodedMessage encoded = mEncodedMessageManager.lookupById(id); assert(encoded != null); // Decode the message IncomingMessage im = null; try { try { im = mMessageDecoder.processMessage(encoded); } catch(NeedsKey.Encryption e) { if(!mSynchronousKeyFetch) { throw e; } try { MIdentity to = mIdentityManager.getIdentityForIBHashedIdentity(e.identity_); UserKeyManager ukm = new UserKeyManager( mIdentityProvider.getEncryptionScheme(), mIdentityProvider.getSignatureScheme(), mDatabaseSource); MEncryptionUserKey suk = new MEncryptionUserKey(); suk.identityId_ = to.id_; suk.when_ = e.identity_.temporalFrame_; suk.userKey_ = mIdentityProvider.syncGetEncryptionKey(e.identity_).key_; ukm.insertEncryptionUserKey(suk); im = mMessageDecoder.processMessage(encoded); } catch (IdentityProviderException exn) { Log.i(TAG, "Failed to get a user key to decode " + encoded.id_, exn); return true; } } } catch (NeedsKey e) { Log.i(TAG, "Failed to decode obj beause a user key was required. " + encoded.id_, e); if(mKeyUpdateHandler != null) { if (DBG) Log.i(TAG, "Updating key for identity #" + e.identity_, e); mKeyUpdateHandler.requestEncryptionKey(e.identity_); } return true; } catch (DiscardMessage.Duplicate e) { //RabbitMQ does not support the "no desliver to self" routing policy. //don't log self-routed device duplicates, everything else we want to know about if(e.mFrom.deviceName_ != mDeviceManager.getLocalDeviceName()) { Log.e(TAG, "Failed to decode message", e); } mEncodedMessageManager.delete(encoded.id_); return true; } catch (DiscardMessage e) { Log.e(TAG, "Failed to decode message", e); mEncodedMessageManager.delete(encoded.id_); return true; } // Decode the app data MDevice device = im.fromDevice_; MIdentity sender = mIdentityManager.getIdentityForId(encoded.fromIdentityId_); boolean whitelisted = (sender.owned_ || sender.whitelisted_); ObjFormat obj; try { obj = ObjEncoder.decode(im.data_); } catch (DiscardMessage e) { Log.e(TAG, "Failed to decode " + im.sequenceNumber_ + " from " + im.fromDevice_, e); mEncodedMessageManager.delete(encoded.id_); return true; } // Look for profile updates, which don't require whitelisting if (handleProfileUpdate(sender, obj)) { //TODO: this may be a lame way of handling this Log.d(TAG, "Found profile update from " + sender.musubiName_); mEncodedMessageManager.delete(encoded.id_); return true; } // Handle feed details if (obj.feedType == FeedType.FIXED) { // Fixed feeds have well-known capabilities. byte[] computedCap = FeedManager.computeFixedIdentifier(im.recipients_); if (!Arrays.equals(computedCap, obj.feedCapability)) { Log.e(TAG, "Capability mismatch"); mEncodedMessageManager.delete(encoded.id_); return true; } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { mDB.beginTransactionNonExclusive(); } else { mDB.beginTransaction(); } try { MFeed feed; boolean asymmetric = false; if (obj.feedType == FeedType.ASYMMETRIC || obj.feedType == FeedType.ONE_TIME_USE) { //never create well-known broadcast feeds feed = mFeedManager.getGlobal(); asymmetric = true; } else { feed = mFeedManager.lookupFeed(obj.feedType, obj.feedCapability); } if (feed == null) { MFeed created = new MFeed(); created.capability_ = obj.feedCapability; if (created.capability_ != null) { created.shortCapability_ = Util.shortHash(created.capability_); } created.type_ = obj.feedType; created.accepted_ = whitelisted; mFeedManager.insertFeed(created); mFeedManager.ensureFeedMember(created.id_, sender.id_); for (MIdentity recipient : im.recipients_) { mFeedManager.ensureFeedMember(created.id_, recipient.id_); //send a profile request if we don't have one from them yet if(recipient.receivedProfileVersion_ == 0) { //we don't really want N profiles, but we may or may not be //friends, so its best to ask with any relevant identities to //maximize the chance we can know who the sender is for(MIdentity persona : im.personas_) { sendProfileRequest(persona, recipient); } } } //if this feed is accepted, then we should send a profile to //all of the other people in it that we don't know if(created.accepted_) { for(MIdentity persona : im.personas_) { MMyAccount provisional_account = mAccountManager.getProvisionalWhitelistForIdentity(persona.id_); MMyAccount whitelist_account = mAccountManager.getWhitelistForIdentity(persona.id_); for (MIdentity recipient : im.recipients_) { mRunProfilePush |= mFeedManager.addToWhitelistsIfNecessary(provisional_account, whitelist_account, persona, recipient); } } } feed = created; } else { if (!feed.accepted_ && whitelisted && !asymmetric) { feed.accepted_ = true; mFeedManager.updateFeed(feed); mDirtyFeeds.add(feed.id_); } if (feed.type_ == FeedType.EXPANDING) { Pair<Boolean, Boolean> expanded_push = expandMembership(mDB, im.personas_, feed, im.recipients_); if (expanded_push.getValue0()) { mDirtyFeeds.add(feed.id_); } mRunProfilePush |= expanded_push.getValue1(); } } // Insert the object MObject object = new MObject(); MApp app = mAppManager.ensureApp(obj.appId); byte[] uhash = ObjEncoder.computeUniversalHash(sender, device, im.hash_); long currentTime = new Date().getTime(); object.id_ = -1; object.feedId_ = feed.id_; object.identityId_ = device.identityId_; object.deviceId_ = device.id_; object.parentId_ = null; object.appId_ = app.id_; object.timestamp_ = obj.timestamp; object.universalHash_ = uhash; object.shortUniversalHash_ = Util.shortHash(uhash); object.type_ = obj.type; object.json_ = obj.jsonSrc; object.raw_ = obj.raw; object.intKey_ = obj.intKey; object.stringKey_ = obj.stringKey; object.lastModifiedTimestamp_ = currentTime; object.encodedId_ = encoded.id_; object.deleted_ = false; object.renderable_ = false; object.processed_ = false; mObjectManager.insertObject(object); // Grant app access if (!MusubiContentProvider.isSuperApp(obj.appId)) { mFeedManager.ensureFeedApp(feed.id_, app.id_); } // Finish up encoded.processed_ = true; encoded.processedTime_ = currentTime; mEncodedMessageManager.updateEncodedMetadata(encoded); mDB.setTransactionSuccessful(); mSomethingChanged = true; } finally { mDB.endTransaction(); } return true; } } }