Add option to export messages in plain text + rebuild database backend

This commit is contained in:
Arne 2025-01-10 13:35:42 +01:00
parent 2e2b13fc72
commit 61fbfed818
7 changed files with 334 additions and 246 deletions

View file

@ -132,6 +132,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static final String OCCUPANT_ID = "occupantId";
public static final String REACTIONS = "reactions";
public static final String ME_COMMAND = "/me ";
public static final String PAYLOADS = "payloads";
public static final String TIME_RECEIVED = "timeReceived";
public static final String SUBJECT = "subject";
public static final String FILE_PARAMS = "fileParams";
public static final String OCCUPANTID = "occupant_id";
public static final String NOTIFICATION_DISMISSED = "notificationDismissed";
public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
@ -288,7 +295,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
String payloadsStr = cursor.getString(cursor.getColumnIndex("payloads"));
String payloadsStr = cursor.getString(cursor.getColumnIndexOrThrow(PAYLOADS));
List<Element> payloads = new ArrayList<>();
if (payloadsStr != null) {
final XmlReader xmlReader = new XmlReader();
@ -302,40 +309,39 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
Log.e(Config.LOGTAG, "Failed to parse: " + payloadsStr, e);
}
}
Message m = new Message(conversation,
cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
cursor.getString(cursor.getColumnIndex(BODY)),
cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)),
cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)),
fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))),
fromString(cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART))),
cursor.getString(cursor.getColumnIndexOrThrow(BODY)),
cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)),
cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0,
cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0,
cursor.getString(cursor.getColumnIndexOrThrow(EDITED)),
cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0,
cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)),
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))),
cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0,
cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0,
cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)),
cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)),
Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS))),
cursor.getLong(cursor.getColumnIndex(cursor.isNull(cursor.getColumnIndex("timeReceived")) ? TIME_SENT : "timeReceived")),
cursor.getString(cursor.getColumnIndex("subject")),
cursor.getString(cursor.getColumnIndex("fileParams")),
cursor.getLong(cursor.getColumnIndexOrThrow(cursor.isNull(cursor.getColumnIndexOrThrow(TIME_RECEIVED)) ? TIME_SENT : TIME_RECEIVED)),
cursor.getString(cursor.getColumnIndexOrThrow(SUBJECT)),
cursor.getString(cursor.getColumnIndexOrThrow(FILE_PARAMS)),
payloads
);
final var legacyOccupant = cursor.getString(cursor.getColumnIndex("occupant_id"));
final var legacyOccupant = cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANTID));
if (legacyOccupant != null) m.setOccupantId(legacyOccupant);
if (cursor.getInt(cursor.getColumnIndex("notificationDismissed")) > 0) m.markNotificationDismissed();
if (cursor.getInt(cursor.getColumnIndexOrThrow(NOTIFICATION_DISMISSED)) > 0) m.markNotificationDismissed();
return m;
}
@ -365,29 +371,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return message;
}
public ContentValues getmonoclesContentValues() {
final FileParams fp = fileParams;
ContentValues values = new ContentValues();
values.put(UUID, uuid);
values.put("subject", subject);
values.put("fileParams", fp == null ? null : fp.toString());
if (fp != null && !fp.isEmpty()) {
List<Element> sims = getSims();
if (sims.isEmpty()) {
addPayload(fp.toSims());
} else {
sims.get(0).replaceChildren(fp.toSims().getChildren());
}
}
values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
values.put("occupant_id", occupantId);
values.put("timeReceived", timeReceived);
values.put("notificationDismissed", notificationDismissed ? 1 : 0);
return values;
}
@Override
public ContentValues getContentValues() {
final FileParams fp = fileParams;
final var values = new ContentValues();
values.put(UUID, uuid);
values.put(CONVERSATION, conversationUuid);
@ -425,6 +411,20 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
values.put(BODY_LANGUAGE, bodyLanguage);
values.put(OCCUPANT_ID, occupantId);
values.put(REACTIONS, Reaction.toString(this.reactions));
values.put(SUBJECT, subject);
values.put(FILE_PARAMS, fp == null ? null : fp.toString());
if (fp != null && !fp.isEmpty()) {
List<Element> sims = getSims();
if (sims.isEmpty()) {
addPayload(fp.toSims());
} else {
sims.get(0).replaceChildren(fp.toSims().getChildren());
}
}
values.put(PAYLOADS, payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
values.put(OCCUPANTID, occupantId);
values.put(TIME_RECEIVED, timeReceived);
values.put(NOTIFICATION_DISMISSED, notificationDismissed ? 1 : 0);
return values;
}

View file

@ -41,8 +41,10 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
@ -76,7 +78,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 60;
private static final int DATABASE_VERSION = 61;
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
@ -225,164 +227,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return instance;
}
protected void monoclesMigrate(SQLiteDatabase db) {
db.beginTransaction();
try {
Cursor cursor = db.rawQuery("PRAGMA monocles.user_version", null);
cursor.moveToNext();
int monoclesVersion = cursor.getInt(0);
cursor.close();
if(monoclesVersion < 1) {
// No cross-DB foreign keys unfortunately
db.execSQL(
"CREATE TABLE monocles." + Message.TABLENAME + "(" +
Message.UUID + " TEXT PRIMARY KEY, " +
"subject TEXT" +
")"
);
db.execSQL("PRAGMA monocles.user_version = 1");
}
if(monoclesVersion < 2) {
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN oobUri TEXT"
);
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN fileParams TEXT"
);
db.execSQL("PRAGMA monocles.user_version = 2");
}
if(monoclesVersion < 3) {
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN payloads TEXT"
);
db.execSQL("PRAGMA monocles.user_version = 3");
}
if(monoclesVersion < 4) {
db.execSQL(
"CREATE TABLE monocles.cids (" +
"cid TEXT NOT NULL PRIMARY KEY," +
"path TEXT NOT NULL" +
")"
);
db.execSQL("PRAGMA monocles.user_version = 4");
}
if(monoclesVersion < 5) {
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN timeReceived NUMBER"
);
db.execSQL("CREATE INDEX monocles.message_time_received_index ON " + Message.TABLENAME + " (timeReceived)");
db.execSQL("PRAGMA monocles.user_version = 5");
}
if(monoclesVersion < 6) {
db.execSQL(
"CREATE TABLE monocles.blocked_media (" +
"cid TEXT NOT NULL PRIMARY KEY" +
")"
);
db.execSQL("PRAGMA monocles.user_version = 6");
}
if(monoclesVersion < 7) {
db.execSQL(
"ALTER TABLE monocles.cids " +
"ADD COLUMN url TEXT"
);
db.execSQL("PRAGMA monocles.user_version = 7");
}
if(monoclesVersion < 8) {
db.execSQL(
"CREATE TABLE monocles.webxdc_updates (" +
"serial INTEGER PRIMARY KEY AUTOINCREMENT, " +
Message.CONVERSATION + " TEXT NOT NULL, " +
"sender TEXT NOT NULL, " +
"thread TEXT NOT NULL, " +
"threadParent TEXT, " +
"info TEXT, " +
"document TEXT, " +
"summary TEXT, " +
"payload TEXT" +
")"
);
db.execSQL("CREATE INDEX monocles.webxdc_index ON webxdc_updates (" + Message.CONVERSATION + ", thread)");
db.execSQL("PRAGMA monocles.user_version = 8");
}
if(monoclesVersion < 9) {
db.execSQL(
"ALTER TABLE monocles.webxdc_updates " +
"ADD COLUMN message_id TEXT"
);
db.execSQL("CREATE UNIQUE INDEX monocles.webxdc_message_id_index ON webxdc_updates (" + Message.CONVERSATION + ", message_id)");
db.execSQL("PRAGMA monocles.user_version = 9");
}
if(monoclesVersion < 10) {
db.execSQL(
"CREATE TABLE monocles.muted_participants (" +
"muc_jid TEXT NOT NULL, " +
"occupant_id TEXT NOT NULL, " +
"nick TEXT NOT NULL," +
"PRIMARY KEY (muc_jid, occupant_id)" +
")"
);
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN occupant_id TEXT"
);
db.execSQL("PRAGMA monocles.user_version = 10");
}
if(monoclesVersion < 11) {
if (Build.VERSION.SDK_INT >= 34) {
db.execSQL(
"ALTER TABLE monocles.muted_participants " +
"DROP COLUMN nick"
);
} else {
db.execSQL("DROP TABLE monocles.muted_participants");
db.execSQL(
"CREATE TABLE monocles.muted_participants (" +
"muc_jid TEXT NOT NULL, " +
"occupant_id TEXT NOT NULL, " +
"PRIMARY KEY (muc_jid, occupant_id)" +
")"
);
}
db.execSQL("PRAGMA monocles.user_version = 11");
}
if(monoclesVersion < 12) {
db.execSQL(
"ALTER TABLE monocles." + Message.TABLENAME + " " +
"ADD COLUMN notificationDismissed NUMBER DEFAULT 0"
);
db.execSQL("PRAGMA monocles.user_version = 12");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
db.execSQL("PRAGMA foreign_keys=ON");
db.rawQuery("PRAGMA secure_delete=ON", null).close();
db.execSQL("ATTACH DATABASE ? AS monocles", new Object[]{context.getDatabasePath("monocles").getPath()});
monoclesMigrate(db);
}
@Override
@ -777,7 +625,96 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OCCUPANT_ID + " TEXT");
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.REACTIONS + " TEXT");
}
}
if (oldVersion < 61 && newVersion >= 61) {
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " +
Message.SUBJECT + " TEXT"
);
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN oobUri TEXT"
);
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN fileParams TEXT"
);
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN payloads TEXT"
);
db.execSQL(
"CREATE TABLE cids (" +
"cid TEXT NOT NULL PRIMARY KEY," +
"path TEXT NOT NULL" +
")"
);
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN timeReceived NUMBER"
);
db.execSQL("CREATE INDEX message_time_received_index ON " + Message.TABLENAME + " (timeReceived)");
db.execSQL(
"CREATE TABLE blocked_media (" +
"cid TEXT NOT NULL PRIMARY KEY" +
")"
);
db.execSQL(
"ALTER TABLE cids " +
"ADD COLUMN url TEXT"
);
db.execSQL(
"CREATE TABLE webxdc_updates (" +
"serial INTEGER PRIMARY KEY AUTOINCREMENT, " +
Message.CONVERSATION + " TEXT NOT NULL, " +
"sender TEXT NOT NULL, " +
"thread TEXT NOT NULL, " +
"threadParent TEXT, " +
"info TEXT, " +
"document TEXT, " +
"summary TEXT, " +
"payload TEXT" +
")"
);
db.execSQL("CREATE INDEX webxdc_index ON webxdc_updates (" + Message.CONVERSATION + ", thread)");
db.execSQL(
"ALTER TABLE webxdc_updates " +
"ADD COLUMN message_id TEXT"
);
db.execSQL("CREATE UNIQUE INDEX webxdc_message_id_index ON webxdc_updates (" + Message.CONVERSATION + ", message_id)");
db.execSQL(
"CREATE TABLE muted_participants (" +
"muc_jid TEXT NOT NULL, " +
"occupant_id TEXT NOT NULL, " +
"nick TEXT NOT NULL," +
"PRIMARY KEY (muc_jid, occupant_id)" +
")"
);
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN occupant_id TEXT"
);
if (Build.VERSION.SDK_INT >= 34) {
db.execSQL(
"ALTER TABLE muted_participants " +
"DROP COLUMN nick"
);
} else {
db.execSQL("DROP TABLE muted_participants");
db.execSQL(
"CREATE TABLE muted_participants (" +
"muc_jid TEXT NOT NULL, " +
"occupant_id TEXT NOT NULL, " +
"PRIMARY KEY (muc_jid, occupant_id)" +
")"
);
}
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " " +
"ADD COLUMN notificationDismissed NUMBER DEFAULT 0"
);
requiresMessageIndexRebuild = true;
}
}
private void canonicalizeJids(SQLiteDatabase db) {
// migrate db to new, canonicalized JID domainpart representation
@ -862,7 +799,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (cid == null) return null;
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("monocles.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
Cursor cursor = db.query("cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
DownloadableFile f = null;
if (cursor.moveToNext()) {
f = new DownloadableFile(cursor.getString(0));
@ -873,7 +810,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public String getUrlForCid(Cid cid) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("monocles.cids", new String[]{"url"}, "cid=?", new String[]{cid.toString()}, null, null, null);
Cursor cursor = db.query("cids", new String[]{"url"}, "cid=?", new String[]{cid.toString()}, null, null, null);
String url = null;
if (cursor.moveToNext()) {
url = cursor.getString(0);
@ -892,8 +829,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cv.put("cid", cid.toString());
if (file != null) cv.put("path", file.getAbsolutePath());
if (url != null) cv.put("url", url);
if (db.update("monocles.cids", cv, "cid=?", new String[]{cid.toString()}) < 1) {
db.insertWithOnConflict("monocles.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
if (db.update("cids", cv, "cid=?", new String[]{cid.toString()}) < 1) {
db.insertWithOnConflict("cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
}
}
@ -901,12 +838,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put("cid", cid.toString());
db.insertWithOnConflict("monocles.blocked_media", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
db.insertWithOnConflict("blocked_media", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
}
public boolean isBlockedMedia(Cid cid) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("monocles.blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
Cursor cursor = db.query("blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
boolean is = false;
if (cursor.moveToNext()) {
is = cursor.getInt(0) > 0;
@ -917,13 +854,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void clearBlockedMedia() {
SQLiteDatabase db = this.getWritableDatabase();
db.execSQL("DELETE FROM monocles.blocked_media");
db.execSQL("DELETE FROM blocked_media");
}
public Multimap<String, String> loadMutedMucUsers() {
Multimap<String, String> result = HashMultimap.create();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("monocles.muted_participants", new String[]{"muc_jid", "occupant_id"}, null, null, null, null, null);
Cursor cursor = db.query("muted_participants", new String[]{"muc_jid", "occupant_id"}, null, null, null, null, null);
while (cursor.moveToNext()) {
result.put(cursor.getString(0), cursor.getString(1));
}
@ -938,7 +875,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
ContentValues cv = new ContentValues();
cv.put("muc_jid", user.getMuc().toString());
cv.put("occupant_id", user.getOccupantId());
db.insertWithOnConflict("monocles.muted_participants", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
db.insertWithOnConflict("muted_participants", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
return true;
}
@ -949,14 +886,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getWritableDatabase();
String where = "muc_jid=? AND occupant_id=?";
String[] whereArgs = {user.getMuc().toString(), user.getOccupantId()};
db.delete("monocles.muted_participants", where, whereArgs);
db.delete("muted_participants", where, whereArgs);
return true;
}
public void insertWebxdcUpdate(final WebxdcUpdate update) {
SQLiteDatabase db = this.getWritableDatabase();
db.insertWithOnConflict("monocles.webxdc_updates", null, update.getContentValues(), SQLiteDatabase.CONFLICT_IGNORE);
db.insertWithOnConflict("webxdc_updates", null, update.getContentValues(), SQLiteDatabase.CONFLICT_IGNORE);
}
public WebxdcUpdate findLastWebxdcUpdate(Message message) {
@ -967,7 +904,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent()};
Cursor cursor = db.query("monocles.webxdc_updates", null,
Cursor cursor = db.query("webxdc_updates", null,
Message.CONVERSATION + "=? AND thread=?",
selectionArgs, null, null, "serial ASC");
WebxdcUpdate update = null;
@ -981,7 +918,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent(), String.valueOf(serial)};
Cursor cursor = db.query("monocles.webxdc_updates", null,
Cursor cursor = db.query("webxdc_updates", null,
Message.CONVERSATION + "=? AND thread=? AND serial>?",
selectionArgs, null, null, "serial ASC");
long maxSerial = 0;
@ -1007,7 +944,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void createMessage(Message message) {
SQLiteDatabase db = this.getWritableDatabase();
db.insert(Message.TABLENAME, null, message.getContentValues());
db.insert("monocles." + Message.TABLENAME, null, message.getmonoclesContentValues());
}
public void createAccount(Account account) {
@ -1113,8 +1049,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
Cursor cursor;
cursor = db.rawQuery(
"SELECT * FROM " + Message.TABLENAME + " " +
"LEFT JOIN monocles." + Message.TABLENAME +
" USING (" + Message.UUID + ")" +
"WHERE " + Message.UUID + "=?",
new String[]{uuid}
);
@ -1149,8 +1083,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
Cursor cursor;
cursor = db.rawQuery(
"SELECT * FROM " + Message.TABLENAME + " " +
"LEFT JOIN monocles." + Message.TABLENAME +
" USING (" + Message.UUID + ")" +
"WHERE " + Message.UUID + " IN (" + TextUtils.join(",", template) + ") OR " + Message.SERVER_MSG_ID + " IN (" + TextUtils.join(",", template) + ") OR " + Message.REMOTE_MSG_ID + " IN (" + TextUtils.join(",", template) + ")",
params.toArray(new String[0])
);
@ -1177,9 +1109,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] selectionArgs = {conversation.getUuid()};
cursor = db.rawQuery(
"SELECT * FROM " + Message.TABLENAME + " " +
"LEFT JOIN monocles." + Message.TABLENAME +
" USING (" + Message.UUID + ")" +
" WHERE " + Message.UUID + " IN (" +
"WHERE " + Message.UUID + " IN (" +
"SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
" WHERE " + Message.CONVERSATION + "=? " +
"ORDER BY " + Message.TIME_SENT + " DESC " +
@ -1192,9 +1122,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
Long.toString(timestamp)};
cursor = db.rawQuery(
"SELECT * FROM " + Message.TABLENAME + " " +
"LEFT JOIN monocles." + Message.TABLENAME +
" USING (" + Message.UUID + ")" +
" WHERE " + Message.UUID + " IN (" +
"WHERE " + Message.UUID + " IN (" +
"SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
" WHERE " + Message.CONVERSATION + "=? AND " +
Message.TIME_SENT + "<? " +
@ -1330,7 +1258,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final Conversation conversation, final String messageId) {
final var db = this.getReadableDatabase();
final String sql =
"select * from messages LEFT JOIN monocles.messages USING (uuid) where conversationUuid=? and serverMsgId=? LIMIT 1";
"select * from messages LEFT JOIN messages USING (uuid) where conversationUuid=? and serverMsgId=? LIMIT 1";
final String[] args = {conversation.getUuid(), messageId};
final Cursor cursor = db.rawQuery(sql, args);
if (cursor == null) {
@ -1350,7 +1278,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final Conversation conversation, final String messageId) {
final var db = this.getReadableDatabase();
final String sql =
"select * from messages LEFT JOIN monocles.messages USING (uuid) where conversationUuid=? and (uuid=? OR remoteMsgId=?) LIMIT 1";
"select * from messages LEFT JOIN messages USING (uuid) where conversationUuid=? and (uuid=? OR remoteMsgId=?) LIMIT 1";
final String[] args = {conversation.getUuid(), messageId, messageId};
final Cursor cursor = db.rawQuery(sql, args);
if (cursor == null) {
@ -1473,15 +1401,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (!includeBody) {
contentValues.remove(Message.BODY);
}
return db.update(Message.TABLENAME, contentValues, Message.UUID + "=?", args) == 1 &&
db.update("monocles." + Message.TABLENAME, message.getmonoclesContentValues(), Message.UUID + "=?", args) == 1;
return db.update(Message.TABLENAME, contentValues, Message.UUID + "=?", args) == 1;
}
public boolean updateMessage(Message message, String uuid) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {uuid};
return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1 &&
db.update("monocles." + Message.TABLENAME, message.getmonoclesContentValues(), Message.UUID + "=?", args) == 1;
return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1;
}
@ -1489,7 +1415,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {uuid};
return db.delete(Message.TABLENAME, Message.UUID + "=?", args) == 1 &&
db.delete("monocles." + Message.TABLENAME, Message.UUID + "=?", args) == 1;
db.delete("" + Message.TABLENAME, Message.UUID + "=?", args) == 1;
}
public void readRoster(Roster roster) {
@ -1531,7 +1457,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.beginTransaction();
final String[] args = {conversation.getUuid()};
int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
db.delete("monocles.webxdc_updates", Message.CONVERSATION + "=?", args);
db.delete("webxdc_updates", Message.CONVERSATION + "=?", args);
db.setTransactionSuccessful();
db.endTransaction();
Log.d(Config.LOGTAG, "deleted " + num + " messages for " + conversation.getJid().asBareJid() + " in " + (SystemClock.elapsedRealtime() - start) + "ms");
@ -1542,7 +1468,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getReadableDatabase();
db.beginTransaction();
db.delete(Message.TABLENAME, "timeSent<?", args);
db.delete("monocles.messages", "timeReceived<?", args);
db.delete("messages", "timeReceived<?", args);
db.setTransactionSuccessful();
db.endTransaction();
}
@ -2142,4 +2068,59 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
return contacts;
}
public Iterable<Message> getMessagesIterable(final Conversation conversation) {
return () -> new Iterator<Message>() {
@Override
public boolean hasNext() {
if (messageCursor == null) return false;
return !messageCursor.isAfterLast();
}
final SQLiteDatabase database = getReadableDatabase();
final String[] queryArgs = {conversation.getUuid(), "1"};
Cursor messageCursor = null;
{
messageCursor = database.query(Message.TABLENAME, null, Message.CONVERSATION
+ "=? and " + Message.DELETED + "<?", queryArgs, null, null, Message.TIME_SENT
+ " ASC", null);
if (messageCursor != null) {
messageCursor.moveToFirst();
}
}
@Override
public Message next() {
if (messageCursor == null || messageCursor.isAfterLast()) {
throw new NoSuchElementException();
}
Message message = null;
try {
message = Message.fromCursor(messageCursor, conversation);
} catch (IOException e) {
messageCursor.close();
throw new RuntimeException(e);
}
messageCursor.moveToNext();
return message;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
protected void finalize() throws Throwable {
if (messageCursor != null) {
messageCursor.close();
}
if (database != null) {
database.close();
}
super.finalize();
}
};
}
}

View file

@ -7,10 +7,12 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ServiceInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.NonNull;
@ -24,6 +26,7 @@ import com.google.common.base.Strings;
import com.google.gson.stream.JsonWriter;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.entities.Account;
@ -33,10 +36,13 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.xmpp.Jid;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
@ -74,11 +80,18 @@ public class ExportBackupWorker extends Worker {
public static final String MIME_TYPE = "application/vnd.conversations.backup";
private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
private static final int NOTIFICATION_ID = 19;
private static final int BACKUP_CREATED_NOTIFICATION_ID = 23;
private final boolean recurringBackup;
boolean ReadableLogsEnabled = false;
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
final var inputData = workerParams.getInputData();
@ -234,6 +247,25 @@ public class ExportBackupWorker extends Worker {
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
mDatabaseBackend = DatabaseBackend.getInstance(Conversations.getContext());
mAccounts = mDatabaseBackend.getAccounts();
final SharedPreferences ReadableLogs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ReadableLogsEnabled = ReadableLogs.getBoolean("export_plain_text_logs", getApplicationContext().getResources().getBoolean(R.bool.plain_text_logs));
try {
if (ReadableLogsEnabled) { // todo
List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
for (Conversation conversation : conversations) {
writeToFile(conversation);
Log.d(Config.LOGTAG, "Exporting readable logs for " + conversation.getJid());
}
}
} catch (Exception e) {
e.printStackTrace();
}
return files;
}
@ -245,7 +277,7 @@ public class ExportBackupWorker extends Worker {
private void messageExportmonocles(final SQLiteDatabase db, final String uuid, final JsonWriter writer, final Progress progress) throws IOException {
final var notificationManager = getApplicationContext().getSystemService(NotificationManager.class);
Cursor cursor = db.rawQuery("select mmessages.* from messages join monocles.messages mmessages using (uuid) join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
Cursor cursor = db.rawQuery("select mmessages.* from messages join messages mmessages using (uuid) join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " monocles messages for account " + uuid);
int i = 0;
@ -253,7 +285,7 @@ public class ExportBackupWorker extends Worker {
while (cursor != null && cursor.moveToNext()) {
writer.beginObject();
writer.name("table");
writer.value("monocles." + Message.TABLENAME);
writer.value("" + Message.TABLENAME);
writer.name("values");
writer.beginObject();
for (int j = 0; j < cursor.getColumnCount(); ++j) {
@ -275,13 +307,13 @@ public class ExportBackupWorker extends Worker {
cursor.close();
}
cursor = db.rawQuery("select webxdc_updates.* from " + Conversation.TABLENAME + " join monocles.webxdc_updates webxdc_updates on " + Conversation.TABLENAME + ".uuid=webxdc_updates." + Message.CONVERSATION + " where conversations.accountUuid=?", new String[]{uuid});
cursor = db.rawQuery("select webxdc_updates.* from " + Conversation.TABLENAME + " join webxdc_updates webxdc_updates on " + Conversation.TABLENAME + ".uuid=webxdc_updates." + Message.CONVERSATION + " where conversations.accountUuid=?", new String[]{uuid});
size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " WebXDC updates for account " + uuid);
while (cursor != null && cursor.moveToNext()) {
writer.beginObject();
writer.name("table");
writer.value("monocles.webxdc_updates");
writer.value("webxdc_updates");
writer.name("values");
writer.beginObject();
for (int j = 0; j < cursor.getColumnCount(); ++j) {
@ -537,4 +569,70 @@ public class ExportBackupWorker extends Worker {
return notification.build();
}
}
private void writeToFile(Conversation conversation) {
Jid accountJid = resolveAccountUuid(conversation.getAccountUuid());
Jid contactJid = conversation.getJid();
final File dir = new File(FileBackend.getBackupDirectory(getApplicationContext()), accountJid.asBareJid().toString());
dir.mkdirs();
BufferedWriter bw = null;
try {
for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) {
if (message == null)
continue;
if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) {
String date = DATE_FORMAT.format(new Date(message.getTimeSent()));
if (bw == null) {
bw = new BufferedWriter(new FileWriter(
new File(dir, contactJid.asBareJid().toString() + ".txt")));
}
String jid = null;
switch (message.getStatus()) {
case Message.STATUS_RECEIVED:
jid = getMessageCounterpart(message);
break;
case Message.STATUS_SEND:
case Message.STATUS_SEND_RECEIVED:
case Message.STATUS_SEND_DISPLAYED:
case Message.STATUS_SEND_FAILED:
jid = accountJid.asBareJid().toString();
break;
}
if (jid != null) {
String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody();
bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid, body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
private Jid resolveAccountUuid(String accountUuid) {
for (Account account : mAccounts) {
if (account.getUuid().equals(accountUuid)) {
return account.getJid();
}
}
return null;
}
private String getMessageCounterpart(Message message) {
String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART);
if (trueCounterpart != null) {
return trueCounterpart;
} else {
return message.getCounterpart().toString();
}
}
}

View file

@ -14,4 +14,5 @@
<bool name="secure_tls">false</bool>
<bool name="prefer_ipv6">false</bool>
<bool name="use_colored_muc_names">false</bool>
<bool name="plain_text_logs">false</bool>
</resources>

View file

@ -1412,4 +1412,6 @@
<string name="show_to_contacts_only">Show to contacts only</string>
<string name="success_export_settings">Settings successfully exported</string>
<string name="error_export_settings">Error while exporting settings</string>
<string name="pref_export_plain_text_logs">Export plain text</string>
<string name="pref_export_plain_text_logs_summary">Export messages unencrypted in human readable plain text</string>
</resources>

View file

@ -27,6 +27,12 @@
android:key="backup_directory"
android:summary="@string/pref_create_backup_summary" />
<SwitchPreferenceCompat
android:defaultValue="@bool/plain_text_logs"
android:key="export_plain_text_logs"
android:summary="@string/pref_export_plain_text_logs_summary"
android:title="@string/pref_export_plain_text_logs" />
<PreferenceCategory
android:title="@string/pref_category_settings">
<Preference

View file

@ -95,9 +95,9 @@ public class ImportBackupService extends Service {
Account.TABLENAME,
Conversation.TABLENAME,
Message.TABLENAME,
"monocles." + Message.TABLENAME,
"monocles.webxdc_updates",
"monocles.muted_participants",
Message.TABLENAME,
"webxdc_updates",
"muted_participants",
SQLiteAxolotlStore.PREKEY_TABLENAME,
SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
SQLiteAxolotlStore.SESSION_TABLENAME,