package org.thoughtcrime.securesms.database; import android.annotation.SuppressLint; 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.SQLiteOpenHelper; import android.graphics.Bitmap; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; public class GroupDatabase extends Database { public static final String DATABASE_UPDATE_ACTION = "org.thoughtcrime.securesms.database.GroupDatabase.UPDATE"; private static final String TAG = GroupDatabase.class.getSimpleName(); private static final String TABLE_NAME = "groups"; private static final String ID = "_id"; private static final String GROUP_ID = "group_id"; private static final String TITLE = "title"; private static final String MEMBERS = "members"; private static final String AVATAR = "avatar"; private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; private static final String AVATAR_RELAY = "avatar_relay"; private static final String TIMESTAMP = "timestamp"; private static final String ACTIVE = "active"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + GROUP_ID + " TEXT, " + TITLE + " TEXT, " + MEMBERS + " TEXT, " + AVATAR + " BLOB, " + AVATAR_ID + " INTEGER, " + AVATAR_KEY + " BLOB, " + AVATAR_CONTENT_TYPE + " TEXT, " + AVATAR_RELAY + " TEXT, " + TIMESTAMP + " INTEGER, " + ACTIVE + " INTEGER DEFAULT 1);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", }; public GroupDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); } public @Nullable GroupRecord getGroup(byte[] groupId) { @SuppressLint("Recycle") Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}, null, null, null); Reader reader = new Reader(cursor); GroupRecord record = reader.getNext(); reader.close(); return record; } public Reader getGroupsFilteredByTitle(String constraint) { Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, TITLE + " LIKE ?", new String[]{"%" + constraint + "%"}, null, null, null); return new Reader(cursor); } public Reader getGroups() { Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null); return new Reader(cursor); } public @NonNull Recipients getGroupMembers(byte[] groupId, boolean includeSelf) { String localNumber = TextSecurePreferences.getLocalNumber(context); List members = getCurrentMembers(groupId); List recipients = new LinkedList<>(); for (String member : members) { if (!includeSelf && member.equals(localNumber)) continue; recipients.addAll(RecipientFactory.getRecipientsFromString(context, member, false) .getRecipientsList()); } return RecipientFactory.getRecipientsFor(context, recipients, false); } public void create(byte[] groupId, String title, List members, TextSecureAttachmentPointer avatar, String relay) { ContentValues contentValues = new ContentValues(); contentValues.put(GROUP_ID, GroupUtil.getEncodedId(groupId)); contentValues.put(TITLE, title); contentValues.put(MEMBERS, Util.join(members, ",")); if (avatar != null) { contentValues.put(AVATAR_ID, avatar.getId()); contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); } contentValues.put(AVATAR_RELAY, relay); contentValues.put(TIMESTAMP, System.currentTimeMillis()); contentValues.put(ACTIVE, 1); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); } public void update(byte[] groupId, String title, TextSecureAttachmentPointer avatar) { ContentValues contentValues = new ContentValues(); if (title != null) contentValues.put(TITLE, title); if (avatar != null) { contentValues.put(AVATAR_ID, avatar.getId()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_KEY, avatar.getKey()); } databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}); RecipientFactory.clearCache(); notifyDatabaseListeners(); } public void updateTitle(byte[] groupId, String title) { ContentValues contentValues = new ContentValues(); contentValues.put(TITLE, title); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}); RecipientFactory.clearCache(); notifyDatabaseListeners(); } public void updateAvatar(byte[] groupId, Bitmap avatar) { updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); } public void updateAvatar(byte[] groupId, byte[] avatar) { ContentValues contentValues = new ContentValues(); contentValues.put(AVATAR, avatar); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(groupId)}); RecipientFactory.clearCache(); notifyDatabaseListeners(); } public void updateMembers(byte[] id, List members) { ContentValues contents = new ContentValues(); contents.put(MEMBERS, Util.join(members, ",")); contents.put(ACTIVE, 1); databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}); } public void remove(byte[] id, String source) { List currentMembers = getCurrentMembers(id); currentMembers.remove(source); ContentValues contents = new ContentValues(); contents.put(MEMBERS, Util.join(currentMembers, ",")); databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}); } private List getCurrentMembers(byte[] id) { Cursor cursor = null; try { cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS}, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}, null, null, null); if (cursor != null && cursor.moveToFirst()) { return Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ","); } return new LinkedList<>(); } finally { if (cursor != null) cursor.close(); } } public boolean isActive(byte[] id) { GroupRecord record = getGroup(id); return record != null && record.isActive(); } public void setActive(byte[] id, boolean active) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(ACTIVE, active ? 1 : 0); database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {GroupUtil.getEncodedId(id)}); } public byte[] allocateGroupId() { try { byte[] groupId = new byte[16]; SecureRandom.getInstance("SHA1PRNG").nextBytes(groupId); return groupId; } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } private void notifyDatabaseListeners() { Intent intent = new Intent(DATABASE_UPDATE_ACTION); context.sendBroadcast(intent); } public static class Reader { private final Cursor cursor; public Reader(Cursor cursor) { this.cursor = cursor; } public @Nullable GroupRecord getNext() { if (cursor == null || !cursor.moveToNext()) { return null; } return new GroupRecord(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)), cursor.getString(cursor.getColumnIndexOrThrow(TITLE)), cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR)), cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)), cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1); } public void close() { if (this.cursor != null) this.cursor.close(); } } public static class GroupRecord { private final String id; private final String title; private final List members; private final byte[] avatar; private final long avatarId; private final byte[] avatarKey; private final String avatarContentType; private final String relay; private final boolean active; public GroupRecord(String id, String title, String members, byte[] avatar, long avatarId, byte[] avatarKey, String avatarContentType, String relay, boolean active) { this.id = id; this.title = title; this.members = Util.split(members, ","); this.avatar = avatar; this.avatarId = avatarId; this.avatarKey = avatarKey; this.avatarContentType = avatarContentType; this.relay = relay; this.active = active; } public byte[] getId() { try { return GroupUtil.getDecodedId(id); } catch (IOException ioe) { throw new AssertionError(ioe); } } public String getEncodedId() { return id; } public String getTitle() { return title; } public List getMembers() { return members; } public byte[] getAvatar() { return avatar; } public long getAvatarId() { return avatarId; } public byte[] getAvatarKey() { return avatarKey; } public String getAvatarContentType() { return avatarContentType; } public String getRelay() { return relay; } public boolean isActive() { return active; } } }