package org.thoughtcrime.securesms.database.helpers; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.SystemClock; import android.text.TextUtils; import androidx.annotation.NonNull; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteOpenHelper; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.SessionDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.loki.api.opengroups.PublicChat; import java.io.File; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @SuppressWarnings("unused") private static final String TAG = SQLCipherOpenHelper.class.getSimpleName(); private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2; private static final int MIGRATE_PREKEYS_VERSION = 3; private static final int MIGRATE_SESSIONS_VERSION = 4; private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5; private static final int ATTACHMENT_DIMENSIONS = 6; private static final int QUOTED_REPLIES = 7; private static final int SHARED_CONTACTS = 8; private static final int FULL_TEXT_SEARCH = 9; private static final int BAD_IMPORT_CLEANUP = 10; private static final int QUOTE_MISSING = 11; private static final int NOTIFICATION_CHANNELS = 12; private static final int SECRET_SENDER = 13; private static final int ATTACHMENT_CAPTIONS = 14; private static final int ATTACHMENT_CAPTIONS_FIX = 15; private static final int PREVIEWS = 16; private static final int CONVERSATION_SEARCH = 17; private static final int SELF_ATTACHMENT_CLEANUP = 18; private static final int RECIPIENT_FORCE_SMS_SELECTION = 19; private static final int JOBMANAGER_STRIKES_BACK = 20; private static final int STICKERS = 21; private static final int lokiV1 = 22; private static final int lokiV2 = 23; private static final int lokiV3 = 24; private static final int lokiV4 = 25; private static final int lokiV5 = 26; private static final int lokiV6 = 27; private static final int lokiV7 = 28; private static final int lokiV8 = 29; private static final int lokiV9 = 30; private static final int lokiV10 = 31; private static final int lokiV11 = 32; private static final int lokiV12 = 33; private static final int lokiV13 = 34; private static final int lokiV14_BACKUP_FILES = 35; private static final int lokiV15 = 36; private static final int lokiV16 = 37; private static final int lokiV17 = 38; private static final int lokiV18_CLEAR_BG_POLL_JOBS = 39; private static final int DATABASE_VERSION = lokiV18_CLEAR_BG_POLL_JOBS; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; private final DatabaseSecret databaseSecret; public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() { @Override public void preKey(SQLiteDatabase db) { db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;"); db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;"); } @Override public void postKey(SQLiteDatabase db) { db.rawExecSQL("PRAGMA kdf_iter = '1';"); db.rawExecSQL("PRAGMA cipher_page_size = 4096;"); } }); this.context = context.getApplicationContext(); this.databaseSecret = databaseSecret; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SmsDatabase.CREATE_TABLE); db.execSQL(MmsDatabase.CREATE_TABLE); db.execSQL(AttachmentDatabase.CREATE_TABLE); db.execSQL(ThreadDatabase.CREATE_TABLE); db.execSQL(IdentityDatabase.CREATE_TABLE); db.execSQL(DraftDatabase.CREATE_TABLE); db.execSQL(PushDatabase.CREATE_TABLE); db.execSQL(GroupDatabase.CREATE_TABLE); db.execSQL(RecipientDatabase.CREATE_TABLE); db.execSQL(GroupReceiptDatabase.CREATE_TABLE); db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE); db.execSQL(SignedPreKeyDatabase.CREATE_TABLE); db.execSQL(SessionDatabase.CREATE_TABLE); for (String sql : SearchDatabase.CREATE_TABLE) { db.execSQL(sql); } for (String sql : JobDatabase.CREATE_TABLE) { db.execSQL(sql); } db.execSQL(StickerDatabase.CREATE_TABLE); db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable3Command()); db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand()); db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand()); db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateCurrentClosedGroupRatchetTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, AttachmentDatabase.CREATE_INDEXS); executeStatements(db, ThreadDatabase.CREATE_INDEXS); executeStatements(db, DraftDatabase.CREATE_INDEXS); executeStatements(db, GroupDatabase.CREATE_INDEXS); executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES); executeStatements(db, StickerDatabase.CREATE_INDEXES); if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) { ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context); android.database.sqlite.SQLiteDatabase legacyDb = legacyHelper.getWritableDatabase(); SQLCipherMigrationHelper.migratePlaintext(context, legacyDb, db); MasterSecret masterSecret = KeyCachingService.getMasterSecret(context); if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null); else TextSecurePreferences.setNeedsSqlCipherMigration(context, true); if (!PreKeyMigrationHelper.migratePreKeys(context, db)) { ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob()); } SessionStoreMigrationHelper.migrateSessions(context, db); PreKeyMigrationHelper.cleanUpPreKeys(context); } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); // Loki - Enable write ahead logging mode and increase the cache size. // This should be disabled if we ever run into serious race condition bugs. db.enableWriteAheadLogging(); db.execSQL("PRAGMA cache_size = 10000"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i(TAG, "Upgrading database: " + oldVersion + ", " + newVersion); db.beginTransaction(); try { if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId()); } if (oldVersion < MIGRATE_PREKEYS_VERSION) { db.execSQL("CREATE TABLE signed_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL, signature TEXT NOT NULL, timestamp INTEGER DEFAULT 0)"); db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)"); if (!PreKeyMigrationHelper.migratePreKeys(context, db)) { ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob()); } } if (oldVersion < MIGRATE_SESSIONS_VERSION) { db.execSQL("CREATE TABLE sessions (_id INTEGER PRIMARY KEY, address TEXT NOT NULL, device INTEGER NOT NULL, record BLOB NOT NULL, UNIQUE(address, device) ON CONFLICT REPLACE)"); SessionStoreMigrationHelper.migrateSessions(context, db); } if (oldVersion < NO_MORE_IMAGE_THUMBNAILS_VERSION) { ContentValues update = new ContentValues(); update.put("thumbnail", (String)null); update.put("aspect_ratio", (String)null); update.put("thumbnail_random", (String)null); try (Cursor cursor = db.query("part", new String[] {"_id", "ct", "thumbnail"}, "thumbnail IS NOT NULL", null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); String contentType = cursor.getString(cursor.getColumnIndexOrThrow("ct")); if (contentType != null && !contentType.startsWith("video")) { String thumbnailPath = cursor.getString(cursor.getColumnIndexOrThrow("thumbnail")); File thumbnailFile = new File(thumbnailPath); thumbnailFile.delete(); db.update("part", update, "_id = ?", new String[] {String.valueOf(id)}); } } } } if (oldVersion < ATTACHMENT_DIMENSIONS) { db.execSQL("ALTER TABLE part ADD COLUMN width INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE part ADD COLUMN height INTEGER DEFAULT 0"); } if (oldVersion < QUOTED_REPLIES) { db.execSQL("ALTER TABLE mms ADD COLUMN quote_id INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_author TEXT"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_body TEXT"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_attachment INTEGER DEFAULT -1"); db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0"); } if (oldVersion < SHARED_CONTACTS) { db.execSQL("ALTER TABLE mms ADD COLUMN shared_contacts TEXT"); } if (oldVersion < FULL_TEXT_SEARCH) { db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, content=sms, content_rowid=_id)"); db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" + " INSERT INTO sms_fts(rowid, body) VALUES (new._id, new.body);\n" + "END;"); db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" + " INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" + "END;\n"); db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" + " INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" + " INSERT INTO sms_fts(rowid, body) VALUES(new._id, new.body);\n" + "END;"); db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, content=mms, content_rowid=_id)"); db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" + " INSERT INTO mms_fts(rowid, body) VALUES (new._id, new.body);\n" + "END;"); db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" + " INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" + "END;\n"); db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" + " INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" + " INSERT INTO mms_fts(rowid, body) VALUES(new._id, new.body);\n" + "END;"); Log.i(TAG, "Beginning to build search index."); long start = SystemClock.elapsedRealtime(); db.execSQL("INSERT INTO sms_fts (rowid, body) SELECT _id, body FROM sms"); long smsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms"); db.execSQL("INSERT INTO mms_fts (rowid, body) SELECT _id, body FROM mms"); long mmsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms"); Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms"); } if (oldVersion < BAD_IMPORT_CLEANUP) { String trimmedCondition = " NOT IN (SELECT _id FROM mms)"; db.delete("group_receipts", "mms_id" + trimmedCondition, null); String[] columns = new String[] { "_id", "unique_id", "_data", "thumbnail"}; try (Cursor cursor = db.query("part", columns, "mid" + trimmedCondition, null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { db.delete("part", "_id = ? AND unique_id = ?", new String[] { String.valueOf(cursor.getLong(0)), String.valueOf(cursor.getLong(1)) }); String data = cursor.getString(2); String thumbnail = cursor.getString(3); if (!TextUtils.isEmpty(data)) { new File(data).delete(); } if (!TextUtils.isEmpty(thumbnail)) { new File(thumbnail).delete(); } } } } // Note: This column only being checked due to upgrade issues as described in #8184 if (oldVersion < QUOTE_MISSING && !columnExists(db, "mms", "quote_missing")) { db.execSQL("ALTER TABLE mms ADD COLUMN quote_missing INTEGER DEFAULT 0"); } // Note: The column only being checked due to upgrade issues as described in #8184 if (oldVersion < NOTIFICATION_CHANNELS && !columnExists(db, "recipient_preferences", "notification_channel")) { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN notification_channel TEXT DEFAULT NULL"); NotificationChannels.create(context); try (Cursor cursor = db.rawQuery("SELECT recipient_ids, system_display_name, signal_profile_name, notification, vibrate FROM recipient_preferences WHERE notification NOT NULL OR vibrate != 0", null)) { while (cursor != null && cursor.moveToNext()) { String addressString = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids")); Address address = Address.fromExternal(context, addressString); String systemName = cursor.getString(cursor.getColumnIndexOrThrow("system_display_name")); String profileName = cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_name")); String messageSound = cursor.getString(cursor.getColumnIndexOrThrow("notification")); Uri messageSoundUri = messageSound != null ? Uri.parse(messageSound) : null; int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow("vibrate")); String displayName = NotificationChannels.getChannelDisplayNameFor(context, systemName, profileName, address); boolean vibrateEnabled = vibrateState == 0 ? TextSecurePreferences.isNotificationVibrateEnabled(context) : vibrateState == 1; if (address.isGroup()) { try(Cursor groupCursor = db.rawQuery("SELECT title FROM groups WHERE group_id = ?", new String[] { address.toGroupString() })) { if (groupCursor != null && groupCursor.moveToFirst()) { String title = groupCursor.getString(groupCursor.getColumnIndexOrThrow("title")); if (!TextUtils.isEmpty(title)) { displayName = title; } } } } String channelId = NotificationChannels.createChannelFor(context, address, displayName, messageSoundUri, vibrateEnabled); ContentValues values = new ContentValues(1); values.put("notification_channel", channelId); db.update("recipient_preferences", values, "recipient_ids = ?", new String[] { addressString }); } } } if (oldVersion < SECRET_SENDER) { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN unidentified_access_mode INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE push ADD COLUMN server_timestamp INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE push ADD COLUMN server_guid TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE group_receipts ADD COLUMN unidentified INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE mms ADD COLUMN unidentified INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE sms ADD COLUMN unidentified INTEGER DEFAULT 0"); } if (oldVersion < ATTACHMENT_CAPTIONS) { db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL"); } // 4.30.8 included a migration, but not a correct CREATE_TABLE statement, so we need to add // this column if it isn't present. if (oldVersion < ATTACHMENT_CAPTIONS_FIX) { if (!columnExists(db, "part", "caption")) { db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL"); } } if (oldVersion < PREVIEWS) { db.execSQL("ALTER TABLE mms ADD COLUMN previews TEXT"); } if (oldVersion < CONVERSATION_SEARCH) { db.execSQL("DROP TABLE sms_fts"); db.execSQL("DROP TABLE mms_fts"); db.execSQL("DROP TRIGGER sms_ai"); db.execSQL("DROP TRIGGER sms_au"); db.execSQL("DROP TRIGGER sms_ad"); db.execSQL("DROP TRIGGER mms_ai"); db.execSQL("DROP TRIGGER mms_au"); db.execSQL("DROP TRIGGER mms_ad"); db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, thread_id UNINDEXED, content=sms, content_rowid=_id)"); db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" + " INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" + "END;"); db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" + " INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" + "END;\n"); db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" + " INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" + " INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" + "END;"); db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, thread_id UNINDEXED, content=mms, content_rowid=_id)"); db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" + " INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" + "END;"); db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" + " INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" + "END;\n"); db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" + " INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" + " INSERT INTO mms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" + "END;"); Log.i(TAG, "Beginning to build search index."); long start = SystemClock.elapsedRealtime(); db.execSQL("INSERT INTO sms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM sms"); long smsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms"); db.execSQL("INSERT INTO mms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM mms"); long mmsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms"); Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms"); } if (oldVersion < SELF_ATTACHMENT_CLEANUP) { String localNumber = TextSecurePreferences.getLocalNumber(context); if (!TextUtils.isEmpty(localNumber)) { try (Cursor threadCursor = db.rawQuery("SELECT _id FROM thread WHERE recipient_ids = ?", new String[]{ localNumber })) { if (threadCursor != null && threadCursor.moveToFirst()) { long threadId = threadCursor.getLong(0); ContentValues updateValues = new ContentValues(1); updateValues.put("pending_push", 0); int count = db.update("part", updateValues, "mid IN (SELECT _id FROM mms WHERE thread_id = ?)", new String[]{ String.valueOf(threadId) }); Log.i(TAG, "Updated " + count + " self-sent attachments."); } } } } if (oldVersion < RECIPIENT_FORCE_SMS_SELECTION) { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN force_sms_selection INTEGER DEFAULT 0"); } if (oldVersion < JOBMANAGER_STRIKES_BACK) { db.execSQL("CREATE TABLE job_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "job_spec_id TEXT UNIQUE, " + "factory_key TEXT, " + "queue_key TEXT, " + "create_time INTEGER, " + "next_run_attempt_time INTEGER, " + "run_attempt INTEGER, " + "max_attempts INTEGER, " + "max_backoff INTEGER, " + "max_instances INTEGER, " + "lifespan INTEGER, " + "serialized_data TEXT, " + "is_running INTEGER)"); db.execSQL("CREATE TABLE constraint_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "job_spec_id TEXT, " + "factory_key TEXT, " + "UNIQUE(job_spec_id, factory_key))"); db.execSQL("CREATE TABLE dependency_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "job_spec_id TEXT, " + "depends_on_job_spec_id TEXT, " + "UNIQUE(job_spec_id, depends_on_job_spec_id))"); } if (oldVersion < STICKERS) { db.execSQL("CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "pack_id TEXT NOT NULL, " + "pack_key TEXT NOT NULL, " + "pack_title TEXT NOT NULL, " + "pack_author TEXT NOT NULL, " + "sticker_id INTEGER, " + "cover INTEGER, " + "emoji TEXT NOT NULL, " + "last_used INTEGER, " + "installed INTEGER," + "file_path TEXT NOT NULL, " + "file_length INTEGER, " + "file_random BLOB, " + "UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)"); db.execSQL("CREATE INDEX IF NOT EXISTS sticker_pack_id_index ON sticker (pack_id);"); db.execSQL("CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON sticker (sticker_id);"); db.execSQL("ALTER TABLE part ADD COLUMN sticker_pack_id TEXT"); db.execSQL("ALTER TABLE part ADD COLUMN sticker_pack_key TEXT"); db.execSQL("ALTER TABLE part ADD COLUMN sticker_id INTEGER DEFAULT -1"); db.execSQL("CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON part (sticker_pack_id)"); } if (oldVersion < lokiV1) { db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); } if (oldVersion < lokiV2) { db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); } if (oldVersion < lokiV3) { db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand()); db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL("ALTER TABLE groups ADD COLUMN avatar_url TEXT"); db.execSQL("ALTER TABLE part ADD COLUMN url TEXT"); } if (oldVersion < lokiV4) { db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand()); } if (oldVersion < lokiV5) { db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); } if (oldVersion < lokiV6) { // Migrate public chats from __textsecure_group__ to __loki_public_chat_group__ try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) { while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) { String chatString = lokiPublicChatCursor.getString(0); PublicChat publicChat = PublicChat.fromJSON(chatString); if (publicChat != null) { byte[] groupId = publicChat.getId().getBytes(); String oldId = GroupUtil.getEncodedId(groupId, false); String newId = GroupUtil.getEncodedOpenGroupId(groupId); ContentValues threadUpdate = new ContentValues(); threadUpdate.put("recipient_ids", newId); db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId }); ContentValues groupUpdate = new ContentValues(); groupUpdate.put("group_id", newId); db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId }); } } } // Migrate RSS feeds from __textsecure_group__ to __loki_rss_feed_group__ String[] rssFeedIds = new String[] { "loki.network.feed", "loki.network.messenger-updates.feed" }; for (String groupId : rssFeedIds) { String oldId = GroupUtil.getEncodedId(groupId.getBytes(), false); String newId = GroupUtil.getEncodedRSSFeedId(groupId.getBytes()); ContentValues threadUpdate = new ContentValues(); threadUpdate.put("recipient_ids", newId); db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId }); ContentValues groupUpdate = new ContentValues(); groupUpdate.put("group_id", newId); db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId }); } // Add admin field in groups db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT"); } if (oldVersion < lokiV7) { db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand()); } if (oldVersion < lokiV8) { db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand()); } if (oldVersion < lokiV9) { db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); } if (oldVersion < lokiV10) { db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); } if (oldVersion < lokiV11) { db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); } if (oldVersion < lokiV12) { db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); db.execSQL(SharedSenderKeysDatabase.getCreateCurrentClosedGroupRatchetTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); } if (oldVersion < lokiV13) { db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable3Command()); } if (oldVersion < lokiV14_BACKUP_FILES) { db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); } if (oldVersion < lokiV15) { db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand()); } if (oldVersion < lokiV16) { db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); } if (oldVersion < lokiV17) { db.execSQL("ALTER TABLE part ADD COLUMN audio_visual_samples BLOB"); db.execSQL("ALTER TABLE part ADD COLUMN audio_duration INTEGER"); } if (oldVersion < lokiV18_CLEAR_BG_POLL_JOBS) { // BackgroundPollJob was replaced with BackgroundPollWorker. Clear all the scheduled job records. db.execSQL("DELETE FROM job_spec WHERE factory_key = 'BackgroundPollJob'"); db.execSQL("DELETE FROM constraint_spec WHERE factory_key = 'BackgroundPollJob'"); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } if (oldVersion < MIGRATE_PREKEYS_VERSION) { PreKeyMigrationHelper.cleanUpPreKeys(context); } } public SQLiteDatabase getReadableDatabase() { return getReadableDatabase(databaseSecret.asString()); } public SQLiteDatabase getWritableDatabase() { return getWritableDatabase(databaseSecret.asString()); } public void markCurrent(SQLiteDatabase db) { db.setVersion(DATABASE_VERSION); } private void executeStatements(SQLiteDatabase db, String[] statements) { for (String statement : statements) db.execSQL(statement); } private static boolean columnExists(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) { try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) { int nameColumnIndex = cursor.getColumnIndexOrThrow("name"); while (cursor.moveToNext()) { String name = cursor.getString(nameColumnIndex); if (name.equals(column)) { return true; } } } return false; } }