/*
* 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.model.helpers;
import gnu.trove.list.linked.TLongLinkedList;
import gnu.trove.procedure.TLongProcedure;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.musubi.App;
import mobisocial.musubi.model.MFeed;
import mobisocial.musubi.model.MFeed.FeedType;
import mobisocial.musubi.model.MFeedApp;
import mobisocial.musubi.model.MFeedMember;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MMyAccount;
import mobisocial.musubi.model.MObject;
import mobisocial.musubi.provider.MusubiContentProvider;
import mobisocial.musubi.provider.MusubiContentProvider.Provided;
import mobisocial.musubi.ui.FeedPannerActivity;
import mobisocial.musubi.util.Util;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.util.Log;
/**
* Manages a feed with access beyond what is allowable in SocialKit.
*
* Note that none of these methods manage whitelists. The expectation is that
* higher level code less frequently handles updating them. The whitelist
* itself is a feed and we don't really want a circular dependency.
* @see MFeed
* @see MFeedMember
*/
public final class FeedManager extends ManagerBase {
static final String TAG = "FeedType";
// A feed display is really a view over a query of objects
public static final String MIME_TYPE = MusubiContentProvider.getType(Provided.FEEDS_ID);
private SecureRandom mSecureRandom;
private IdentitiesManager mIdentitiesManager;
private SQLiteStatement mSqlUpdateFeed;
private SQLiteStatement mSqlEnsureMember;
private SQLiteStatement mSqlEnsureApp;
private SQLiteStatement mSqlQueryAllowed;
private SQLiteStatement mSqlDeleteFeedMember;
private SQLiteStatement mSqlDeleteFeedApp;
private SQLiteStatement mSqlMembersForObject;
private SQLiteStatement mSqlGetUnreadMessageCount;
private String mSqlUnacceptedFeedsWithMember;
String mSqlThumbnailForFeed;
static String[] STANDARD_FIELDS = new String[] {
MFeed.COL_ID,
MFeed.COL_TYPE,
MFeed.COL_CAPABILITY,
MFeed.COL_SHORT_CAPABILITY,
MFeed.COL_LATEST_RENDERABLE_OBJ_ID,
MFeed.COL_LATEST_RENDERABLE_OBJ_TIME,
MFeed.COL_NUM_UNREAD,
MFeed.COL_NAME,
MFeed.COL_ACCEPTED
};
final int _id = 0;
final int type = 1;
final int capability = 2;
final int shortCapability = 3;
final int latestRenderableObjId = 4;
final int latestRenderableObjTime = 5;
final int numUnread = 6;
final int name = 7;
final int accepted = 8;
private SQLiteStatement mSqlCheckMembership;
public FeedManager(SQLiteOpenHelper databaseSource) {
super(databaseSource);
mSecureRandom = new SecureRandom();
}
public FeedManager(SQLiteDatabase db) {
super(db);
}
private SecureRandom getSecureRandom() {
if (mSecureRandom == null) {
mSecureRandom = new SecureRandom();
}
return mSecureRandom;
}
private IdentitiesManager getIdentitiesManager() {
if (mIdentitiesManager == null) {
mIdentitiesManager = new IdentitiesManager(initializeDatabase());
}
return mIdentitiesManager;
}
public static Intent getViewingIntent(Context context, Uri feedUri) {
Intent launch = new Intent(Intent.ACTION_VIEW);
launch.setDataAndType(feedUri, MIME_TYPE);
launch.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
launch.setClass(context, FeedPannerActivity.class);
return launch;
}
/**
* Returns a stable byte array for the given list of identities.
* The identifier is constant regardless of the order of the identities and
* is derived from the identifier's principalHash.
*/
public static byte[] computeFixedIdentifier(MIdentity... identities) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("your platform does not support sha256", e);
}
Comparator<MIdentity> comparator = new Comparator<MIdentity>() {
@Override
public int compare(MIdentity lhs, MIdentity rhs) {
if(lhs.type_.ordinal() < rhs.type_.ordinal())
return -1;
if(lhs.type_.ordinal() > rhs.type_.ordinal())
return 1;
ByteBuffer buffer2 = ByteBuffer.wrap(rhs.principalHash_);
return ByteBuffer.wrap(lhs.principalHash_).compareTo(buffer2);
}
};
Arrays.sort(identities, comparator);
byte lastType = 0x0;
byte[] lastHash = new byte[0];
for (MIdentity identity : identities) {
byte type = (byte)identity.type_.ordinal();
byte[] hash = identity.principalHash_;
if (type == lastType && Arrays.equals(hash, lastHash)) {
continue;
}
md.update(type);
md.update(hash);
lastType = type;
lastHash = hash;
}
return md.digest();
}
public MFeed lookupFeed(FeedType type, byte[] capability) {
long shortCapability = Util.shortHash(capability);
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
String[] columns = new String[] { MFeed.COL_ID };
String selection = MFeed.COL_TYPE + " = ? and " + MFeed.COL_SHORT_CAPABILITY + " = ?";
String[] selectionArgs = new String[] {
Integer.toString(type.ordinal()), Long.toString(shortCapability) };
String groupBy = null, having = null, orderBy = null;
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
try {
while (c.moveToNext()) {
MFeed lookup = lookupFeed(c.getLong(0));
if (Arrays.equals(lookup.capability_, capability)) {
return lookup;
}
}
return null;
} finally {
c.close();
}
}
/**
* Returns a fixed feed, creating it if necessary. A fixed feed has static
* membership with a capability derived deterministically from the list of
* participants.
*
* This method creates and inserts a new fixed feed and inserts the feed
* membership.
*
*/
public MFeed getOrCreateFixedFeed(MIdentity... participants) {
if (!hasOwner(participants)) {
IdentitiesManager im = new IdentitiesManager(initializeDatabase());
MIdentity me = im.getMyDefaultIdentity(participants);
if (me == null) {
throw new IllegalArgumentException("Feed creation needs owned identity");
}
MIdentity[] us = new MIdentity[participants.length + 1];
System.arraycopy(participants, 0, us, 1, participants.length);
us[0] = me;
participants = us;
}
byte[] capability = computeFixedIdentifier(participants);
MFeed feed = lookupFeed(FeedType.FIXED, capability);
if (feed != null) {
return feed;
}
MFeed created = new MFeed();
created = new MFeed();
created.type_ = FeedType.FIXED;
created.capability_ = capability;
created.shortCapability_ = Util.shortHash(capability);
created.accepted_ = true;
return createFeedWithMembers(created, participants);
}
/**
* Creates a new feed with expandable membership with the given membership. If the feed
* does not have an owned identity, one is inserted automatically. If no owned identities
* are available, an exception is thrown.
* @param list
* @return
*/
public MFeed createExpandingFeed(MIdentity... participants) {
if (!hasOwner(participants)) {
IdentitiesManager im = new IdentitiesManager(initializeDatabase());
MIdentity me = im.getMyDefaultIdentity(participants);
if (me == null) {
throw new IllegalArgumentException("Feed creation needs owned identity");
}
MIdentity[] us = new MIdentity[participants.length + 1];
System.arraycopy(participants, 0, us, 1, participants.length);
us[0] = me;
participants = us;
}
MFeed created = new MFeed();
created = new MFeed();
created.type_ = FeedType.EXPANDING;
created.capability_ = new byte[32];
getSecureRandom().nextBytes(created.capability_);
created.shortCapability_ = Util.shortHash(created.capability_);
created.accepted_ = true;
return createFeedWithMembers(created, participants);
}
/**
* Creates a new feed with expandable membership with the given membership.
* @param list
* @return
*/
public MFeed createOneShotFeed(MIdentity... participants) {
MFeed created = new MFeed();
created = new MFeed();
created.type_ = FeedType.ONE_TIME_USE;
createFeedWithMembers(created, participants);
return created;
}
static boolean hasOwner(MIdentity[] participants) {
for (MIdentity id : participants) {
if (id.owned_) {
return true;
}
}
return false;
}
public static LinkedList<MIdentity> getOwners(MIdentity[] participants) {
LinkedList<MIdentity> owners = new LinkedList<MIdentity>();
for (MIdentity id : participants) {
if (id.owned_) {
owners.add(id);
}
}
return owners;
}
/**
* Creates a feed with the given parameters and members. If the feed
* fails insertion, the database is unchanged and feed.id_ is set to -1.
*/
MFeed createFeedWithMembers(MFeed feed, MIdentity[] participants) {
SQLiteDatabase db = initializeDatabase();
db.beginTransaction();
try {
insertFeed(feed);
if (feed.id_ == -1) {
return feed;
}
for (MIdentity id : participants) {
ensureFeedMember(feed.id_, id.id_);
}
db.setTransactionSuccessful();
return feed;
} finally {
db.endTransaction();
}
}
public void updateFeed(MFeed feed) {
SQLiteDatabase db = initializeDatabase();
if (mSqlUpdateFeed == null) {
synchronized (this) {
if(mSqlUpdateFeed == null) {
String sql = new StringBuilder()
.append("UPDATE ").append(MFeed.TABLE).append(" SET ")
.append(MFeed.COL_TYPE).append("=?,")
.append(MFeed.COL_CAPABILITY).append("=?,")
.append(MFeed.COL_SHORT_CAPABILITY).append("=?,")
.append(MFeed.COL_LATEST_RENDERABLE_OBJ_ID).append("=?,")
.append(MFeed.COL_LATEST_RENDERABLE_OBJ_TIME).append("=?,")
.append(MFeed.COL_NUM_UNREAD).append("=?,")
.append(MFeed.COL_NAME).append("=?,")
.append(MFeed.COL_ACCEPTED).append("=?")
.append(" WHERE ").append(MFeed.COL_ID).append("=?").toString();
mSqlUpdateFeed = db.compileStatement(sql);
}
}
}
synchronized (mSqlUpdateFeed) {
bindStandardFields(mSqlUpdateFeed, feed);
mSqlUpdateFeed.bindLong(STANDARD_FIELDS.length, feed.id_);
mSqlUpdateFeed.execute();
}
}
public boolean updateFeedDetails(long feedId, String feedName, byte[] thumbnail) {
ContentValues cv = new ContentValues();
if (feedName != null) {
cv.put(MFeed.COL_NAME, feedName);
}
if (thumbnail != null) {
cv.put(MFeed.COL_THUMBNAIL, thumbnail);
}
String whereClause = MFeed.COL_ID + "=?";
String[] whereArgs = new String[] { Long.toString(feedId) };
return initializeDatabase().update(MFeed.TABLE, cv, whereClause, whereArgs) > 0;
}
public byte[] getFeedThumbnailForId(long feedId) {
if (mSqlThumbnailForFeed == null) {
synchronized(this) {
mSqlThumbnailForFeed = new StringBuilder(100)
.append("SELECT ").append(MFeed.COL_THUMBNAIL)
.append(" FROM ").append(MFeed.TABLE)
.append(" WHERE ").append(MFeed.COL_ID).append("=?")
.toString();
}
}
String[] selectionArgs = new String[] { Long.toString(feedId) };
Cursor c = initializeDatabase().rawQuery(mSqlThumbnailForFeed, selectionArgs);
try {
if (c.moveToFirst()) {
if (!c.isNull(0)) {
return c.getBlob(0);
}
}
return null;
} finally {
c.close();
}
}
public long insertFeed(MFeed feed) {
SQLiteDatabase db = initializeDatabase();
ContentValues values = new ContentValues();
values.put(MFeed.COL_TYPE, feed.type_.ordinal());
if (feed.capability_ != null) {
values.put(MFeed.COL_CAPABILITY, feed.capability_);
values.put(MFeed.COL_SHORT_CAPABILITY, feed.shortCapability_);
}
if (feed.latestRenderableObjId_ != null) {
values.put(MFeed.COL_LATEST_RENDERABLE_OBJ_ID, feed.latestRenderableObjId_);
}
if (feed.latestRenderableObjTime_ != null) {
values.put(MFeed.COL_LATEST_RENDERABLE_OBJ_TIME, feed.latestRenderableObjTime_);
}
values.put(MFeed.COL_NUM_UNREAD, feed.numUnread_);
if (feed.name_ != null) {
values.put(MFeed.COL_NAME, feed.name_);
}
values.put(MFeed.COL_ACCEPTED, feed.accepted_);
feed.id_ = db.insert(MFeed.TABLE, null, values);
return feed.id_;
}
/**
* Sets a feed's unread message count to 0
* @return true if the database was updated.
*/
public boolean resetUnreadMessageCount(Uri feedUri) {
try {
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
ContentValues values = new ContentValues();
values.put(MFeed.COL_NUM_UNREAD, 0);
String whereClause = MFeed.COL_ID + " = ? AND " + MFeed.COL_NUM_UNREAD + " > 0";
String[] whereArgs = new String[] {
feedUri.getLastPathSegment()
};
return (db.update(table, values, whereClause, whereArgs) > 0);
} catch (Exception e) {
Log.e(App.TAG, "Error clearing unread messages", e);
return false;
}
}
public int getUnreadMessageCount(long feedId) {
SQLiteDatabase db = initializeDatabase();
if (mSqlGetUnreadMessageCount == null) {
synchronized (this) {
if (mSqlGetUnreadMessageCount == null) {
StringBuilder sql = new StringBuilder(50)
.append("SELECT ").append(MFeed.COL_NUM_UNREAD)
.append(" FROM ").append(MFeed.TABLE)
.append(" WHERE ").append(MFeed.COL_ID).append("=?");
mSqlGetUnreadMessageCount = db.compileStatement(sql.toString());
}
}
}
synchronized (mSqlGetUnreadMessageCount) {
mSqlGetUnreadMessageCount.bindLong(1, feedId);
try {
return (int)mSqlGetUnreadMessageCount.simpleQueryForLong();
} catch (SQLiteDoneException e) {
return 0;
}
}
}
/**
* Increments a feed's unread message count by 1.
*/
public void incrementUnreadMessageCount(Context context, long feedId) {
try {
SQLiteDatabase db = initializeDatabase();
// Can't do db.update() with a computed column.
String sql = new StringBuilder("UPDATE ").append(MFeed.TABLE).append(" SET ")
.append(MFeed.COL_NUM_UNREAD).append(" = ").append(MFeed.COL_NUM_UNREAD)
.append("+1 WHERE ").append(MFeed.COL_ID).append(" = ").append(feedId)
.toString();
db.rawQuery(sql, null);
context.getContentResolver().notifyChange(
MusubiContentProvider.uriForDir(Provided.FEEDS), null);
} catch (Exception e) {
Log.e(App.TAG, "Error clearing unread messages", e);
}
}
public boolean isLatestFeedNameSuggestion(MObject obj) {
SQLiteDatabase db = initializeDatabase();
String table = MObject.TABLE;
String[] columns = new String[] {MObject.COL_ID};
String selection = MObject.COL_FEED_ID + " = ? AND " + MObject.COL_LAST_MODIFIED_TIMESTAMP + " > ?";
String[] selectionArgs = new String[] { Long.toString(obj.feedId_), Long.toString(obj.lastModifiedTimestamp_) };
String groupBy = null, having = null, orderBy = null;
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
return c.getCount() == 0;
}
public MFeed lookupFeed(long id) {
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
String[] columns = STANDARD_FIELDS;
String selection = MFeed.COL_ID + " = ?";
String[] selectionArgs = new String[] { Long.toString(id) };
String groupBy = null, having = null, orderBy = null;
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
try {
if (c.moveToFirst()) {
MFeed feed = new MFeed();
feed.id_ = id;
feed.type_ = FeedType.values()[c.getInt(type)];
if (!c.isNull(capability)) {
feed.capability_ = c.getBlob(capability);
feed.shortCapability_ = c.getLong(shortCapability);
}
if (!c.isNull(latestRenderableObjId)) {
feed.latestRenderableObjId_ = c.getLong(latestRenderableObjId);
}
if (!c.isNull(latestRenderableObjTime)) {
feed.latestRenderableObjTime_ = c.getLong(latestRenderableObjTime);
}
feed.numUnread_ = c.getLong(numUnread);
feed.name_ = c.getString(name);
feed.accepted_ = c.getLong(accepted) != 0;
return feed;
} else {
return null;
}
} finally {
c.close();
}
}
/**
* Returns the _id of an identity owned by the local user in a given feed.
*/
public Long getOwnedIdentityForFeed(long feedId) {
SQLiteDatabase db = initializeDatabase();
StringBuilder sql = new StringBuilder();
sql.append("select ").append(MIdentity.TABLE).append(".").append(MIdentity.COL_ID)
.append(" from ").append(MFeedMember.TABLE).append(",").append(MIdentity.TABLE)
.append(" where ").append(MFeedMember.TABLE).append(".")
.append(MFeedMember.COL_IDENTITY_ID).append("=")
.append(MIdentity.TABLE).append(".").append(MIdentity.COL_ID).append(" and ")
.append(MIdentity.COL_OWNED).append(" = 1 and ").append(MFeedMember.TABLE)
.append(".").append(MFeedMember.COL_FEED_ID).append(" = ?");
String[] selectionArgs = new String[] { Long.toString(feedId) };
Cursor cursor = db.rawQuery(sql.toString(), selectionArgs);
try {
if (!cursor.moveToFirst()) {
return null;
}
return cursor.getLong(0);
} finally {
cursor.close();
}
}
/**
* select count(*) from feed_members where feed_id = ? and identity_id in ?
*/
int countIdentitiesInFeed(long feedId, MIdentity[] identities) {
StringBuilder members = new StringBuilder(100).append("(");
for (MIdentity id : identities) {
members.append(id.id_).append(",");
}
members.setLength(members.length() - 1);
members.append(")");
StringBuilder sql = new StringBuilder(100)
.append("SELECT count(*) FROM ").append(MFeedMember.TABLE)
.append(" WHERE ").append(MFeedMember.COL_FEED_ID).append("=?")
.append(" AND ").append(MFeedMember.COL_IDENTITY_ID).append(" in ").append(members);
String[] selectionArgs = new String[] { Long.toString(feedId) };
Cursor c = initializeDatabase().rawQuery(sql.toString(), selectionArgs);
try {
if (c.moveToFirst()) {
return c.getInt(0);
}
throw new IllegalStateException("Query failed.");
} finally {
c.close();
}
}
public void ensureFeedMember(long feed, long ident) {
SQLiteDatabase db = initializeDatabase();
if (mSqlEnsureMember == null) {
synchronized (this) {
if(mSqlEnsureMember == null) {
String sql = new StringBuilder()
.append("INSERT OR IGNORE INTO ").append(MFeedMember.TABLE).append("(")
.append(MFeedMember.COL_FEED_ID)
.append(",")
.append(MFeedMember.COL_IDENTITY_ID)
.append(") VALUES (?,?)").toString();
mSqlEnsureMember = db.compileStatement(sql);
}
}
}
synchronized (mSqlEnsureMember) {
mSqlEnsureMember.bindLong(1, feed);
mSqlEnsureMember.bindLong(2, ident);
mSqlEnsureMember.execute();
}
}
public void deleteFeedMember(long feed, long ident) {
SQLiteDatabase db = initializeDatabase();
if (mSqlDeleteFeedMember == null) {
synchronized (this) {
if(mSqlDeleteFeedMember == null) {
String sql = new StringBuilder()
.append("DELETE FROM ").append(MFeedMember.TABLE).append(" WHERE ")
.append(MFeedMember.COL_FEED_ID).append("=?").append(" AND ")
.append(MFeedMember.COL_IDENTITY_ID).append("=?").toString();
mSqlDeleteFeedMember = db.compileStatement(sql);
}
}
}
synchronized (mSqlDeleteFeedMember) {
mSqlDeleteFeedMember.bindLong(1, feed);
mSqlDeleteFeedMember.bindLong(2, ident);
mSqlDeleteFeedMember.execute();
}
}
public void ensureFeedApp(long feed, long app) {
SQLiteDatabase db = initializeDatabase();
if (mSqlEnsureApp == null) {
synchronized (this) {
if(mSqlEnsureApp == null) {
String sql = new StringBuilder()
.append("INSERT OR IGNORE INTO ").append(MFeedApp.TABLE).append("(")
.append(MFeedApp.COL_FEED_ID)
.append(",")
.append(MFeedApp.COL_APP_ID)
.append(") VALUES (?,?)").toString();
mSqlEnsureApp = db.compileStatement(sql);
}
}
}
synchronized (mSqlEnsureApp) {
mSqlEnsureApp.bindLong(1, feed);
mSqlEnsureApp.bindLong(2, app);
mSqlEnsureApp.execute();
}
}
public void deleteFeedApp(long feed, long app) {
SQLiteDatabase db = initializeDatabase();
if (mSqlDeleteFeedApp == null) {
synchronized (this) {
if(mSqlDeleteFeedApp == null) {
String sql = new StringBuilder()
.append("DELETE FROM ").append(MFeedApp.TABLE).append(" WHERE ")
.append(MFeedApp.COL_FEED_ID).append("=?").append(" AND ")
.append(MFeedApp.COL_APP_ID).append("=?").toString();
mSqlDeleteFeedApp = db.compileStatement(sql);
}
}
}
synchronized (mSqlDeleteFeedApp) {
mSqlDeleteFeedApp.bindLong(1, feed);
mSqlDeleteFeedApp.bindLong(2, app);
mSqlDeleteFeedApp.execute();
}
}
/**
* Returns a list of accepted feed id's for a given user id
*/
public long[] getFeedsForIdentityId(long identityId) {
SQLiteDatabase db = initializeDatabase();
String table = MFeedMember.TABLE + " inner join " + MFeed.TABLE + " on " +
MFeedMember.TABLE + "." + MFeedMember.COL_FEED_ID + " = " +
MFeed.TABLE + "." + MFeed.COL_ID;
String[] columns = new String[] { MFeedMember.COL_FEED_ID };
String selection = MFeedMember.COL_IDENTITY_ID + " = ? AND " + MFeed.COL_ACCEPTED + "=1";
String[] selectionArgs = new String[] { Long.toString(identityId) };
String groupBy = null, having = null;
String orderBy = MFeedMember.COL_FEED_ID + " desc";
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
long[] feedIds = new long[c.getCount()];
int i = 0;
try {
while (c.moveToNext()) {
feedIds[i++] = c.getLong(0);
}
return feedIds;
} finally {
c.close();
}
}
/**
* Returns the known members of the given feed.
*/
public MIdentity[] getFeedMembers(MFeed feed) {
long[] ids = getFeedMembers(feed.id_);
return getIdentitiesManager().getIdentitiesForIds(ids);
}
private String feedMemberIdsQuery;
private String getFeedMemberIdsQuery() {
if (feedMemberIdsQuery == null) {
feedMemberIdsQuery = new StringBuilder(80)
.append("SELECT ").append(MFeedMember.COL_IDENTITY_ID)
.append(" FROM ").append(MFeedMember.TABLE)
.append(" WHERE ").append(MFeedMember.COL_FEED_ID).append("=?").toString();
}
return feedMemberIdsQuery;
}
public long[] getFeedMembers(long feedId) {
SQLiteDatabase db = initializeDatabase();
Cursor c = db.rawQuery(getFeedMemberIdsQuery(), new String[] { Long.toString(feedId) });
long[] identityIds = new long[c.getCount()];
int i = 0;
while (c.moveToNext()) {
identityIds[i++] = c.getLong(0);
}
c.close();
return identityIds;
}
/**
* Returns the known members of the given feed.
*/
public int getFeedMemberCount(long feedId) {
SQLiteDatabase db = initializeDatabase();
String table = MFeedMember.TABLE;
String[] columns = new String[] { "count(*)" };
String selection = MFeedMember.COL_FEED_ID + " = ?";
String[] selectionArgs = new String[] { Long.toString(feedId) };
String groupBy = null, having = null, orderBy = null;
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
try {
if (c.moveToFirst()) {
return c.getInt(0);
} else {
return 0;
}
} finally {
c.close();
}
}
/**
* Returns the known members of the given feed.
*/
public MIdentity[] getFeedMembersGroupedByVisibleName(MFeed feed) {
SQLiteDatabase db = initializeDatabase();
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);
long[] identityIds = new long[c.getCount()];
int i = 0;
try {
if (c.moveToFirst()) {
do {
identityIds[i++] = c.getLong(0);
} while (c.moveToNext());
return getIdentitiesManager().getIdentitiesForIdsGroupedByVisibleName(identityIds);
} else {
return new MIdentity[0];
}
} finally {
c.close();
}
}
/**
* Returns a cursor for the known members of the given feed;
*/
public Cursor getKnownProfileFeedMembersCursor(long feedId) {
//TODO: in developer mode, show don't supress unknown users.
SQLiteDatabase db = initializeDatabase();
String[] selectionArgs = new String[] {Long.toString(feedId)};
String sql = "SELECT " + MIdentity.TABLE + "." + MIdentity.COL_ID
+ " FROM " + MFeedMember.TABLE + " JOIN " + MIdentity.TABLE + " ON "
+ MFeedMember.TABLE + "." + MFeedMember.COL_IDENTITY_ID + " = " + MIdentity.TABLE + "." + MIdentity.COL_ID
+ " WHERE "
+ MFeedMember.TABLE + "." + MFeedMember.COL_FEED_ID + " = ? AND "
+ "("
+ MIdentity.TABLE + "." + MIdentity.COL_MUSUBI_NAME + " IS NOT NULL OR "
+ MIdentity.TABLE + "." + MIdentity.COL_NAME + " IS NOT NULL OR "
+ MIdentity.TABLE + "." + MIdentity.COL_THUMBNAIL + " IS NOT NULL OR "
+ MIdentity.TABLE + "." + MIdentity.COL_MUSUBI_THUMBNAIL + " IS NOT NULL OR "
+ MIdentity.TABLE + "." + MIdentity.COL_PRINCIPAL + " IS NOT NULL"
+ ")"
+ " ORDER BY " + MIdentity.TABLE + "." + MIdentity.COL_NAME + " COLLATE NOCASE ASC";
return db.rawQuery(sql, selectionArgs);
}
/**
* Returns a cursor for the unclaimed members of the given feed;
*/
public Cursor getEmailReachableUnclaimedFeedMembersCursor(long feedId) {
//TODO: in developer mode, show don't supress unknown users.
SQLiteDatabase db = initializeDatabase();
String[] selectionArgs = new String[] {Long.toString(feedId), "0", "0", "0"};
String sql = "SELECT "
+ MIdentity.TABLE + "." + MIdentity.COL_ID
+ " FROM " + MFeedMember.TABLE + ", " + MIdentity.TABLE
+ " WHERE "
+ MFeedMember.TABLE + "." + MFeedMember.COL_FEED_ID + " = ? AND "
+ MFeedMember.TABLE + "." + MFeedMember.COL_IDENTITY_ID + " = " + MIdentity.TABLE + "." + MIdentity.COL_ID + " AND "
+ MIdentity.TABLE + "." + MIdentity.COL_CLAIMED + " = ? " + " AND "
+ MIdentity.TABLE + "." + MIdentity.COL_HAS_SENT_EMAIL + " = ? " + " AND "
+ MIdentity.TABLE + "." + MIdentity.COL_OWNED + " = ? " + " AND "
+ MIdentity.TABLE + "." + MIdentity.COL_PRINCIPAL + " IS NOT NULL "
+ " AND " + MIdentity.TABLE + "." + MIdentity.COL_TYPE + "=" + Authority.Email.ordinal()
//TODO: if we can get the an email address @ facebook for our friends on facebook, then uncomment this...
//+ " AND " + MIdentity.TABLE + "." + MIdentity.COL_TYPE + "=" + Authority.Facebook.ordinal()
+ " ORDER BY " + MIdentity.TABLE + "." + MIdentity.COL_NAME + " COLLATE NOCASE ASC";
Log.w(TAG, sql);
return db.rawQuery(sql, selectionArgs);
}
public void deleteFeedAndMembers(MFeed feed) {
SQLiteDatabase db = initializeDatabase();
String whereClause = MFeedMember.COL_FEED_ID + "=?";
String[] whereArgs = new String[] { Long.toString(feed.id_) };
db.delete(MFeedMember.TABLE, whereClause, whereArgs);
whereClause = MFeed.COL_ID + "=?";
// whereArgs same as above
db.delete(MFeed.TABLE, whereClause, whereArgs);
}
/**
* Rename a feed locally
*
public void renameFeed(MFeed feed, String name) {
try {
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
ContentValues values = new ContentValues();
values.put(MFeed.COL_NAME, name);
String whereClause = MFeed.COL_ID + " = ?";
String[] whereArgs = new String[] { Long.toString(feed.id_) };
db.update(table, values, whereClause, whereArgs);
} catch (Exception e) {
Log.e(App.TAG, "Error renaming feed", e);
}
}*/
private void bindStandardFields(SQLiteStatement statement, MFeed feed) {
statement.bindLong(type, feed.type_.ordinal());
if (feed.capability_ == null) {
statement.bindNull(capability);
statement.bindNull(shortCapability);
} else {
statement.bindBlob(capability, feed.capability_);
statement.bindLong(shortCapability, feed.shortCapability_);
}
if (feed.latestRenderableObjId_ == null) {
statement.bindNull(latestRenderableObjId);
} else {
statement.bindLong(latestRenderableObjId, feed.latestRenderableObjId_);
}
if (feed.latestRenderableObjTime_ == null) {
statement.bindNull(latestRenderableObjTime);
} else {
statement.bindLong(latestRenderableObjTime, feed.latestRenderableObjTime_);
}
statement.bindLong(numUnread, feed.numUnread_);
if (feed.name_ == null) {
statement.bindNull(name);
} else {
statement.bindString(name, feed.name_);
}
statement.bindLong(accepted, feed.accepted_ ? 1 : 0);
}
public MFeed getGlobal() {
return lookupFeed(MFeed.GLOBAL_BROADCAST_FEED_ID);
}
public MFeed getWizFeed() {
return lookupFeed(MFeed.WIZ_FEED_ID);
}
public boolean isInAllowedFeed(MIdentity owner) {
SQLiteDatabase db = initializeDatabase();
if (mSqlQueryAllowed == null) {
synchronized (this) {
StringBuilder sql = new StringBuilder(100)
.append("SELECT count(*)")
.append(" FROM ").append(MFeedMember.TABLE)
.append(" INNER JOIN ").append(MFeed.TABLE).append(" ON ")
.append(MFeedMember.TABLE).append(".").append(MFeedMember.COL_FEED_ID)
.append("=").append(MFeed.TABLE).append(".").append(MFeed.COL_ID)
.append(" WHERE ").append(MFeedMember.COL_IDENTITY_ID).append("=?")
.append(" AND " + MFeed.COL_ACCEPTED + "=1");
mSqlQueryAllowed = db.compileStatement(sql.toString());
}
}
synchronized (mSqlQueryAllowed) {
mSqlQueryAllowed.bindLong(1, owner.id_);
return mSqlQueryAllowed.simpleQueryForLong() > 0;
}
}
//TODO: this almost certainly has a race with the pipeline processor.... because
//they both update the feed and the pipeline processor doesn't hold a transaction
//the whole time
public void acceptFeedsFromMember(Context context, long identityId) {
TLongLinkedList ids = getUnacceptedFeedsWithMember(identityId);
if(ids.size() == 0)
return;
ids.forEach(new TLongProcedure() {
@Override
public boolean execute(long feedId) {
MFeed feed = lookupFeed(feedId);
feed.accepted_ = true;
feed.latestRenderableObjTime_ = new Date().getTime();
updateFeed(feed);
return true;
}
});
context.getContentResolver().notifyChange(MusubiContentProvider.uriForDir(Provided.FEEDS), null);
}
public TLongLinkedList getUnacceptedFeedsWithMember(long identityId) {
if (mSqlUnacceptedFeedsWithMember == null) {
mSqlUnacceptedFeedsWithMember = "SELECT " + MFeed.TABLE + "." + MFeed.COL_ID + " FROM " + MFeed.TABLE + " JOIN " + MObject.TABLE + " ON " +
MFeed.TABLE + "." + MFeed.COL_ID + "=" + MObject.TABLE + "." + MObject.COL_FEED_ID + " JOIN " +
MFeedMember.TABLE + " ON " + MFeed.TABLE + "." + MFeed.COL_ID + "=" + MFeedMember.TABLE + "." + MFeedMember.COL_FEED_ID +
" WHERE " + MObject.TABLE + "." + MObject.COL_IDENTITY_ID + "=? AND " + MObject.TABLE + "." + MObject.COL_IDENTITY_ID +
"=" + MFeedMember.TABLE + "." + MFeedMember.COL_IDENTITY_ID + " AND " +
MFeed.TABLE + "." + MFeed.COL_TYPE + " IN (" + MFeed.FeedType.FIXED.ordinal() + "," +
MFeed.FeedType.EXPANDING.ordinal() + ") AND " + MFeed.TABLE + "." + MFeed.COL_ACCEPTED + "=0 " +
"GROUP BY " + MFeed.TABLE + "." + MFeed.COL_ID;
}
SQLiteDatabase db = initializeDatabase();
Cursor c = db.rawQuery(mSqlUnacceptedFeedsWithMember, new String[] { String.valueOf(identityId) });
TLongLinkedList ids = new TLongLinkedList(c.getCount());
try {
while(c.moveToNext()) {
ids.add(c.getLong(0));
}
return ids;
} finally {
c.close();
}
}
public boolean isFeedMember(long feedId, long identityId) {
SQLiteDatabase db = initializeDatabase();
if (mSqlCheckMembership == null) {
synchronized (this) {
if(mSqlCheckMembership == null) {
String sql = new StringBuilder()
.append("SELECT COUNT(").append(MFeedMember.COL_ID).append(") FROM ").append(MFeedMember.TABLE).append(" WHERE ")
.append(MFeedMember.COL_FEED_ID).append("=?").append(" AND ")
.append(MFeedMember.COL_IDENTITY_ID).append("=?").toString();
mSqlCheckMembership = db.compileStatement(sql);
}
}
}
synchronized (mSqlCheckMembership) {
mSqlCheckMembership.bindLong(1, feedId);
mSqlCheckMembership.bindLong(2, identityId);
return mSqlCheckMembership.simpleQueryForLong() != 0;
}
}
public boolean addToWhitelistsIfNecessary(MMyAccount provisionalAccount, MMyAccount whitelistAccount, MIdentity persona, MIdentity recipient) {
if(recipient.blocked_) {
return false;
}
//TODO: if things are flaky or we want to work better in the face of
//clear data, then we could also reset the sentProfileVersion for the identity
//but this is probably good enough...
assert(provisionalAccount.feedId_ != null);
assert(whitelistAccount.feedId_ != null);
boolean changed = false;
//if they are not whitelisted, then they need to be provisionally
//whitelisted because this feed was accepted
if(!recipient.whitelisted_) {
if(!isFeedMember(provisionalAccount.feedId_, recipient.id_)) {
ensureFeedMember(provisionalAccount.feedId_, recipient.id_);
changed = true;
}
} else {
//but they may be whitelisted and contacting you via an identity that you did not think
//they knew you at. in which case we need to track this so that we ensure we send a profile
//to them using this specific identity.
if(!isFeedMember(whitelistAccount.feedId_, recipient.id_)) {
ensureFeedMember(whitelistAccount.feedId_, recipient.id_);
changed = true;
}
}
return changed;
}
public static final String VISIBLE_FEED_SELECTION = SQLClauseHelper.andClauses(MFeed.COL_LATEST_RENDERABLE_OBJ_ID + " is not null", MFeed.COL_ACCEPTED + "=1");
public ArrayList<Long> getFeedIdsForDisplay() {
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
String[] columns = new String[] { MFeed.COL_ID };
String selection = VISIBLE_FEED_SELECTION;
String groupBy = null, having = null;
String orderBy = MFeed.COL_LATEST_RENDERABLE_OBJ_TIME + " DESC";
Cursor c = db.query(table, columns, selection, null, groupBy, having, orderBy);
ArrayList<Long> feedIds = new ArrayList<Long>(c.getCount());
try {
while (c.moveToNext()) {
feedIds.add(c.getLong(0));
}
return feedIds;
} finally {
c.close();
}
}
public Long getCachedLatestRenderable(long feedId) {
SQLiteDatabase db = initializeDatabase();
String table = MFeed.TABLE;
String[] columns = new String[] { MFeed.COL_LATEST_RENDERABLE_OBJ_ID };
String selection = MFeed.COL_ID + "=? AND " + MFeed.COL_LATEST_RENDERABLE_OBJ_ID + " is not null";
String[] selectionArgs = new String[] { Long.toString(feedId) };
String groupBy = null, having = null, orderBy = null;
Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
try {
if (c.moveToFirst()) {
return c.getLong(0);
} else {
return null;
}
} finally {
c.close();
}
}
public static final String visibleFeedSelection(long[] feedIds) {
StringBuilder selection = new StringBuilder(80);
selection.append(MFeed.COL_LATEST_RENDERABLE_OBJ_ID).append(" is not null");
if (feedIds.length > 0) {
selection.append(" AND ").append(MFeed.TABLE).append(".").append(MFeed.COL_ID)
.append(" IN (").append(feedIds[0]);
for (int i = 1; i < feedIds.length; i++) {
selection.append(",").append(feedIds[i]);
}
selection.append(")");
} else {
selection.append(" AND 1=0"); // heh
}
selection.append(" AND ").append(MFeed.COL_ACCEPTED).append("=1");
return selection.toString();
}
public long membersInObjectFeed(long objectId) {
SQLiteDatabase db = initializeDatabase();
if (mSqlMembersForObject == null) {
synchronized (this) {
StringBuilder sql = new StringBuilder(80)
.append("SELECT count(*) from ").append(MFeedMember.TABLE)
.append(" WHERE ").append(MFeedMember.COL_FEED_ID).append("= ")
.append(" (SELECT ").append(MObject.COL_FEED_ID).append(" FROM ")
.append(MObject.TABLE).append(" WHERE ").append(MObject.COL_ID)
.append("=?)");
mSqlMembersForObject = db.compileStatement(sql.toString());
}
}
synchronized (mSqlMembersForObject) {
mSqlMembersForObject.bindLong(1, objectId);
return mSqlMembersForObject.simpleQueryForLong();
}
}
@Override
public synchronized void close() {
if (mIdentitiesManager != null) {
mIdentitiesManager.close();
mIdentitiesManager = null;
}
if (mSqlUpdateFeed != null) {
mSqlUpdateFeed.close();
mSqlUpdateFeed = null;
}
if (mSqlEnsureMember != null) {
mSqlEnsureMember.close();
mSqlEnsureMember = null;
}
if (mSqlEnsureApp != null) {
mSqlEnsureApp.close();
mSqlEnsureApp = null;
}
if (mSqlQueryAllowed != null) {
mSqlQueryAllowed.close();
mSqlQueryAllowed = null;
}
if (mSqlDeleteFeedMember != null) {
mSqlDeleteFeedMember.close();
mSqlDeleteFeedMember = null;
}
if (mSqlDeleteFeedApp != null) {
mSqlDeleteFeedApp.close();
mSqlDeleteFeedApp = null;
}
if (mSqlMembersForObject != null) {
mSqlMembersForObject.close();
mSqlMembersForObject = null;
}
if (mSqlGetUnreadMessageCount != null) {
mSqlGetUnreadMessageCount.close();
mSqlGetUnreadMessageCount = null;
}
}
}