737810475e
This was a holdover from Signal's origins as a pure SMS app. It causes problems, depends on undefined device specific behavior, and should no longer be necessary now that we have all the information we need to E164 all numbers. // FREEBIE
368 lines
13 KiB
Java
368 lines
13 KiB
Java
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 org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
|
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.signalservice.api.messages.SignalServiceAttachmentPointer;
|
|
|
|
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 AVATAR_DIGEST = "avatar_digest";
|
|
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, " +
|
|
AVATAR_DIGEST + " BLOB);";
|
|
|
|
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 boolean isUnknownGroup(byte[] groupId) {
|
|
return getGroup(groupId) == null;
|
|
}
|
|
|
|
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) {
|
|
List<Address> members = getCurrentMembers(groupId);
|
|
List<Recipient> recipients = new LinkedList<>();
|
|
|
|
for (Address member : members) {
|
|
if (!includeSelf && Util.isOwnNumber(context, member))
|
|
continue;
|
|
|
|
recipients.add(RecipientFactory.getRecipientFor(context, member, false));
|
|
}
|
|
|
|
return RecipientFactory.getRecipientsFor(context, recipients, false);
|
|
}
|
|
|
|
public void create(byte[] groupId, String title, List<Address> members,
|
|
SignalServiceAttachmentPointer 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_DIGEST, avatar.getDigest().orNull());
|
|
}
|
|
|
|
contentValues.put(AVATAR_RELAY, relay);
|
|
contentValues.put(TIMESTAMP, System.currentTimeMillis());
|
|
contentValues.put(ACTIVE, 1);
|
|
|
|
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
|
RecipientFactory.clearCache(context);
|
|
notifyConversationListListeners();
|
|
}
|
|
|
|
public void update(byte[] groupId, String title, SignalServiceAttachmentPointer 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());
|
|
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
|
|
}
|
|
|
|
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
|
|
GROUP_ID + " = ?",
|
|
new String[] {GroupUtil.getEncodedId(groupId)});
|
|
|
|
RecipientFactory.clearCache(context);
|
|
notifyDatabaseListeners();
|
|
notifyConversationListListeners();
|
|
}
|
|
|
|
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(context);
|
|
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(context);
|
|
notifyDatabaseListeners();
|
|
}
|
|
|
|
public void updateMembers(byte[] id, List<Address> 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, Address source) {
|
|
List<Address> 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<Address> 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()) {
|
|
List<Address> results = new LinkedList<>();
|
|
|
|
for (String member : Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",")) {
|
|
results.add(Address.fromSerialized(member));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
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,
|
|
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)));
|
|
}
|
|
|
|
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<Address> members;
|
|
private final byte[] avatar;
|
|
private final long avatarId;
|
|
private final byte[] avatarKey;
|
|
private final byte[] avatarDigest;
|
|
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, byte[] avatarDigest)
|
|
{
|
|
this.id = id;
|
|
this.title = title;
|
|
this.members = Address.fromSerializedList(members, ",");
|
|
this.avatar = avatar;
|
|
this.avatarId = avatarId;
|
|
this.avatarKey = avatarKey;
|
|
this.avatarDigest = avatarDigest;
|
|
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<Address> getMembers() {
|
|
return members;
|
|
}
|
|
|
|
public byte[] getAvatar() {
|
|
return avatar;
|
|
}
|
|
|
|
public long getAvatarId() {
|
|
return avatarId;
|
|
}
|
|
|
|
public byte[] getAvatarKey() {
|
|
return avatarKey;
|
|
}
|
|
|
|
public byte[] getAvatarDigest() {
|
|
return avatarDigest;
|
|
}
|
|
|
|
public String getAvatarContentType() {
|
|
return avatarContentType;
|
|
}
|
|
|
|
public String getRelay() {
|
|
return relay;
|
|
}
|
|
|
|
public boolean isActive() {
|
|
return active;
|
|
}
|
|
}
|
|
}
|