/* * 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.Date; import java.util.HashSet; import java.util.Set; import mobisocial.metrics.UsageMetrics; import mobisocial.musubi.App; import mobisocial.musubi.feed.iface.DbEntryHandler; import mobisocial.musubi.model.DbObjCache; import mobisocial.musubi.model.DbRelation; import mobisocial.musubi.model.MApp; import mobisocial.musubi.model.MFeed; import mobisocial.musubi.model.MFeed.FeedType; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MObject; import mobisocial.musubi.model.helpers.DatabaseManager; import mobisocial.musubi.obj.ObjHelpers; import mobisocial.musubi.obj.handler.NotificationHandler; import mobisocial.musubi.obj.handler.ProfileScanningObjHandler; import mobisocial.musubi.objects.AppObj; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.MusubiContentProvider.Provided; import mobisocial.musubi.util.Util; import mobisocial.socialkit.musubi.DbObj; import mobisocial.socialkit.musubi.Musubi; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentResolver; import android.content.ContentValues; 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.Handler; import android.os.HandlerThread; import android.util.Log; /** * Scans for messages that should be sent over the network. * @see MusubiService * @see MessageDecodeProcessor */ class ObjPipelineProcessor extends ContentObserver { private static final String TAG = "ObjPipelineProcessor"; static final long ONE_WEEK = 1000*60*60*24*7; private final Context mContext; private final DatabaseManager mDatabaseManager; private final Set<String> mPendingParentHashes; private final NotificationHandler mNotificationHandler; private final ProfileScanningObjHandler mProfileScanner; private final Musubi mMusubi; final HandlerThread mThread; public static ObjPipelineProcessor newInstance(Context context) { HandlerThread thread = new HandlerThread("ObjPipelineThread"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); return new ObjPipelineProcessor(context, thread); } private ObjPipelineProcessor(Context context, HandlerThread thread) { super(new Handler(thread.getLooper())); mThread = thread; mContext = context; mMusubi = App.getMusubi(context); mDatabaseManager = new DatabaseManager(context); mPendingParentHashes = new HashSet<String>(); mNotificationHandler = new NotificationHandler(context); mProfileScanner = new ProfileScanningObjHandler(); } @Override public void onChange(boolean selfChange) { final ContentResolver resolver = mContext.getContentResolver(); SQLiteDatabase db = mDatabaseManager.getDatabase(); long[] ids = getUnprocessedObjs(); TLongHashSet feedsToNotify = new TLongHashSet(ids.length); for (long id : ids) { boolean setParent = false; long accessTime = new Date().getTime(); MObject object = mDatabaseManager.getObjectManager().getObjectForId(id); //object can be null, because the delete obj might take it away from us if(object == null) continue; try { MIdentity sender = mDatabaseManager.getIdentitiesManager().getIdentityForId(object.identityId_); boolean keepObject = true; assert(object != null); assert(object.universalHash_ != null); assert(!object.processed_); assert(Util.shortHash(object.universalHash_) == object.shortUniversalHash_); JSONObject json = null; if (object.json_ != null) { try { json = new JSONObject(object.json_); } catch (JSONException e) { Log.e(TAG, "Ejecting object with bad json " + object.json_); mDatabaseManager.getObjectManager().delete(object.id_); continue; } if (json.has(ObjHelpers.TARGET_HASH)) { String relation = json.optString(ObjHelpers.TARGET_RELATION); boolean isParent = (relation == null || relation.equals(DbRelation.RELATION_PARENT)); String hashString = json.getString(ObjHelpers.TARGET_HASH); if (isParent && hashString != null && hashString.length() > 12) { byte[] hash; try { hash = Util.convertToByteArray(hashString); long parentId = mDatabaseManager.getObjectManager().getObjectIdForHash(hash); if (parentId == -1) { Log.w(TAG, "no parent hash for " + hashString); if (object.lastModifiedTimestamp_ < System.currentTimeMillis() - ONE_WEEK) { Log.e(TAG, "removing old object with no parent: " + object.json_); mDatabaseManager.getObjectManager().delete(object.id_); continue; } mPendingParentHashes.add(hashString); continue; } setParent = true; object.parentId_ = parentId; } catch (Exception e) { Log.e(TAG, "bad parent hash " + hashString); } } } if (json.has(AppObj.APP_NAME) || json.has(AppObj.ANDROID_PACKAGE_NAME)) { try { MApp app = mDatabaseManager.getAppManager().lookupApp(object.appId_); boolean appUpdated = false; if (json.has(AppObj.APP_NAME)) { String name = json.getString(AppObj.APP_NAME); if (!name.equals(app.name_)) { app.name_ = name; appUpdated = true; } } if (json.has(AppObj.ANDROID_PACKAGE_NAME)) { String pkg = json.getString(AppObj.ANDROID_PACKAGE_NAME); if (!pkg.equals(app.androidPackage_)) { app.androidPackage_ = pkg; appUpdated = true; } } if (appUpdated) { mDatabaseManager.getAppManager().updateApp(app); } } catch (JSONException e) { Log.e(TAG, "error extracting from json", e); } } } DbObj obj; try { obj = getDbObj(object, json); } catch (JSONException e) { Log.e(TAG, "Ejecting obj with bad json " + object.json_); mDatabaseManager.getObjectManager().delete(object.id_); continue; } mProfileScanner.handleObjFromNetwork(mContext, obj); DbEntryHandler helper = ObjHelpers.forType(object.type_); db.beginTransaction(); try { MFeed feed = mDatabaseManager.getFeedManager().lookupFeed(object.feedId_); if (helper.isRenderable(obj)) { object.renderable_ = true; feed.latestRenderableObjId_ = object.id_; feed.latestRenderableObjTime_ = accessTime; // local, not remote Uri feedUri = MusubiContentProvider.uriForItem(Provided.FEEDS, feed.id_); boolean viewingFeed = feedUri.equals(App.getCurrentFeed()); if (!sender.owned_ && !viewingFeed) { feed.numUnread_++; } feedsToNotify.add(feed.id_); } keepObject = helper.processObject(mContext, feed, sender, object); object.processed_ = true; if (setParent && object.renderable_) { cacheParentId(db, object, object.parentId_); updateParentLastModified(db, object.parentId_); } if (feed.type_ == FeedType.ONE_TIME_USE) { mDatabaseManager.getFeedManager().deleteFeedAndMembers(feed); } else { mDatabaseManager.getFeedManager().updateFeed(feed); } if (keepObject) { mDatabaseManager.getObjectManager().updateObjectPipelineMetadata(object); } else { mDatabaseManager.getObjectManager().delete(object.id_); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } // If this object has pending children, notify this processor: String hashString = Util.convertToHex(object.universalHash_); if (mPendingParentHashes.contains(hashString)) { mPendingParentHashes.remove(hashString); resolver.notifyChange(MusubiService.APP_OBJ_READY, this); } resolver.notifyChange(MusubiContentProvider.uriForItem(Provided.OBJECTS, id), this); mNotificationHandler.handle(helper, sender.owned_, obj); } catch(Exception e) { Log.e(TAG, "Error processing object " + object.id_ + ": " + object.type_, e); mDatabaseManager.getObjectManager().delete(object.id_); UsageMetrics.getUsageMetrics(mContext).report(e); } } if (feedsToNotify.size() > 0) { resolver.notifyChange(MusubiContentProvider.uriForDir(Provided.FEEDS), this); feedsToNotify.forEach(new TLongProcedure() { @Override public boolean execute(long id) { resolver.notifyChange(MusubiContentProvider.uriForItem(Provided.FEEDS, id), ObjPipelineProcessor.this); return true; } }); } } DbObj getDbObj(MObject object, JSONObject json) throws JSONException { String appId = mDatabaseManager.getAppManager().getAppIdentifier(object.appId_); String type = object.type_; String stringKey = object.stringKey_; long localId = object.id_; byte[] hash = object.universalHash_; byte[] raw = object.raw_; long senderId = object.identityId_; long feedId = object.feedId_; Integer intKey = object.intKey_; long timestamp = object.timestamp_; Long parentId = object.parentId_; return new DbObj(mMusubi, appId, feedId, parentId, senderId, localId, type, json, raw, intKey, stringKey, timestamp, hash); } // Visible for testing long[] getUnprocessedObjs() { String table = MObject.TABLE; String[] columns = new String[] { MObject.COL_ID }; String selection = MObject.COL_ENCODED_ID + " IS NOT NULL AND " + MObject.COL_PROCESSED + " = 0"; String[] selectionArgs = null; String groupBy = null, having = null, orderBy = null; Cursor c = mDatabaseManager.getDatabase().query( table, columns, selection, selectionArgs, groupBy, having, orderBy); int i = 0; long[] ids = new long[c.getCount()]; while (c.moveToNext()) { ids[i++] = c.getLong(0); } try { return ids; } finally { c.close(); } } void cacheParentId(SQLiteDatabase db, MObject child, long parentId) { ContentValues values = new ContentValues(); values.put(DbObjCache.PARENT_OBJ, parentId); values.put(DbObjCache.LATEST_OBJ, child.id_); MObject other = getCachedLatest(parentId); if (other != null) { if (child.intKey_ == null || other.intKey_ == null || child.intKey_ > other.intKey_) { String whereClause = DbObjCache.PARENT_OBJ + " = ?"; String[] whereArgs = new String[] { Long.toString(parentId) }; db.update(DbObjCache.TABLE, values, whereClause, whereArgs); } } else { db.insert(DbObjCache.TABLE, null, values); } } private MObject getCachedLatest(long parentId) { SQLiteOpenHelper h = App.getDatabaseSource(mContext); Cursor c = null; String[] columns = new String[] { DbObjCache.LATEST_OBJ }; String selection = DbObjCache.PARENT_OBJ + " = ?"; String[] selectionArgs = new String[] { Long.toString(parentId) }; String groupBy = null; String having = null; String orderBy = null; c = h.getWritableDatabase().query(DbObjCache.TABLE, columns, selection, selectionArgs, groupBy, having, orderBy); try { if (c.moveToFirst()) { long id = c.getLong(0); return mDatabaseManager.getObjectManager().getObjectForId(id); } } finally { c.close(); } return null; } /** * Update the parent's last modified timestamp */ void updateParentLastModified(SQLiteDatabase db, long id) { ContentValues cv = new ContentValues(); cv.put(MObject.COL_LAST_MODIFIED_TIMESTAMP, new Date().getTime()); String whereClause = MObject.COL_ID + "=?"; String[] whereArgs = new String[] { Long.toString(id) }; db.update(MObject.TABLE, cv, whereClause, whereArgs); } }