1package eu.siacs.conversations.persistance;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6import android.database.DatabaseUtils;
7import android.database.sqlite.SQLiteCantOpenDatabaseException;
8import android.database.sqlite.SQLiteDatabase;
9import android.database.sqlite.SQLiteOpenHelper;
10import android.os.Environment;
11import android.util.Base64;
12import android.util.Log;
13
14import org.json.JSONObject;
15import org.whispersystems.libsignal.SignalProtocolAddress;
16import org.whispersystems.libsignal.IdentityKey;
17import org.whispersystems.libsignal.IdentityKeyPair;
18import org.whispersystems.libsignal.InvalidKeyException;
19import org.whispersystems.libsignal.state.PreKeyRecord;
20import org.whispersystems.libsignal.state.SessionRecord;
21import org.whispersystems.libsignal.state.SignedPreKeyRecord;
22
23import java.io.ByteArrayInputStream;
24import java.io.File;
25import java.io.IOException;
26import java.security.cert.CertificateEncodingException;
27import java.security.cert.CertificateException;
28import java.security.cert.CertificateFactory;
29import java.security.cert.X509Certificate;
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.HashMap;
33import java.util.HashSet;
34import java.util.Iterator;
35import java.util.List;
36import java.util.Map;
37import java.util.Set;
38import java.util.concurrent.CopyOnWriteArrayList;
39import org.json.JSONException;
40
41import eu.siacs.conversations.Config;
42import eu.siacs.conversations.crypto.axolotl.AxolotlService;
43import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
44import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
45import eu.siacs.conversations.entities.Account;
46import eu.siacs.conversations.entities.Contact;
47import eu.siacs.conversations.entities.Conversation;
48import eu.siacs.conversations.entities.Message;
49import eu.siacs.conversations.entities.PresenceTemplate;
50import eu.siacs.conversations.entities.Roster;
51import eu.siacs.conversations.entities.ServiceDiscoveryResult;
52import eu.siacs.conversations.services.ShortcutService;
53import eu.siacs.conversations.utils.CryptoHelper;
54import eu.siacs.conversations.utils.MimeUtils;
55import eu.siacs.conversations.xmpp.jid.InvalidJidException;
56import eu.siacs.conversations.xmpp.jid.Jid;
57import eu.siacs.conversations.xmpp.mam.MamReference;
58
59public class DatabaseBackend extends SQLiteOpenHelper {
60
61 private static DatabaseBackend instance = null;
62
63 private static final String DATABASE_NAME = "history";
64 private static final int DATABASE_VERSION = 38;
65
66 private static String CREATE_CONTATCS_STATEMENT = "create table "
67 + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
68 + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT,"
69 + Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
70 + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
71 + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
72 + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
73 + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
74 + Account.TABLENAME + "(" + Account.UUID
75 + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
76 + Contact.JID + ") ON CONFLICT REPLACE);";
77
78 private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table "
79 + ServiceDiscoveryResult.TABLENAME + "("
80 + ServiceDiscoveryResult.HASH + " TEXT, "
81 + ServiceDiscoveryResult.VER + " TEXT, "
82 + ServiceDiscoveryResult.RESULT + " TEXT, "
83 + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
84 + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
85
86 private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE "
87 + PresenceTemplate.TABELNAME + "("
88 + PresenceTemplate.UUID + " TEXT, "
89 + PresenceTemplate.LAST_USED + " NUMBER,"
90 + PresenceTemplate.MESSAGE + " TEXT,"
91 + PresenceTemplate.STATUS + " TEXT,"
92 + "UNIQUE("+PresenceTemplate.MESSAGE + "," +PresenceTemplate.STATUS+") ON CONFLICT REPLACE);";
93
94 private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
95 + SQLiteAxolotlStore.PREKEY_TABLENAME + "("
96 + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
97 + SQLiteAxolotlStore.ID + " INTEGER, "
98 + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
99 + SQLiteAxolotlStore.ACCOUNT
100 + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
101 + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
102 + SQLiteAxolotlStore.ID
103 + ") ON CONFLICT REPLACE"
104 + ");";
105
106 private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
107 + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
108 + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
109 + SQLiteAxolotlStore.ID + " INTEGER, "
110 + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
111 + SQLiteAxolotlStore.ACCOUNT
112 + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
113 + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
114 + SQLiteAxolotlStore.ID
115 + ") ON CONFLICT REPLACE" +
116 ");";
117
118 private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
119 + SQLiteAxolotlStore.SESSION_TABLENAME + "("
120 + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
121 + SQLiteAxolotlStore.NAME + " TEXT, "
122 + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
123 + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
124 + SQLiteAxolotlStore.ACCOUNT
125 + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
126 + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
127 + SQLiteAxolotlStore.NAME + ", "
128 + SQLiteAxolotlStore.DEVICE_ID
129 + ") ON CONFLICT REPLACE"
130 + ");";
131
132 private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
133 + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
134 + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
135 + SQLiteAxolotlStore.NAME + " TEXT, "
136 + SQLiteAxolotlStore.OWN + " INTEGER, "
137 + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
138 + SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
139 + SQLiteAxolotlStore.TRUST + " TEXT, "
140 + SQLiteAxolotlStore.ACTIVE + " NUMBER, "
141 + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
142 + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
143 + SQLiteAxolotlStore.ACCOUNT
144 + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
145 + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
146 + SQLiteAxolotlStore.NAME + ", "
147 + SQLiteAxolotlStore.FINGERPRINT
148 + ") ON CONFLICT IGNORE"
149 + ");";
150
151 private static String START_TIMES_TABLE = "start_times";
152
153 private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
154
155 private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON "+Message.TABLENAME+"("+Message.TIME_SENT+")";
156 private static String CREATE_MESSAGE_CONVERSATION_INDEX = "create INDEX message_conversation_index ON "+Message.TABLENAME+"("+Message.CONVERSATION+")";
157
158 private DatabaseBackend(Context context) {
159 super(context, DATABASE_NAME, null, DATABASE_VERSION);
160 }
161
162 @Override
163 public void onCreate(SQLiteDatabase db) {
164 db.execSQL("PRAGMA foreign_keys=ON;");
165 db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID+ " TEXT PRIMARY KEY,"
166 + Account.USERNAME + " TEXT,"
167 + Account.SERVER + " TEXT,"
168 + Account.PASSWORD + " TEXT,"
169 + Account.DISPLAY_NAME + " TEXT, "
170 + Account.STATUS + " TEXT,"
171 + Account.STATUS_MESSAGE + " TEXT,"
172 + Account.ROSTERVERSION + " TEXT,"
173 + Account.OPTIONS + " NUMBER, "
174 + Account.AVATAR + " TEXT, "
175 + Account.KEYS + " TEXT, "
176 + Account.HOSTNAME + " TEXT, "
177 + Account.PORT + " NUMBER DEFAULT 5222)");
178 db.execSQL("create table " + Conversation.TABLENAME + " ("
179 + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
180 + " TEXT, " + Conversation.CONTACT + " TEXT, "
181 + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
182 + " TEXT, " + Conversation.CREATED + " NUMBER, "
183 + Conversation.STATUS + " NUMBER, " + Conversation.MODE
184 + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY("
185 + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME
186 + "(" + Account.UUID + ") ON DELETE CASCADE);");
187 db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
188 + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
189 + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
190 + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
191 + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
192 + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
193 + Message.RELATIVE_FILE_PATH + " TEXT, "
194 + Message.SERVER_MSG_ID + " TEXT, "
195 + Message.FINGERPRINT + " TEXT, "
196 + Message.CARBON + " INTEGER, "
197 + Message.EDITED + " TEXT, "
198 + Message.READ + " NUMBER DEFAULT 1, "
199 + Message.OOB + " INTEGER, "
200 + Message.ERROR_MESSAGE + " TEXT,"
201 + Message.READ_BY_MARKERS + " TEXT,"
202 + Message.MARKABLE + " NUMBER DEFAULT 0,"
203 + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
204 + Message.CONVERSATION + ") REFERENCES "
205 + Conversation.TABLENAME + "(" + Conversation.UUID
206 + ") ON DELETE CASCADE);");
207 db.execSQL(CREATE_MESSAGE_TIME_INDEX);
208 db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX);
209 db.execSQL(CREATE_CONTATCS_STATEMENT);
210 db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
211 db.execSQL(CREATE_SESSIONS_STATEMENT);
212 db.execSQL(CREATE_PREKEYS_STATEMENT);
213 db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
214 db.execSQL(CREATE_IDENTITIES_STATEMENT);
215 db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
216 db.execSQL(CREATE_START_TIMES_TABLE);
217 }
218
219 @Override
220 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
221 if (oldVersion < 2 && newVersion >= 2) {
222 db.execSQL("update " + Account.TABLENAME + " set "
223 + Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
224 }
225 if (oldVersion < 3 && newVersion >= 3) {
226 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
227 + Message.TYPE + " NUMBER");
228 }
229 if (oldVersion < 5 && newVersion >= 5) {
230 db.execSQL("DROP TABLE " + Contact.TABLENAME);
231 db.execSQL(CREATE_CONTATCS_STATEMENT);
232 db.execSQL("UPDATE " + Account.TABLENAME + " SET "
233 + Account.ROSTERVERSION + " = NULL");
234 }
235 if (oldVersion < 6 && newVersion >= 6) {
236 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
237 + Message.TRUE_COUNTERPART + " TEXT");
238 }
239 if (oldVersion < 7 && newVersion >= 7) {
240 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
241 + Message.REMOTE_MSG_ID + " TEXT");
242 db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
243 + Contact.AVATAR + " TEXT");
244 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN "
245 + Account.AVATAR + " TEXT");
246 }
247 if (oldVersion < 8 && newVersion >= 8) {
248 db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
249 + Conversation.ATTRIBUTES + " TEXT");
250 }
251 if (oldVersion < 9 && newVersion >= 9) {
252 db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
253 + Contact.LAST_TIME + " NUMBER");
254 db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
255 + Contact.LAST_PRESENCE + " TEXT");
256 }
257 if (oldVersion < 10 && newVersion >= 10) {
258 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
259 + Message.RELATIVE_FILE_PATH + " TEXT");
260 }
261 if (oldVersion < 11 && newVersion >= 11) {
262 db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
263 + Contact.GROUPS + " TEXT");
264 db.execSQL("delete from " + Contact.TABLENAME);
265 db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
266 }
267 if (oldVersion < 12 && newVersion >= 12) {
268 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
269 + Message.SERVER_MSG_ID + " TEXT");
270 }
271 if (oldVersion < 13 && newVersion >= 13) {
272 db.execSQL("delete from " + Contact.TABLENAME);
273 db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
274 }
275 if (oldVersion < 14 && newVersion >= 14) {
276 canonicalizeJids(db);
277 }
278 if (oldVersion < 15 && newVersion >= 15) {
279 recreateAxolotlDb(db);
280 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
281 + Message.FINGERPRINT + " TEXT");
282 }
283 if (oldVersion < 16 && newVersion >= 16) {
284 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
285 + Message.CARBON + " INTEGER");
286 }
287 if (oldVersion < 19 && newVersion >= 19) {
288 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT");
289 }
290 if (oldVersion < 20 && newVersion >= 20) {
291 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
292 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
293 }
294 if (oldVersion < 26 && newVersion >= 26) {
295 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
296 db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT");
297 }
298 /* Any migrations that alter the Account table need to happen BEFORE this migration, as it
299 * depends on account de-serialization.
300 */
301 if (oldVersion < 17 && newVersion >= 17 && newVersion < 31) {
302 List<Account> accounts = getAccounts(db);
303 for (Account account : accounts) {
304 String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
305 if (ownDeviceIdString == null) {
306 continue;
307 }
308 int ownDeviceId = Integer.valueOf(ownDeviceIdString);
309 SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId);
310 deleteSession(db, account, ownAddress);
311 IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
312 if (identityKeyPair != null) {
313 String[] selectionArgs = {
314 account.getUuid(),
315 CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize())
316 };
317 ContentValues values = new ContentValues();
318 values.put(SQLiteAxolotlStore.TRUSTED, 2);
319 db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
320 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
321 + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
322 selectionArgs);
323 } else {
324 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
325 }
326 }
327 }
328 if (oldVersion < 18 && newVersion >= 18) {
329 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1");
330 }
331
332 if (oldVersion < 21 && newVersion >= 21) {
333 List<Account> accounts = getAccounts(db);
334 for (Account account : accounts) {
335 account.unsetPgpSignature();
336 db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
337 + "=?", new String[]{account.getUuid()});
338 }
339 }
340
341 if (oldVersion >= 15 && oldVersion < 22 && newVersion >= 22) {
342 db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
343 }
344
345 if (oldVersion < 23 && newVersion >= 23) {
346 db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
347 }
348
349 if (oldVersion < 24 && newVersion >= 24) {
350 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
351 }
352
353 if (oldVersion < 25 && newVersion >= 25) {
354 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
355 }
356
357 if (oldVersion < 26 && newVersion >= 26) {
358 db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
359 }
360
361 if (oldVersion < 27 && newVersion >= 27) {
362 db.execSQL("DELETE FROM "+ServiceDiscoveryResult.TABLENAME);
363 }
364
365 if (oldVersion < 28 && newVersion >= 28) {
366 canonicalizeJids(db);
367 }
368
369 if (oldVersion < 29 && newVersion >= 29) {
370 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
371 }
372 if (oldVersion < 30 && newVersion >= 30) {
373 db.execSQL(CREATE_START_TIMES_TABLE);
374 }
375 if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) {
376 db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
377 db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
378 HashMap<Integer,ContentValues> migration = new HashMap<>();
379 migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true));
380 migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
381 migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
382 migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
383 migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
384 migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
385 migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
386 migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
387 migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
388 for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) {
389 String whereClause = SQLiteAxolotlStore.TRUSTED+"=?";
390 String[] where = {String.valueOf(entry.getKey())};
391 db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where);
392 }
393
394 }
395 if (oldVersion >= 15 && oldVersion < 32 && newVersion >= 32) {
396 db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
397 ContentValues defaults = new ContentValues();
398 defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis());
399 db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null);
400 }
401 if (oldVersion >= 15 && oldVersion < 33 && newVersion >= 33) {
402 String whereClause = SQLiteAxolotlStore.OWN+"=1";
403 db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null);
404 }
405
406 if (oldVersion < 34 && newVersion >= 34) {
407 db.execSQL(CREATE_MESSAGE_TIME_INDEX);
408
409 final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)+"/Conversations/");
410 final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/");
411 final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/");
412 final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/");
413 if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
414 final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/");
415 newPicturesDirectory.getParentFile().mkdirs();
416 if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
417 Log.d(Config.LOGTAG,"moved "+oldPicturesDirectory.getAbsolutePath()+" to "+newPicturesDirectory.getAbsolutePath());
418 }
419 }
420 if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
421 newFilesDirectory.mkdirs();
422 newVideosDirectory.mkdirs();
423 final File[] files = oldFilesDirectory.listFiles();
424 if (files == null) {
425 return;
426 }
427 for(File file : files) {
428 if (file.getName().equals(".nomedia")) {
429 if (file.delete()) {
430 Log.d(Config.LOGTAG,"deleted nomedia file in "+oldFilesDirectory.getAbsolutePath());
431 }
432 } else if (file.isFile()) {
433 final String name = file.getName();
434 boolean isVideo = false;
435 int start = name.lastIndexOf('.') + 1;
436 if (start < name.length()) {
437 String mime= MimeUtils.guessMimeTypeFromExtension(name.substring(start));
438 isVideo = mime != null && mime.startsWith("video/");
439 }
440 File dst = new File((isVideo ? newVideosDirectory : newFilesDirectory).getAbsolutePath()+"/"+file.getName());
441 if (file.renameTo(dst)) {
442 Log.d(Config.LOGTAG, "moved " + file + " to " + dst);
443 }
444 }
445 }
446 }
447 }
448 if (oldVersion < 35 && newVersion >= 35) {
449 db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX);
450 }
451 if (oldVersion < 36 && newVersion >= 36) {
452 List<Account> accounts = getAccounts(db);
453 for (Account account : accounts) {
454 account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,true);
455 account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY,false);
456 db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
457 + "=?", new String[]{account.getUuid()});
458 }
459 }
460
461 if (oldVersion < 37 && newVersion >= 37) {
462 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ_BY_MARKERS + " TEXT");
463 }
464
465 if (oldVersion < 38 && newVersion >= 38) {
466 db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
467 }
468 }
469
470 private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
471 ContentValues values = new ContentValues();
472 values.put(SQLiteAxolotlStore.TRUST,trust.toString());
473 values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
474 return values;
475 }
476
477 private void canonicalizeJids(SQLiteDatabase db) {
478 // migrate db to new, canonicalized JID domainpart representation
479
480 // Conversation table
481 Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
482 while (cursor.moveToNext()) {
483 String newJid;
484 try {
485 newJid = Jid.fromString(
486 cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
487 ).toPreppedString();
488 } catch (InvalidJidException ignored) {
489 Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
490 + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
491 + ": " + ignored + ". Skipping...");
492 continue;
493 }
494
495 String updateArgs[] = {
496 newJid,
497 cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
498 };
499 db.execSQL("update " + Conversation.TABLENAME
500 + " set " + Conversation.CONTACTJID + " = ? "
501 + " where " + Conversation.UUID + " = ?", updateArgs);
502 }
503 cursor.close();
504
505 // Contact table
506 cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
507 while (cursor.moveToNext()) {
508 String newJid;
509 try {
510 newJid = Jid.fromString(
511 cursor.getString(cursor.getColumnIndex(Contact.JID))
512 ).toPreppedString();
513 } catch (InvalidJidException ignored) {
514 Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
515 + cursor.getString(cursor.getColumnIndex(Contact.JID))
516 + ": " + ignored + ". Skipping...");
517 continue;
518 }
519
520 String updateArgs[] = {
521 newJid,
522 cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
523 cursor.getString(cursor.getColumnIndex(Contact.JID)),
524 };
525 db.execSQL("update " + Contact.TABLENAME
526 + " set " + Contact.JID + " = ? "
527 + " where " + Contact.ACCOUNT + " = ? "
528 + " AND " + Contact.JID + " = ?", updateArgs);
529 }
530 cursor.close();
531
532 // Account table
533 cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
534 while (cursor.moveToNext()) {
535 String newServer;
536 try {
537 newServer = Jid.fromParts(
538 cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
539 cursor.getString(cursor.getColumnIndex(Account.SERVER)),
540 "mobile"
541 ).getDomainpart();
542 } catch (InvalidJidException ignored) {
543 Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
544 + cursor.getString(cursor.getColumnIndex(Account.SERVER))
545 + ": " + ignored + ". Skipping...");
546 continue;
547 }
548
549 String updateArgs[] = {
550 newServer,
551 cursor.getString(cursor.getColumnIndex(Account.UUID)),
552 };
553 db.execSQL("update " + Account.TABLENAME
554 + " set " + Account.SERVER + " = ? "
555 + " where " + Account.UUID + " = ?", updateArgs);
556 }
557 cursor.close();
558 }
559
560 public static synchronized DatabaseBackend getInstance(Context context) {
561 if (instance == null) {
562 instance = new DatabaseBackend(context);
563 }
564 return instance;
565 }
566
567 public void createConversation(Conversation conversation) {
568 SQLiteDatabase db = this.getWritableDatabase();
569 db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
570 }
571
572 public void createMessage(Message message) {
573 SQLiteDatabase db = this.getWritableDatabase();
574 db.insert(Message.TABLENAME, null, message.getContentValues());
575 }
576
577 public void createAccount(Account account) {
578 SQLiteDatabase db = this.getWritableDatabase();
579 db.insert(Account.TABLENAME, null, account.getContentValues());
580 }
581
582 public void insertDiscoveryResult(ServiceDiscoveryResult result) {
583 SQLiteDatabase db = this.getWritableDatabase();
584 db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
585 }
586
587 public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
588 SQLiteDatabase db = this.getReadableDatabase();
589 String[] selectionArgs = {hash, ver};
590 Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null,
591 ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
592 selectionArgs, null, null, null);
593 if (cursor.getCount() == 0) {
594 cursor.close();
595 return null;
596 }
597 cursor.moveToFirst();
598
599 ServiceDiscoveryResult result = null;
600 try {
601 result = new ServiceDiscoveryResult(cursor);
602 } catch (JSONException e) { /* result is still null */ }
603
604 cursor.close();
605 return result;
606 }
607
608 public void insertPresenceTemplate(PresenceTemplate template) {
609 SQLiteDatabase db = this.getWritableDatabase();
610 db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());
611 }
612
613 public List<PresenceTemplate> getPresenceTemplates() {
614 ArrayList<PresenceTemplate> templates = new ArrayList<>();
615 SQLiteDatabase db = this.getReadableDatabase();
616 Cursor cursor = db.query(PresenceTemplate.TABELNAME,null,null,null,null,null,PresenceTemplate.LAST_USED+" desc");
617 while (cursor.moveToNext()) {
618 templates.add(PresenceTemplate.fromCursor(cursor));
619 }
620 cursor.close();
621 return templates;
622 }
623
624 public void deletePresenceTemplate(PresenceTemplate template) {
625 Log.d(Config.LOGTAG,"deleting presence template with uuid "+template.getUuid());
626 SQLiteDatabase db = this.getWritableDatabase();
627 String where = PresenceTemplate.UUID+"=?";
628 String[] whereArgs = {template.getUuid()};
629 db.delete(PresenceTemplate.TABELNAME,where,whereArgs);
630 }
631
632 public CopyOnWriteArrayList<Conversation> getConversations(int status) {
633 CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
634 SQLiteDatabase db = this.getReadableDatabase();
635 String[] selectionArgs = {Integer.toString(status)};
636 Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME
637 + " where " + Conversation.STATUS + " = ? order by "
638 + Conversation.CREATED + " desc", selectionArgs);
639 while (cursor.moveToNext()) {
640 list.add(Conversation.fromCursor(cursor));
641 }
642 cursor.close();
643 return list;
644 }
645
646 public ArrayList<Message> getMessages(Conversation conversations, int limit) {
647 return getMessages(conversations, limit, -1);
648 }
649
650 public ArrayList<Message> getMessages(Conversation conversation, int limit,
651 long timestamp) {
652 ArrayList<Message> list = new ArrayList<>();
653 SQLiteDatabase db = this.getReadableDatabase();
654 Cursor cursor;
655 if (timestamp == -1) {
656 String[] selectionArgs = {conversation.getUuid()};
657 cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
658 + "=?", selectionArgs, null, null, Message.TIME_SENT
659 + " DESC", String.valueOf(limit));
660 } else {
661 String[] selectionArgs = {conversation.getUuid(),
662 Long.toString(timestamp)};
663 cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
664 + "=? and " + Message.TIME_SENT + "<?", selectionArgs,
665 null, null, Message.TIME_SENT + " DESC",
666 String.valueOf(limit));
667 }
668 if (cursor.getCount() > 0) {
669 cursor.moveToLast();
670 do {
671 Message message = Message.fromCursor(cursor,conversation);
672 if (message != null) {
673 list.add(message);
674 }
675 } while (cursor.moveToPrevious());
676 }
677 cursor.close();
678 return list;
679 }
680
681 public Iterable<Message> getMessagesIterable(final Conversation conversation) {
682 return new Iterable<Message>() {
683 @Override
684 public Iterator<Message> iterator() {
685 class MessageIterator implements Iterator<Message> {
686 SQLiteDatabase db = getReadableDatabase();
687 String[] selectionArgs = {conversation.getUuid()};
688 Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
689 + "=?", selectionArgs, null, null, Message.TIME_SENT
690 + " ASC", null);
691
692 public MessageIterator() {
693 cursor.moveToFirst();
694 }
695
696 @Override
697 public boolean hasNext() {
698 return !cursor.isAfterLast();
699 }
700
701 @Override
702 public Message next() {
703 Message message = Message.fromCursor(cursor, conversation);
704 cursor.moveToNext();
705 return message;
706 }
707
708 @Override
709 public void remove() {
710 throw new UnsupportedOperationException();
711 }
712 }
713 return new MessageIterator();
714 }
715 };
716 }
717
718 public Conversation findConversation(final Account account, final Jid contactJid) {
719 SQLiteDatabase db = this.getReadableDatabase();
720 String[] selectionArgs = {account.getUuid(),
721 contactJid.toBareJid().toPreppedString() + "/%",
722 contactJid.toBareJid().toPreppedString()
723 };
724 Cursor cursor = db.query(Conversation.TABLENAME, null,
725 Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
726 + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
727 if (cursor.getCount() == 0) {
728 cursor.close();
729 return null;
730 }
731 cursor.moveToFirst();
732 Conversation conversation = Conversation.fromCursor(cursor);
733 cursor.close();
734 return conversation;
735 }
736
737 public void updateConversation(final Conversation conversation) {
738 final SQLiteDatabase db = this.getWritableDatabase();
739 final String[] args = {conversation.getUuid()};
740 db.update(Conversation.TABLENAME, conversation.getContentValues(),
741 Conversation.UUID + "=?", args);
742 }
743
744 public List<Account> getAccounts() {
745 SQLiteDatabase db = this.getReadableDatabase();
746 return getAccounts(db);
747 }
748
749 public List<Jid> getAccountJids() {
750 SQLiteDatabase db = this.getReadableDatabase();
751 final List<Jid> jids = new ArrayList<>();
752 final String[] columns = new String[]{Account.USERNAME, Account.SERVER};
753 Cursor cursor = db.query(Account.TABLENAME,columns,null,null,null,null,null);
754 try {
755 while(cursor.moveToNext()) {
756 jids.add(Jid.fromParts(cursor.getString(0),cursor.getString(1),null));
757 }
758 return jids;
759 } catch (Exception e) {
760 return jids;
761 } finally {
762 if (cursor != null) {
763 cursor.close();
764 }
765 }
766 }
767
768 private List<Account> getAccounts(SQLiteDatabase db) {
769 List<Account> list = new ArrayList<>();
770 Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
771 null, null);
772 while (cursor.moveToNext()) {
773 list.add(Account.fromCursor(cursor));
774 }
775 cursor.close();
776 return list;
777 }
778
779 public boolean updateAccount(Account account) {
780 SQLiteDatabase db = this.getWritableDatabase();
781 String[] args = {account.getUuid()};
782 final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args);
783 return rows == 1;
784 }
785
786 public boolean deleteAccount(Account account) {
787 SQLiteDatabase db = this.getWritableDatabase();
788 String[] args = {account.getUuid()};
789 final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args);
790 return rows == 1;
791 }
792
793 @Override
794 public SQLiteDatabase getWritableDatabase() {
795 SQLiteDatabase db = super.getWritableDatabase();
796 db.execSQL("PRAGMA foreign_keys=ON;");
797 return db;
798 }
799
800 public void updateMessage(Message message) {
801 SQLiteDatabase db = this.getWritableDatabase();
802 String[] args = {message.getUuid()};
803 db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
804 + "=?", args);
805 }
806
807 public void updateMessage(Message message, String uuid) {
808 SQLiteDatabase db = this.getWritableDatabase();
809 String[] args = {uuid};
810 db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
811 + "=?", args);
812 }
813
814 public void readRoster(Roster roster) {
815 SQLiteDatabase db = this.getReadableDatabase();
816 Cursor cursor;
817 String args[] = {roster.getAccount().getUuid()};
818 cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null);
819 while (cursor.moveToNext()) {
820 roster.initContact(Contact.fromCursor(cursor));
821 }
822 cursor.close();
823 }
824
825 public void writeRoster(final Roster roster) {
826 final Account account = roster.getAccount();
827 final SQLiteDatabase db = this.getWritableDatabase();
828 db.beginTransaction();
829 for (Contact contact : roster.getContacts()) {
830 if (contact.getOption(Contact.Options.IN_ROSTER)) {
831 db.insert(Contact.TABLENAME, null, contact.getContentValues());
832 } else {
833 String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
834 String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()};
835 db.delete(Contact.TABLENAME, where, whereArgs);
836 }
837 }
838 db.setTransactionSuccessful();
839 db.endTransaction();
840 account.setRosterVersion(roster.getVersion());
841 updateAccount(account);
842 }
843
844 public void deleteMessagesInConversation(Conversation conversation) {
845 SQLiteDatabase db = this.getWritableDatabase();
846 String[] args = {conversation.getUuid()};
847 db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
848 }
849
850 public boolean expireOldMessages(long timestamp) {
851 String where = Message.TIME_SENT+"<?";
852 String[] whereArgs = {String.valueOf(timestamp)};
853 SQLiteDatabase db = this.getReadableDatabase();
854 return db.delete(Message.TABLENAME,where,whereArgs) > 0;
855 }
856
857 public MamReference getLastMessageReceived(Account account) {
858 Cursor cursor = null;
859 try {
860 SQLiteDatabase db = this.getReadableDatabase();
861 String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) and conversations.mode=0 order by messages.timesent desc limit 1";
862 String[] args = {account.getUuid()};
863 cursor = db.rawQuery(sql, args);
864 if (cursor.getCount() == 0) {
865 return null;
866 } else {
867 cursor.moveToFirst();
868 return new MamReference(cursor.getLong(0), cursor.getString(1));
869 }
870 } catch (Exception e) {
871 return null;
872 } finally {
873 if (cursor != null) {
874 cursor.close();
875 }
876 }
877 }
878
879 public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
880 String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
881 String[] args = {account.getUuid(), fingerprint};
882 Cursor cursor = getReadableDatabase().rawQuery(SQL,args);
883 long time;
884 if (cursor.moveToFirst()) {
885 time = cursor.getLong(0);
886 } else {
887 time = 0;
888 }
889 cursor.close();
890 return time;
891 }
892
893 public MamReference getLastClearDate(Account account) {
894 SQLiteDatabase db = this.getReadableDatabase();
895 String[] columns = {Conversation.ATTRIBUTES};
896 String selection = Conversation.ACCOUNT + "=?";
897 String[] args = {account.getUuid()};
898 Cursor cursor = db.query(Conversation.TABLENAME,columns,selection,args,null,null,null);
899 MamReference maxClearDate = new MamReference(0);
900 while (cursor.moveToNext()) {
901 try {
902 final JSONObject o = new JSONObject(cursor.getString(0));
903 maxClearDate = MamReference.max(maxClearDate, MamReference.fromAttribute(o.getString(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY)));
904 } catch (Exception e) {
905 //ignored
906 }
907 }
908 cursor.close();
909 return maxClearDate;
910 }
911
912 private Cursor getCursorForSession(Account account, SignalProtocolAddress contact) {
913 final SQLiteDatabase db = this.getReadableDatabase();
914 String[] selectionArgs = {account.getUuid(),
915 contact.getName(),
916 Integer.toString(contact.getDeviceId())};
917 return db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
918 null,
919 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
920 + SQLiteAxolotlStore.NAME + " = ? AND "
921 + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
922 selectionArgs,
923 null, null, null);
924 }
925
926 public SessionRecord loadSession(Account account, SignalProtocolAddress contact) {
927 SessionRecord session = null;
928 Cursor cursor = getCursorForSession(account, contact);
929 if (cursor.getCount() != 0) {
930 cursor.moveToFirst();
931 try {
932 session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
933 } catch (IOException e) {
934 cursor.close();
935 throw new AssertionError(e);
936 }
937 }
938 cursor.close();
939 return session;
940 }
941
942 public List<Integer> getSubDeviceSessions(Account account, SignalProtocolAddress contact) {
943 final SQLiteDatabase db = this.getReadableDatabase();
944 return getSubDeviceSessions(db, account, contact);
945 }
946
947 private List<Integer> getSubDeviceSessions(SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
948 List<Integer> devices = new ArrayList<>();
949 String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
950 String[] selectionArgs = {account.getUuid(),
951 contact.getName()};
952 Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
953 columns,
954 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
955 + SQLiteAxolotlStore.NAME + " = ?",
956 selectionArgs,
957 null, null, null);
958
959 while (cursor.moveToNext()) {
960 devices.add(cursor.getInt(
961 cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
962 }
963
964 cursor.close();
965 return devices;
966 }
967
968 public List<String> getKnownSignalAddresses(Account account) {
969 List<String> addresses = new ArrayList<>();
970 String[] colums = {"DISTINCT "+SQLiteAxolotlStore.NAME};
971 String[] selectionArgs = {account.getUuid()};
972 Cursor cursor = getReadableDatabase().query(SQLiteAxolotlStore.SESSION_TABLENAME,
973 colums,
974 SQLiteAxolotlStore.ACCOUNT + " = ?",
975 selectionArgs,
976 null,null,null
977 );
978 while (cursor.moveToNext()) {
979 addresses.add(cursor.getString(0));
980 }
981 cursor.close();
982 return addresses;
983 }
984
985 public boolean containsSession(Account account, SignalProtocolAddress contact) {
986 Cursor cursor = getCursorForSession(account, contact);
987 int count = cursor.getCount();
988 cursor.close();
989 return count != 0;
990 }
991
992 public void storeSession(Account account, SignalProtocolAddress contact, SessionRecord session) {
993 SQLiteDatabase db = this.getWritableDatabase();
994 ContentValues values = new ContentValues();
995 values.put(SQLiteAxolotlStore.NAME, contact.getName());
996 values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
997 values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT));
998 values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
999 db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
1000 }
1001
1002 public void deleteSession(Account account, SignalProtocolAddress contact) {
1003 SQLiteDatabase db = this.getWritableDatabase();
1004 deleteSession(db, account, contact);
1005 }
1006
1007 private void deleteSession(SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
1008 String[] args = {account.getUuid(),
1009 contact.getName(),
1010 Integer.toString(contact.getDeviceId())};
1011 db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
1012 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
1013 + SQLiteAxolotlStore.NAME + " = ? AND "
1014 + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
1015 args);
1016 }
1017
1018 public void deleteAllSessions(Account account, SignalProtocolAddress contact) {
1019 SQLiteDatabase db = this.getWritableDatabase();
1020 String[] args = {account.getUuid(), contact.getName()};
1021 db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
1022 SQLiteAxolotlStore.ACCOUNT + "=? AND "
1023 + SQLiteAxolotlStore.NAME + " = ?",
1024 args);
1025 }
1026
1027 private Cursor getCursorForPreKey(Account account, int preKeyId) {
1028 SQLiteDatabase db = this.getReadableDatabase();
1029 String[] columns = {SQLiteAxolotlStore.KEY};
1030 String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
1031 Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME,
1032 columns,
1033 SQLiteAxolotlStore.ACCOUNT + "=? AND "
1034 + SQLiteAxolotlStore.ID + "=?",
1035 selectionArgs,
1036 null, null, null);
1037
1038 return cursor;
1039 }
1040
1041 public PreKeyRecord loadPreKey(Account account, int preKeyId) {
1042 PreKeyRecord record = null;
1043 Cursor cursor = getCursorForPreKey(account, preKeyId);
1044 if (cursor.getCount() != 0) {
1045 cursor.moveToFirst();
1046 try {
1047 record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
1048 } catch (IOException e) {
1049 throw new AssertionError(e);
1050 }
1051 }
1052 cursor.close();
1053 return record;
1054 }
1055
1056 public boolean containsPreKey(Account account, int preKeyId) {
1057 Cursor cursor = getCursorForPreKey(account, preKeyId);
1058 int count = cursor.getCount();
1059 cursor.close();
1060 return count != 0;
1061 }
1062
1063 public void storePreKey(Account account, PreKeyRecord record) {
1064 SQLiteDatabase db = this.getWritableDatabase();
1065 ContentValues values = new ContentValues();
1066 values.put(SQLiteAxolotlStore.ID, record.getId());
1067 values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
1068 values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
1069 db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
1070 }
1071
1072 public void deletePreKey(Account account, int preKeyId) {
1073 SQLiteDatabase db = this.getWritableDatabase();
1074 String[] args = {account.getUuid(), Integer.toString(preKeyId)};
1075 db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
1076 SQLiteAxolotlStore.ACCOUNT + "=? AND "
1077 + SQLiteAxolotlStore.ID + "=?",
1078 args);
1079 }
1080
1081 private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
1082 SQLiteDatabase db = this.getReadableDatabase();
1083 String[] columns = {SQLiteAxolotlStore.KEY};
1084 String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
1085 Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
1086 columns,
1087 SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
1088 selectionArgs,
1089 null, null, null);
1090
1091 return cursor;
1092 }
1093
1094 public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
1095 SignedPreKeyRecord record = null;
1096 Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
1097 if (cursor.getCount() != 0) {
1098 cursor.moveToFirst();
1099 try {
1100 record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
1101 } catch (IOException e) {
1102 throw new AssertionError(e);
1103 }
1104 }
1105 cursor.close();
1106 return record;
1107 }
1108
1109 public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
1110 List<SignedPreKeyRecord> prekeys = new ArrayList<>();
1111 SQLiteDatabase db = this.getReadableDatabase();
1112 String[] columns = {SQLiteAxolotlStore.KEY};
1113 String[] selectionArgs = {account.getUuid()};
1114 Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
1115 columns,
1116 SQLiteAxolotlStore.ACCOUNT + "=?",
1117 selectionArgs,
1118 null, null, null);
1119
1120 while (cursor.moveToNext()) {
1121 try {
1122 prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
1123 } catch (IOException ignored) {
1124 }
1125 }
1126 cursor.close();
1127 return prekeys;
1128 }
1129
1130 public int getSignedPreKeysCount(Account account) {
1131 String[] columns = {"count("+SQLiteAxolotlStore.KEY+")"};
1132 String[] selectionArgs = {account.getUuid()};
1133 SQLiteDatabase db = this.getReadableDatabase();
1134 Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
1135 columns,
1136 SQLiteAxolotlStore.ACCOUNT + "=?",
1137 selectionArgs,
1138 null, null, null);
1139 final int count;
1140 if (cursor.moveToFirst()) {
1141 count = cursor.getInt(0);
1142 } else {
1143 count = 0;
1144 }
1145 cursor.close();
1146 return count;
1147 }
1148
1149 public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
1150 Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
1151 int count = cursor.getCount();
1152 cursor.close();
1153 return count != 0;
1154 }
1155
1156 public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
1157 SQLiteDatabase db = this.getWritableDatabase();
1158 ContentValues values = new ContentValues();
1159 values.put(SQLiteAxolotlStore.ID, record.getId());
1160 values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
1161 values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
1162 db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
1163 }
1164
1165 public void deleteSignedPreKey(Account account, int signedPreKeyId) {
1166 SQLiteDatabase db = this.getWritableDatabase();
1167 String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
1168 db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
1169 SQLiteAxolotlStore.ACCOUNT + "=? AND "
1170 + SQLiteAxolotlStore.ID + "=?",
1171 args);
1172 }
1173
1174 private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
1175 final SQLiteDatabase db = this.getReadableDatabase();
1176 return getIdentityKeyCursor(db, account, name, own);
1177 }
1178
1179 private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) {
1180 return getIdentityKeyCursor(db, account, name, own, null);
1181 }
1182
1183 private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
1184 final SQLiteDatabase db = this.getReadableDatabase();
1185 return getIdentityKeyCursor(db, account, fingerprint);
1186 }
1187
1188 private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) {
1189 return getIdentityKeyCursor(db, account, null, null, fingerprint);
1190 }
1191
1192 private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
1193 String[] columns = {SQLiteAxolotlStore.TRUST,
1194 SQLiteAxolotlStore.ACTIVE,
1195 SQLiteAxolotlStore.LAST_ACTIVATION,
1196 SQLiteAxolotlStore.KEY};
1197 ArrayList<String> selectionArgs = new ArrayList<>(4);
1198 selectionArgs.add(account.getUuid());
1199 String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
1200 if (name != null) {
1201 selectionArgs.add(name);
1202 selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
1203 }
1204 if (fingerprint != null) {
1205 selectionArgs.add(fingerprint);
1206 selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
1207 }
1208 if (own != null) {
1209 selectionArgs.add(own ? "1" : "0");
1210 selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
1211 }
1212 Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
1213 columns,
1214 selectionString,
1215 selectionArgs.toArray(new String[selectionArgs.size()]),
1216 null, null, null);
1217
1218 return cursor;
1219 }
1220
1221 public IdentityKeyPair loadOwnIdentityKeyPair(Account account) {
1222 SQLiteDatabase db = getReadableDatabase();
1223 return loadOwnIdentityKeyPair(db, account);
1224 }
1225
1226 private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
1227 String name = account.getJid().toBareJid().toPreppedString();
1228 IdentityKeyPair identityKeyPair = null;
1229 Cursor cursor = getIdentityKeyCursor(db, account, name, true);
1230 if (cursor.getCount() != 0) {
1231 cursor.moveToFirst();
1232 try {
1233 identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT));
1234 } catch (InvalidKeyException e) {
1235 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
1236 }
1237 }
1238 cursor.close();
1239
1240 return identityKeyPair;
1241 }
1242
1243 public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
1244 return loadIdentityKeys(account, name, null);
1245 }
1246
1247 public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) {
1248 Set<IdentityKey> identityKeys = new HashSet<>();
1249 Cursor cursor = getIdentityKeyCursor(account, name, false);
1250
1251 while (cursor.moveToNext()) {
1252 if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
1253 continue;
1254 }
1255 try {
1256 String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
1257 if (key != null) {
1258 identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
1259 } else {
1260 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
1261 }
1262 } catch (InvalidKeyException e) {
1263 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
1264 }
1265 }
1266 cursor.close();
1267
1268 return identityKeys;
1269 }
1270
1271 public long numTrustedKeys(Account account, String name) {
1272 SQLiteDatabase db = getReadableDatabase();
1273 String[] args = {
1274 account.getUuid(),
1275 name,
1276 FingerprintStatus.Trust.TRUSTED.toString(),
1277 FingerprintStatus.Trust.VERIFIED.toString(),
1278 FingerprintStatus.Trust.VERIFIED_X509.toString()
1279 };
1280 return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
1281 SQLiteAxolotlStore.ACCOUNT + " = ?"
1282 + " AND " + SQLiteAxolotlStore.NAME + " = ?"
1283 + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)"
1284 + " AND " +SQLiteAxolotlStore.ACTIVE + " > 0",
1285 args
1286 );
1287 }
1288
1289 private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) {
1290 SQLiteDatabase db = this.getWritableDatabase();
1291 ContentValues values = new ContentValues();
1292 values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
1293 values.put(SQLiteAxolotlStore.NAME, name);
1294 values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
1295 values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
1296 values.put(SQLiteAxolotlStore.KEY, base64Serialized);
1297 values.putAll(status.toContentValues());
1298 String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
1299 String[] whereArgs = {account.getUuid(),name,fingerprint};
1300 int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
1301 if (rows == 0) {
1302 db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
1303 }
1304 }
1305
1306 public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
1307 SQLiteDatabase db = this.getWritableDatabase();
1308 ContentValues values = new ContentValues();
1309 values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
1310 values.put(SQLiteAxolotlStore.NAME, name);
1311 values.put(SQLiteAxolotlStore.OWN, 0);
1312 values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
1313 values.putAll(status.toContentValues());
1314 db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
1315 }
1316
1317 public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
1318 Cursor cursor = getIdentityKeyCursor(account, fingerprint);
1319 final FingerprintStatus status;
1320 if (cursor.getCount() > 0) {
1321 cursor.moveToFirst();
1322 status = FingerprintStatus.fromCursor(cursor);
1323 } else {
1324 status = null;
1325 }
1326 cursor.close();
1327 return status;
1328 }
1329
1330 public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
1331 SQLiteDatabase db = this.getWritableDatabase();
1332 return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
1333 }
1334
1335 private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
1336 String[] selectionArgs = {
1337 account.getUuid(),
1338 fingerprint
1339 };
1340 int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(),
1341 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
1342 + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
1343 selectionArgs);
1344 return rows == 1;
1345 }
1346
1347 public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) {
1348 SQLiteDatabase db = this.getWritableDatabase();
1349 String[] selectionArgs = {
1350 account.getUuid(),
1351 fingerprint
1352 };
1353 try {
1354 ContentValues values = new ContentValues();
1355 values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded());
1356 return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
1357 SQLiteAxolotlStore.ACCOUNT + " = ? AND "
1358 + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
1359 selectionArgs) == 1;
1360 } catch (CertificateEncodingException e) {
1361 Log.d(Config.LOGTAG, "could not encode certificate");
1362 return false;
1363 }
1364 }
1365
1366 public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) {
1367 SQLiteDatabase db = this.getReadableDatabase();
1368 String[] selectionArgs = {
1369 account.getUuid(),
1370 fingerprint
1371 };
1372 String[] colums = {SQLiteAxolotlStore.CERTIFICATE};
1373 String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? ";
1374 Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null);
1375 if (cursor.getCount() < 1) {
1376 return null;
1377 } else {
1378 cursor.moveToFirst();
1379 byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
1380 cursor.close();
1381 if (certificate == null || certificate.length == 0) {
1382 return null;
1383 }
1384 try {
1385 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
1386 return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate));
1387 } catch (CertificateException e) {
1388 Log.d(Config.LOGTAG,"certificate exception "+e.getMessage());
1389 return null;
1390 }
1391 }
1392 }
1393
1394 public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
1395 storeIdentityKey(account, name, false, CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status);
1396 }
1397
1398 public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
1399 storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false));
1400 }
1401
1402
1403 private void recreateAxolotlDb(SQLiteDatabase db) {
1404 Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
1405 db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
1406 db.execSQL(CREATE_SESSIONS_STATEMENT);
1407 db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
1408 db.execSQL(CREATE_PREKEYS_STATEMENT);
1409 db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
1410 db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
1411 db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
1412 db.execSQL(CREATE_IDENTITIES_STATEMENT);
1413 }
1414
1415 public void wipeAxolotlDb(Account account) {
1416 String accountName = account.getUuid();
1417 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
1418 SQLiteDatabase db = this.getWritableDatabase();
1419 String[] deleteArgs = {
1420 accountName
1421 };
1422 db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
1423 SQLiteAxolotlStore.ACCOUNT + " = ?",
1424 deleteArgs);
1425 db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
1426 SQLiteAxolotlStore.ACCOUNT + " = ?",
1427 deleteArgs);
1428 db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
1429 SQLiteAxolotlStore.ACCOUNT + " = ?",
1430 deleteArgs);
1431 db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
1432 SQLiteAxolotlStore.ACCOUNT + " = ?",
1433 deleteArgs);
1434 }
1435
1436 public List<ShortcutService.FrequentContact> getFrequentContacts(int days) {
1437 SQLiteDatabase db = this.getReadableDatabase();
1438 final String SQL = "select "+Conversation.TABLENAME+"."+Conversation.ACCOUNT+","+Conversation.TABLENAME+"."+Conversation.CONTACTJID+" from "+Conversation.TABLENAME+" join "+Message.TABLENAME+" on conversations.uuid=messages.conversationUuid where messages.status!=0 and carbon==0 and conversations.mode=0 and messages.timeSent>=? group by conversations.uuid order by count(body) desc limit 4;";
1439 String[] whereArgs = new String[]{String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))};
1440 Cursor cursor = db.rawQuery(SQL,whereArgs);
1441 ArrayList<ShortcutService.FrequentContact> contacts = new ArrayList<>();
1442 while(cursor.moveToNext()) {
1443 try {
1444 contacts.add(new ShortcutService.FrequentContact(cursor.getString(0), Jid.fromString(cursor.getString(1))));
1445 } catch (Exception e) {
1446 Log.d(Config.LOGTAG,e.getMessage());
1447 }
1448 }
1449 cursor.close();
1450 return contacts;
1451 }
1452}