2021-11-18 17:36:52 +00:00
package org.thoughtcrime.securesms.database.helpers
import android.app.NotificationChannel
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.SystemClock
import android.preference.PreferenceManager
import android.text.TextUtils
import com.annimon.stream.Stream
import com.google.protobuf.InvalidProtocolBufferException
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.color.MaterialColor
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.CursorUtil
import org.thoughtcrime.securesms.util.FileUtils
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SqlUtil
import org.thoughtcrime.securesms.util.Stopwatch
import org.thoughtcrime.securesms.util.Triple
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.DistributionId
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.lang.AssertionError
import java.util.ArrayList
import java.util.HashSet
import java.util.LinkedList
import java.util.Locale
/ * *
* Contains all of the database migrations for [ SignalDatabase ] . Broken into a separate file for cleanliness .
* /
object SignalDatabaseMigrations {
private val TAG : String = Log . tag ( SignalDatabaseMigrations . javaClass )
private const val RECIPIENT _CALL _RINGTONE _VERSION = 2
private const val MIGRATE _PREKEYS _VERSION = 3
private const val MIGRATE _SESSIONS _VERSION = 4
private const val NO _MORE _IMAGE _THUMBNAILS _VERSION = 5
private const val ATTACHMENT _DIMENSIONS = 6
private const val QUOTED _REPLIES = 7
private const val SHARED _CONTACTS = 8
private const val FULL _TEXT _SEARCH = 9
private const val BAD _IMPORT _CLEANUP = 10
private const val QUOTE _MISSING = 11
private const val NOTIFICATION _CHANNELS = 12
private const val SECRET _SENDER = 13
private const val ATTACHMENT _CAPTIONS = 14
private const val ATTACHMENT _CAPTIONS _FIX = 15
private const val PREVIEWS = 16
private const val CONVERSATION _SEARCH = 17
private const val SELF _ATTACHMENT _CLEANUP = 18
private const val RECIPIENT _FORCE _SMS _SELECTION = 19
private const val JOBMANAGER _STRIKES _BACK = 20
private const val STICKERS = 21
private const val REVEALABLE _MESSAGES = 22
private const val VIEW _ONCE _ONLY = 23
private const val RECIPIENT _IDS = 24
private const val RECIPIENT _SEARCH = 25
private const val RECIPIENT _CLEANUP = 26
private const val MMS _RECIPIENT _CLEANUP = 27
private const val ATTACHMENT _HASHING = 28
private const val NOTIFICATION _RECIPIENT _IDS = 29
private const val BLUR _HASH = 30
private const val MMS _RECIPIENT _CLEANUP _2 = 31
private const val ATTACHMENT _TRANSFORM _PROPERTIES = 32
private const val ATTACHMENT _CLEAR _HASHES = 33
private const val ATTACHMENT _CLEAR _HASHES _2 = 34
private const val UUIDS = 35
private const val USERNAMES = 36
private const val REACTIONS = 37
private const val STORAGE _SERVICE = 38
private const val REACTIONS _UNREAD _INDEX = 39
private const val RESUMABLE _DOWNLOADS = 40
private const val KEY _VALUE _STORE = 41
private const val ATTACHMENT _DISPLAY _ORDER = 42
private const val SPLIT _PROFILE _NAMES = 43
private const val STICKER _PACK _ORDER = 44
private const val MEGAPHONES = 45
private const val MEGAPHONE _FIRST _APPEARANCE = 46
private const val PROFILE _KEY _TO _DB = 47
private const val PROFILE _KEY _CREDENTIALS = 48
private const val ATTACHMENT _FILE _INDEX = 49
private const val STORAGE _SERVICE _ACTIVE = 50
private const val GROUPS _V2 _RECIPIENT _CAPABILITY = 51
private const val TRANSFER _FILE _CLEANUP = 52
private const val PROFILE _DATA _MIGRATION = 53
private const val AVATAR _LOCATION _MIGRATION = 54
private const val GROUPS _V2 = 55
private const val ATTACHMENT _UPLOAD _TIMESTAMP = 56
private const val ATTACHMENT _CDN _NUMBER = 57
private const val JOB _INPUT _DATA = 58
private const val SERVER _TIMESTAMP = 59
private const val REMOTE _DELETE = 60
private const val COLOR _MIGRATION = 61
private const val LAST _SCROLLED = 62
private const val LAST _PROFILE _FETCH = 63
private const val SERVER _DELIVERED _TIMESTAMP = 64
private const val QUOTE _CLEANUP = 65
private const val BORDERLESS = 66
private const val REMAPPED _RECORDS = 67
private const val MENTIONS = 68
private const val PINNED _CONVERSATIONS = 69
private const val MENTION _GLOBAL _SETTING _MIGRATION = 70
private const val UNKNOWN _STORAGE _FIELDS = 71
private const val STICKER _CONTENT _TYPE = 72
private const val STICKER _EMOJI _IN _NOTIFICATIONS = 73
private const val THUMBNAIL _CLEANUP = 74
private const val STICKER _CONTENT _TYPE _CLEANUP = 75
private const val MENTION _CLEANUP = 76
private const val MENTION _CLEANUP _V2 = 77
private const val REACTION _CLEANUP = 78
private const val CAPABILITIES _REFACTOR = 79
private const val GV1 _MIGRATION = 80
private const val NOTIFIED _TIMESTAMP = 81
private const val GV1 _MIGRATION _LAST _SEEN = 82
private const val VIEWED _RECEIPTS = 83
private const val CLEAN _UP _GV1 _IDS = 84
private const val GV1 _MIGRATION _REFACTOR = 85
private const val CLEAR _PROFILE _KEY _CREDENTIALS = 86
private const val LAST _RESET _SESSION _TIME = 87
private const val WALLPAPER = 88
private const val ABOUT = 89
private const val SPLIT _SYSTEM _NAMES = 90
private const val PAYMENTS = 91
private const val CLEAN _STORAGE _IDS = 92
private const val MP4 _GIF _SUPPORT = 93
private const val BLUR _AVATARS = 94
private const val CLEAN _STORAGE _IDS _WITHOUT _INFO = 95
private const val CLEAN _REACTION _NOTIFICATIONS = 96
private const val STORAGE _SERVICE _REFACTOR = 97
private const val CLEAR _MMS _STORAGE _IDS = 98
private const val SERVER _GUID = 99
private const val CHAT _COLORS = 100
private const val AVATAR _COLORS = 101
private const val EMOJI _SEARCH = 102
private const val SENDER _KEY = 103
private const val MESSAGE _DUPE _INDEX = 104
private const val MESSAGE _LOG = 105
private const val MESSAGE _LOG _2 = 106
private const val ABANDONED _MESSAGE _CLEANUP = 107
private const val THREAD _AUTOINCREMENT = 108
private const val MMS _AUTOINCREMENT = 109
private const val ABANDONED _ATTACHMENT _CLEANUP = 110
private const val AVATAR _PICKER = 111
private const val THREAD _CLEANUP = 112
private const val SESSION _MIGRATION = 113
private const val IDENTITY _MIGRATION = 114
private const val GROUP _CALL _RING _TABLE = 115
private const val CLEANUP _SESSION _MIGRATION = 116
private const val RECEIPT _TIMESTAMP = 117
private const val BADGES = 118
private const val SENDER _KEY _UUID = 119
private const val SENDER _KEY _SHARED _TIMESTAMP = 120
private const val REACTION _REFACTOR = 121
2021-12-06 17:18:42 +00:00
private const val PNI = 122
2021-12-08 18:22:36 +00:00
private const val NOTIFICATION _PROFILES = 123
2021-12-20 18:16:31 +00:00
private const val NOTIFICATION _PROFILES _END _FIX = 124
2022-01-18 22:08:38 +00:00
private const val REACTION _BACKUP _CLEANUP = 125
2022-01-19 16:12:07 +00:00
private const val REACTION _REMOTE _DELETE _CLEANUP = 126
2022-01-28 17:16:30 +00:00
private const val PNI _CLEANUP = 127
2021-11-18 17:36:52 +00:00
2022-01-28 17:16:30 +00:00
const val DATABASE _VERSION = 127
2021-11-18 17:36:52 +00:00
@JvmStatic
fun migrate ( context : Context , db : SQLiteDatabase , oldVersion : Int , newVersion : Int ) {
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 . id )
}
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 ) ) {
ApplicationDependencies . getJobManager ( ) . add ( 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 ) {
val update = ContentValues ( ) . apply {
put ( " thumbnail " , null as String ? )
put ( " aspect_ratio " , null as String ? )
put ( " thumbnail_random " , null as String ? )
}
db . query ( " part " , arrayOf ( " _id " , " ct " , " thumbnail " ) , " thumbnail IS NOT NULL " , null , null , null , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val id : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) )
val contentType : String ? = cursor . getString ( cursor . getColumnIndexOrThrow ( " ct " ) )
if ( contentType != null && ! contentType . startsWith ( " video " ) ) {
val thumbnailPath : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " thumbnail " ) )
val thumbnailFile : File = File ( thumbnailPath )
thumbnailFile . delete ( )
db . update ( " part " , update , " _id = ? " , arrayOf ( id . toString ( ) ) )
}
}
}
}
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 ) {
// language=text
db . execSQL ( " CREATE VIRTUAL TABLE sms_fts USING fts5(body, content=sms, content_rowid=_id) " )
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _ai AFTER INSERT ON sms BEGIN
INSERT INTO sms _fts ( rowid , body ) VALUES ( new . _id , new . body ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _ad AFTER DELETE ON sms BEGIN
INSERT INTO sms _fts ( sms _fts , rowid , body ) VALUES ( ' delete ' , old . _id , old . body ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _au AFTER UPDATE ON sms BEGIN
INSERT INTO sms _fts ( sms _fts , rowid , body ) VALUES ( ' delete ' , old . _id , old . body ) ;
INSERT INTO sms _fts ( rowid , body ) VALUES ( new . _id , new . body ) ;
END ;
""" .trimIndent()
)
// language=text
db . execSQL ( " CREATE VIRTUAL TABLE mms_fts USING fts5(body, content=mms, content_rowid=_id) " )
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _ai AFTER INSERT ON mms BEGIN
INSERT INTO mms _fts ( rowid , body ) VALUES ( new . _id , new . body ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _ad AFTER DELETE ON mms BEGIN
INSERT INTO mms _fts ( mms _fts , rowid , body ) VALUES ( ' delete ' , old . _id , old . body ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _au AFTER UPDATE ON mms BEGIN
INSERT INTO mms _fts ( mms _fts , rowid , body ) VALUES ( ' delete ' , old . _id , old . body ) ;
INSERT INTO mms _fts ( rowid , body ) VALUES ( new . _id , new . body ) ;
END ;
""" .trimIndent()
)
Log . i ( TAG , " Beginning to build search index. " )
val start = SystemClock . elapsedRealtime ( )
db . execSQL ( " INSERT INTO sms_fts (rowid, body) SELECT _id, body FROM sms " )
val 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 " )
val 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 ) {
val trimmedCondition = " NOT IN (SELECT _id FROM mms) "
db . delete ( " group_receipts " , " mms_id $trimmedCondition " , null )
val columns = arrayOf ( " _id " , " unique_id " , " _data " , " thumbnail " )
db . query ( " part " , columns , " mid $trimmedCondition " , null , null , null , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
db . delete ( " part " , " _id = ? AND unique_id = ? " , arrayOf ( cursor . getLong ( 0 ) . toString ( ) , cursor . getLong ( 1 ) . toString ( ) ) )
val data : String = cursor . getString ( 2 )
val thumbnail : String = cursor . getString ( 3 )
if ( ! TextUtils . isEmpty ( data ) ) {
File ( data ) . delete ( )
}
if ( ! TextUtils . isEmpty ( thumbnail ) ) {
File ( thumbnail ) . delete ( )
}
}
}
}
// Note: This column only being checked due to upgrade issues as described in #8184
if ( oldVersion < QUOTE _MISSING && ! SqlUtil . 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 && ! SqlUtil . columnExists ( db , " recipient_preferences " , " notification_channel " ) ) {
db . execSQL ( " ALTER TABLE recipient_preferences ADD COLUMN notification_channel TEXT DEFAULT NULL " )
NotificationChannels . create ( context )
db . rawQuery ( " SELECT recipient_ids, system_display_name, signal_profile_name, notification, vibrate FROM recipient_preferences WHERE notification NOT NULL OR vibrate != 0 " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val rawAddress : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " recipient_ids " ) )
val address : String = PhoneNumberFormatter . get ( context ) . format ( rawAddress )
val systemName : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " system_display_name " ) )
val profileName : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " signal_profile_name " ) )
val messageSound : String ? = cursor . getString ( cursor . getColumnIndexOrThrow ( " notification " ) )
val messageSoundUri : Uri ? = if ( messageSound != null ) Uri . parse ( messageSound ) else null
val vibrateState : Int = cursor . getInt ( cursor . getColumnIndexOrThrow ( " vibrate " ) )
var displayName : String ? = NotificationChannels . getChannelDisplayNameFor ( context , systemName , profileName , null , address )
val vibrateEnabled : Boolean = if ( vibrateState == 0 ) SignalStore . settings ( ) . isMessageVibrateEnabled ( ) else vibrateState == 1
if ( GroupId . isEncodedGroup ( address ) ) {
db . rawQuery ( " SELECT title FROM groups WHERE group_id = ? " , arrayOf ( address ) ) . use { groupCursor ->
if ( groupCursor != null && groupCursor . moveToFirst ( ) ) {
val title : String = groupCursor . getString ( groupCursor . getColumnIndexOrThrow ( " title " ) )
if ( ! TextUtils . isEmpty ( title ) ) {
displayName = title
}
}
}
}
val channelId : String ? = NotificationChannels . createChannelFor ( context , " contact_ " + address + " _ " + System . currentTimeMillis ( ) , ( displayName ) !! , messageSoundUri , vibrateEnabled , null )
val values = ContentValues ( 1 ) . apply {
put ( " notification_channel " , channelId )
}
db . update ( " recipient_preferences " , values , " recipient_ids = ? " , arrayOf ( rawAddress ) )
}
}
}
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 ( ! SqlUtil . 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 " )
// language=text
db . execSQL ( " CREATE VIRTUAL TABLE sms_fts USING fts5(body, thread_id UNINDEXED, content=sms, content_rowid=_id) " )
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _ai AFTER INSERT ON sms BEGIN
INSERT INTO sms _fts ( rowid , body , thread _id ) VALUES ( new . _id , new . body , new . thread _id ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _ad AFTER DELETE ON sms BEGIN
INSERT INTO sms _fts ( sms _fts , rowid , body , thread _id ) VALUES ( ' delete ' , old . _id , old . body , old . thread _id ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER sms _au AFTER UPDATE ON sms BEGIN
INSERT INTO sms _fts ( sms _fts , rowid , body , thread _id ) VALUES ( ' delete ' , old . _id , old . body , old . thread _id ) ;
INSERT INTO sms _fts ( rowid , body , thread _id ) VALUES ( new . _id , new . body , new . thread _id ) ;
END ;
""" .trimIndent()
)
// language=text
db . execSQL ( " CREATE VIRTUAL TABLE mms_fts USING fts5(body, thread_id UNINDEXED, content=mms, content_rowid=_id) " )
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _ai AFTER INSERT ON mms BEGIN
INSERT INTO mms _fts ( rowid , body , thread _id ) VALUES ( new . _id , new . body , new . thread _id ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _ad AFTER DELETE ON mms BEGIN
INSERT INTO mms _fts ( mms _fts , rowid , body , thread _id ) VALUES ( ' delete ' , old . _id , old . body , old . thread _id ) ;
END ;
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TRIGGER mms _au AFTER UPDATE ON mms BEGIN
INSERT INTO mms _fts ( mms _fts , rowid , body , thread _id ) VALUES ( ' delete ' , old . _id , old . body , old . thread _id ) ;
INSERT INTO mms _fts ( rowid , body , thread _id ) VALUES ( new . _id , new . body , new . thread _id ) ;
END ;
""" .trimIndent()
)
Log . i ( TAG , " Beginning to build search index. " )
val start = SystemClock . elapsedRealtime ( )
db . execSQL ( " INSERT INTO sms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM sms " )
val 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 " )
val 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 ) {
2022-01-03 16:59:06 +00:00
val localNumber = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_local_number " , null )
2021-11-18 17:36:52 +00:00
if ( ! TextUtils . isEmpty ( localNumber ) ) {
db . rawQuery ( " SELECT _id FROM thread WHERE recipient_ids = ? " , arrayOf ( localNumber ) ) . use { threadCursor ->
if ( threadCursor != null && threadCursor . moveToFirst ( ) ) {
val threadId : Long = threadCursor . getLong ( 0 )
val updateValues = ContentValues ( 1 ) . apply {
put ( " pending_push " , 0 )
}
val count : Int = db . update ( " part " , updateValues , " mid IN (SELECT _id FROM mms WHERE thread_id = ?) " , arrayOf ( threadId . toString ( ) ) )
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 (
// language=sql
"""
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
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE constraint _spec (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
job _spec _id TEXT ,
factory _key TEXT ,
UNIQUE ( job _spec _id , factory _key )
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
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 )
)
""" .trimIndent()
)
}
if ( oldVersion < STICKERS ) {
db . execSQL (
// language=sql
"""
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
)
""" .trimIndent()
)
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 < REVEALABLE _MESSAGES ) {
db . execSQL ( " ALTER TABLE mms ADD COLUMN reveal_duration INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN reveal_start_time INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE thread ADD COLUMN snippet_content_type TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE thread ADD COLUMN snippet_extras TEXT DEFAULT NULL " )
}
if ( oldVersion < VIEW _ONCE _ONLY ) {
db . execSQL ( " UPDATE mms SET reveal_duration = 1 WHERE reveal_duration > 0 " )
db . execSQL ( " UPDATE mms SET reveal_start_time = 0 " )
}
if ( oldVersion < RECIPIENT _IDS ) {
RecipientIdMigrationHelper . execute ( db )
}
if ( oldVersion < RECIPIENT _SEARCH ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN system_phone_type INTEGER DEFAULT -1 " )
2022-01-03 16:59:06 +00:00
val localNumber = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_local_number " , null )
2021-11-18 17:36:52 +00:00
if ( ! TextUtils . isEmpty ( localNumber ) ) {
db . query ( " recipient " , null , " phone = ? " , arrayOf ( localNumber ) , null , null , null ) . use { cursor ->
if ( cursor == null || ! cursor . moveToFirst ( ) ) {
val values = ContentValues ( ) . apply {
put ( " phone " , localNumber )
put ( " registered " , 1 )
put ( " profile_sharing " , 1 )
}
db . insert ( " recipient " , null , values )
} else {
db . execSQL ( " UPDATE recipient SET registered = ?, profile_sharing = ? WHERE phone = ? " , arrayOf ( " 1 " , " 1 " , localNumber ) )
}
}
}
}
if ( oldVersion < RECIPIENT _CLEANUP ) {
RecipientIdCleanupHelper . execute ( db )
}
if ( oldVersion < MMS _RECIPIENT _CLEANUP ) {
val values = ContentValues ( 1 )
values . put ( " address " , " -1 " )
val count = db . update ( " mms " , values , " address = ? " , arrayOf ( " 0 " ) )
Log . i ( TAG , " MMS recipient cleanup updated $count rows. " )
}
if ( oldVersion < ATTACHMENT _HASHING ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN data_hash TEXT DEFAULT NULL " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS part_data_hash_index ON part (data_hash) " )
}
if ( oldVersion < NOTIFICATION _RECIPIENT _IDS && Build . VERSION . SDK _INT >= 26 ) {
val notificationManager = ServiceUtil . getNotificationManager ( context )
val channels = Stream . of ( notificationManager . notificationChannels )
. filter { c : NotificationChannel -> c . getId ( ) . startsWith ( " contact_ " ) }
. toList ( )
Log . i ( TAG , " Migrating " + channels . size + " channels to use RecipientId's. " )
for ( oldChannel : NotificationChannel in channels ) {
notificationManager . deleteNotificationChannel ( oldChannel . id )
val startIndex = " contact_ " . length
val endIndex = oldChannel . id . lastIndexOf ( " _ " )
val address = oldChannel . id . substring ( startIndex , endIndex )
var recipientId : String ?
db . query ( " recipient " , arrayOf ( " _id " ) , " phone = ? OR email = ? OR group_id = ? " , arrayOf ( address , address , address ) , null , null , null ) . use { cursor ->
recipientId = if ( cursor != null && cursor . moveToFirst ( ) ) {
cursor . getString ( cursor . getColumnIndexOrThrow ( " _id " ) )
} else {
Log . w ( TAG , " Couldn't find recipient for address: $address " )
null
}
}
if ( recipientId == null ) {
continue
}
val newId = " contact_ " + recipientId + " _ " + System . currentTimeMillis ( )
val newChannel = NotificationChannel ( newId , oldChannel . name , oldChannel . importance )
Log . i ( TAG , " Updating channel ID from ' " + oldChannel . id + " ' to ' " + newChannel . id + " '. " )
newChannel . group = oldChannel . group
newChannel . setSound ( oldChannel . sound , oldChannel . audioAttributes )
newChannel . setBypassDnd ( oldChannel . canBypassDnd ( ) )
newChannel . enableVibration ( oldChannel . shouldVibrate ( ) )
newChannel . vibrationPattern = oldChannel . vibrationPattern
newChannel . lockscreenVisibility = oldChannel . lockscreenVisibility
newChannel . setShowBadge ( oldChannel . canShowBadge ( ) )
newChannel . lightColor = oldChannel . lightColor
newChannel . enableLights ( oldChannel . shouldShowLights ( ) )
notificationManager . createNotificationChannel ( newChannel )
val contentValues = ContentValues ( 1 ) . apply {
put ( " notification_channel " , newChannel . id )
}
db . update ( " recipient " , contentValues , " _id = ? " , arrayOf ( recipientId ) )
}
}
if ( oldVersion < BLUR _HASH ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN blur_hash TEXT DEFAULT NULL " )
}
if ( oldVersion < MMS _RECIPIENT _CLEANUP _2 ) {
val values = ContentValues ( 1 ) . apply {
put ( " address " , " -1 " )
}
val count = db . update ( " mms " , values , " address = ? OR address IS NULL " , arrayOf ( " 0 " ) )
Log . i ( TAG , " MMS recipient cleanup 2 updated $count rows. " )
}
if ( oldVersion < ATTACHMENT _TRANSFORM _PROPERTIES ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN transform_properties TEXT DEFAULT NULL " )
}
if ( oldVersion < ATTACHMENT _CLEAR _HASHES ) {
db . execSQL ( " UPDATE part SET data_hash = null " )
}
if ( oldVersion < ATTACHMENT _CLEAR _HASHES _2 ) {
db . execSQL ( " UPDATE part SET data_hash = null " )
}
if ( oldVersion < UUIDS ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN uuid_supported INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE push ADD COLUMN source_uuid TEXT DEFAULT NULL " )
}
if ( oldVersion < USERNAMES ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN username TEXT DEFAULT NULL " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS recipient_username_index ON recipient (username) " )
}
if ( oldVersion < REACTIONS ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN reactions BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN reactions BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE sms ADD COLUMN reactions_unread INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN reactions_unread INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE sms ADD COLUMN reactions_last_seen INTEGER DEFAULT -1 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN reactions_last_seen INTEGER DEFAULT -1 " )
}
if ( oldVersion < STORAGE _SERVICE ) {
db . execSQL (
// language=sql
"""
CREATE TABLE storage _key (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
type INTEGER ,
key TEXT UNIQUE
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX IF NOT EXISTS storage_key_type_index ON storage_key (type) " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN system_info_pending INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN storage_service_key TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN dirty INTEGER DEFAULT 0 " )
db . execSQL ( " CREATE UNIQUE INDEX recipient_storage_service_key ON recipient (storage_service_key) " )
db . execSQL ( " CREATE INDEX recipient_dirty_index ON recipient (dirty) " )
}
if ( oldVersion < REACTIONS _UNREAD _INDEX ) {
db . execSQL ( " CREATE INDEX IF NOT EXISTS sms_reactions_unread_index ON sms (reactions_unread); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON mms (reactions_unread); " )
}
if ( oldVersion < RESUMABLE _DOWNLOADS ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN transfer_file TEXT DEFAULT NULL " )
}
if ( oldVersion < KEY _VALUE _STORE ) {
db . execSQL (
// language=sql
"""
CREATE TABLE key _value (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
key TEXT UNIQUE ,
value TEXT ,
type INTEGER
)
""" .trimIndent()
)
}
if ( oldVersion < ATTACHMENT _DISPLAY _ORDER ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN display_order INTEGER DEFAULT 0 " )
}
if ( oldVersion < SPLIT _PROFILE _NAMES ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN profile_family_name TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN profile_joined_name TEXT DEFAULT NULL " )
}
if ( oldVersion < STICKER _PACK _ORDER ) {
db . execSQL ( " ALTER TABLE sticker ADD COLUMN pack_order INTEGER DEFAULT 0 " )
}
if ( oldVersion < MEGAPHONES ) {
db . execSQL (
// language=sql
"""
CREATE TABLE megaphone (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
event TEXT UNIQUE ,
seen _count INTEGER ,
last _seen INTEGER ,
finished INTEGER
)
""" .trimIndent()
)
}
if ( oldVersion < MEGAPHONE _FIRST _APPEARANCE ) {
db . execSQL ( " ALTER TABLE megaphone ADD COLUMN first_visible INTEGER DEFAULT 0 " )
}
if ( oldVersion < PROFILE _KEY _TO _DB ) {
2022-01-03 16:59:06 +00:00
val localNumber = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_local_number " , null )
2021-11-18 17:36:52 +00:00
if ( ! TextUtils . isEmpty ( localNumber ) ) {
val encodedProfileKey = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_profile_key " , null )
val profileKey = if ( encodedProfileKey != null ) Base64 . decodeOrThrow ( encodedProfileKey ) else Util . getSecretBytes ( 32 )
val values = ContentValues ( 1 ) . apply {
put ( " profile_key " , Base64 . encodeBytes ( profileKey ) )
}
if ( db . update ( " recipient " , values , " phone = ? " , arrayOf ( localNumber ) ) == 0 ) {
throw AssertionError ( " No rows updated! " )
}
}
}
if ( oldVersion < PROFILE _KEY _CREDENTIALS ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN profile_key_credential TEXT DEFAULT NULL " )
}
if ( oldVersion < ATTACHMENT _FILE _INDEX ) {
db . execSQL ( " CREATE INDEX IF NOT EXISTS part_data_index ON part (_data) " )
}
if ( oldVersion < STORAGE _SERVICE _ACTIVE ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN group_type INTEGER DEFAULT 0 " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS recipient_group_type_index ON recipient (group_type) " )
db . execSQL ( " UPDATE recipient set group_type = 1 WHERE group_id NOT NULL AND group_id LIKE '__signal_mms_group__%' " )
db . execSQL ( " UPDATE recipient set group_type = 2 WHERE group_id NOT NULL AND group_id LIKE '__textsecure_group__%' " )
db . rawQuery ( " SELECT _id FROM recipient WHERE registered = 1 or group_type = 2 " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val id : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " _id " ) )
val values = ContentValues ( 2 ) . apply {
put ( " dirty " , 2 )
put ( " storage_service_key " , Base64 . encodeBytes ( StorageSyncHelper . generateKey ( ) ) )
}
db . update ( " recipient " , values , " _id = ? " , arrayOf ( id ) )
}
}
}
if ( oldVersion < GROUPS _V2 _RECIPIENT _CAPABILITY ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN gv2_capability INTEGER DEFAULT 0 " )
}
if ( oldVersion < TRANSFER _FILE _CLEANUP ) {
val partsDirectory : File = context . getDir ( " parts " , Context . MODE _PRIVATE )
if ( partsDirectory . exists ( ) ) {
val transferFiles = partsDirectory . listFiles { dir : File ? , name : String -> name . startsWith ( " transfer " ) }
var deleteCount = 0
Log . i ( TAG , " Found " + transferFiles . size + " dangling transfer files. " )
for ( file : File in transferFiles ) {
if ( file . delete ( ) ) {
Log . i ( TAG , " Deleted " + file . name )
deleteCount ++
}
}
Log . i ( TAG , " Deleted $deleteCount dangling transfer files. " )
} else {
Log . w ( TAG , " Part directory did not exist. Skipping. " )
}
}
if ( oldVersion < PROFILE _DATA _MIGRATION ) {
2022-01-03 16:59:06 +00:00
val localNumber = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_local_number " , null )
2021-11-18 17:36:52 +00:00
if ( localNumber != null ) {
val encodedProfileName = PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_profile_name " , null )
val profileName = ProfileName . fromSerialized ( encodedProfileName )
db . execSQL ( " UPDATE recipient SET signal_profile_name = ?, profile_family_name = ?, profile_joined_name = ? WHERE phone = ? " , arrayOf ( profileName . givenName , profileName . familyName , profileName . toString ( ) , localNumber ) )
}
}
if ( oldVersion < AVATAR _LOCATION _MIGRATION ) {
val oldAvatarDirectory : File = File ( context . getFilesDir ( ) , " avatars " )
val results = oldAvatarDirectory . listFiles ( )
if ( results != null ) {
Log . i ( TAG , " Preparing to migrate " + results . size + " avatars. " )
for ( file : File in results ) {
if ( Util . isLong ( file . name ) ) {
try {
AvatarHelper . setAvatar ( context , RecipientId . from ( file . name ) , FileInputStream ( file ) )
} catch ( e : IOException ) {
Log . w ( TAG , " Failed to copy file " + file . name + " ! Skipping. " )
}
} else {
Log . w ( TAG , " Invalid avatar name ' " + file . name + " '! Skipping. " )
}
}
} else {
Log . w ( TAG , " No avatar directory files found. " )
}
if ( ! FileUtils . deleteDirectory ( oldAvatarDirectory ) ) {
Log . w ( TAG , " Failed to delete avatar directory. " )
}
db . rawQuery ( " SELECT recipient_id, avatar FROM groups " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val recipientId : RecipientId = RecipientId . from ( cursor . getLong ( cursor . getColumnIndexOrThrow ( " recipient_id " ) ) )
val avatar : ByteArray ? = cursor . getBlob ( cursor . getColumnIndexOrThrow ( " avatar " ) )
try {
AvatarHelper . setAvatar ( context , recipientId , if ( avatar != null ) ByteArrayInputStream ( avatar ) else null )
} catch ( e : IOException ) {
Log . w ( TAG , " Failed to copy avatar for " + recipientId + " ! Skipping. " , e )
}
}
}
db . execSQL ( " UPDATE groups SET avatar_id = 0 WHERE avatar IS NULL " )
db . execSQL ( " UPDATE groups SET avatar = NULL " )
}
if ( oldVersion < GROUPS _V2 ) {
db . execSQL ( " ALTER TABLE groups ADD COLUMN master_key " )
db . execSQL ( " ALTER TABLE groups ADD COLUMN revision " )
db . execSQL ( " ALTER TABLE groups ADD COLUMN decrypted_group " )
}
if ( oldVersion < ATTACHMENT _UPLOAD _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN upload_timestamp DEFAULT 0 " )
}
if ( oldVersion < ATTACHMENT _CDN _NUMBER ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN cdn_number INTEGER DEFAULT 0 " )
}
if ( oldVersion < JOB _INPUT _DATA ) {
db . execSQL ( " ALTER TABLE job_spec ADD COLUMN serialized_input_data TEXT DEFAULT NULL " )
}
if ( oldVersion < SERVER _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN date_server INTEGER DEFAULT -1 " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS sms_date_server_index ON sms (date_server) " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN date_server INTEGER DEFAULT -1 " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS mms_date_server_index ON mms (date_server) " )
}
if ( oldVersion < REMOTE _DELETE ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN remote_deleted INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN remote_deleted INTEGER DEFAULT 0 " )
}
if ( oldVersion < COLOR _MIGRATION ) {
db . rawQuery ( " SELECT _id, system_display_name FROM recipient WHERE system_display_name NOT NULL AND color IS NULL " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val id : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) )
val name : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " system_display_name " ) )
val values : ContentValues = ContentValues ( )
values . put ( " color " , ContactColorsLegacy . generateForV2 ( name ) . serialize ( ) )
db . update ( " recipient " , values , " _id = ? " , arrayOf ( id . toString ( ) ) )
}
}
}
if ( oldVersion < LAST _SCROLLED ) {
db . execSQL ( " ALTER TABLE thread ADD COLUMN last_scrolled INTEGER DEFAULT 0 " )
}
if ( oldVersion < LAST _PROFILE _FETCH ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN last_profile_fetch INTEGER DEFAULT 0 " )
}
if ( oldVersion < SERVER _DELIVERED _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE push ADD COLUMN server_delivered_timestamp INTEGER DEFAULT 0 " )
}
if ( oldVersion < QUOTE _CLEANUP ) {
val query = (
// language=sql
"""
SELECT _data
FROM (
SELECT _data , MIN ( quote ) AS all _quotes
FROM part
WHERE _data NOT NULL AND data _hash NOT NULL
GROUP BY _data
)
WHERE all _quotes = 1
""" .trimIndent()
)
var count = 0
db . rawQuery ( query , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val data : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " _data " ) )
if ( File ( data ) . delete ( ) ) {
val values = ContentValues ( ) . apply {
putNull ( " _data " )
putNull ( " data_random " )
putNull ( " thumbnail " )
putNull ( " thumbnail_random " )
putNull ( " data_hash " )
}
db . update ( " part " , values , " _data = ? " , arrayOf ( data ) )
count ++
} else {
Log . w ( TAG , " [QuoteCleanup] Failed to delete " + data )
}
}
}
Log . i ( TAG , " [QuoteCleanup] Cleaned up $count quotes. " )
}
if ( oldVersion < BORDERLESS ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN borderless INTEGER DEFAULT 0 " )
}
if ( oldVersion < REMAPPED _RECORDS ) {
db . execSQL (
// language=sql
"""
CREATE TABLE remapped _recipients (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
old _id INTEGER UNIQUE ,
new _id INTEGER
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE remapped _threads (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
old _id INTEGER UNIQUE ,
new _id INTEGER
)
""" .trimIndent()
)
}
if ( oldVersion < MENTIONS ) {
db . execSQL (
// language=sql
"""
CREATE TABLE mention (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
thread _id INTEGER ,
message _id INTEGER ,
recipient _id INTEGER ,
range _start INTEGER ,
range _length INTEGER
)
"""
)
db . execSQL ( " CREATE INDEX IF NOT EXISTS mention_message_id_index ON mention (message_id) " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id); " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN quote_mentions BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN mentions_self INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN mention_setting INTEGER DEFAULT 0 " )
}
if ( oldVersion < PINNED _CONVERSATIONS ) {
db . execSQL ( " ALTER TABLE thread ADD COLUMN pinned INTEGER DEFAULT 0 " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS thread_pinned_index ON thread (pinned) " )
}
if ( oldVersion < MENTION _GLOBAL _SETTING _MIGRATION ) {
val updateAlways = ContentValues ( )
updateAlways . put ( " mention_setting " , 0 )
db . update ( " recipient " , updateAlways , " mention_setting = 1 " , null )
val updateNever = ContentValues ( )
updateNever . put ( " mention_setting " , 1 )
db . update ( " recipient " , updateNever , " mention_setting = 2 " , null )
}
if ( oldVersion < UNKNOWN _STORAGE _FIELDS ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN storage_proto TEXT DEFAULT NULL " )
}
if ( oldVersion < STICKER _CONTENT _TYPE ) {
db . execSQL ( " ALTER TABLE sticker ADD COLUMN content_type TEXT DEFAULT NULL " )
}
if ( oldVersion < STICKER _EMOJI _IN _NOTIFICATIONS ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN sticker_emoji TEXT DEFAULT NULL " )
}
if ( oldVersion < THUMBNAIL _CLEANUP ) {
var total = 0
var deleted = 0
db . rawQuery ( " SELECT thumbnail FROM part WHERE thumbnail NOT NULL " , null ) . use { cursor ->
if ( cursor != null ) {
total = cursor . getCount ( )
Log . w ( TAG , " Found " + total + " thumbnails to delete. " )
}
while ( cursor != null && cursor . moveToNext ( ) ) {
val file : File = File ( CursorUtil . requireString ( cursor , " thumbnail " ) )
if ( file . delete ( ) ) {
deleted ++
} else {
Log . w ( TAG , " Failed to delete file! " + file . getAbsolutePath ( ) )
}
}
}
Log . w ( TAG , " Deleted $deleted / $total thumbnail files. " )
}
if ( oldVersion < STICKER _CONTENT _TYPE _CLEANUP ) {
val values = ContentValues ( ) . apply {
put ( " ct " , " image/webp " )
}
val query = " sticker_id NOT NULL AND (ct IS NULL OR ct = '') "
val rows = db . update ( " part " , values , query , null )
Log . i ( TAG , " Updated $rows sticker attachment content types. " )
}
if ( oldVersion < MENTION _CLEANUP ) {
val selectMentionIdsNotInGroupsV2 = " select mention._id from mention left join thread on mention.thread_id = thread._id left join recipient on thread.recipient_ids = recipient._id where recipient.group_type != 3 "
db . delete ( " mention " , " _id in ( $selectMentionIdsNotInGroupsV2 ) " , null )
db . delete ( " mention " , " message_id NOT IN (SELECT _id FROM mms) OR thread_id NOT IN (SELECT _id from thread) " , null )
val idsToDelete : MutableList < Long ? > = LinkedList ( )
db . rawQuery ( " select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val rangeStart : Int = CursorUtil . requireInt ( cursor , " range_start " )
val rangeLength : Int = CursorUtil . requireInt ( cursor , " range_length " )
val body : String ? = CursorUtil . requireString ( cursor , " body " )
if ( ( body == null ) || body . isEmpty ( ) || ( rangeStart < 0 ) || ( rangeLength < 0 ) || ( ( rangeStart + rangeLength ) > body . length ) ) {
idsToDelete . add ( CursorUtil . requireLong ( cursor , " _id " ) )
}
}
}
if ( Util . hasItems ( idsToDelete ) ) {
val ids = TextUtils . join ( " , " , idsToDelete )
db . delete ( " mention " , " _id in ( $ids ) " , null )
}
}
if ( oldVersion < MENTION _CLEANUP _V2 ) {
val selectMentionIdsWithMismatchingThreadIds = " select mention._id from mention left join mms on mention.message_id = mms._id where mention.thread_id != mms.thread_id "
db . delete ( " mention " , " _id in ( $selectMentionIdsWithMismatchingThreadIds ) " , null )
val idsToDelete : MutableList < Long ? > = LinkedList ( )
val mentionTuples : MutableSet < Triple < Long , Int , Int > > = HashSet ( )
db . rawQuery ( " select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id order by mention._id desc " , null ) . use { cursor ->
while ( cursor != null && cursor . moveToNext ( ) ) {
val mentionId : Long = CursorUtil . requireLong ( cursor , " _id " )
val messageId : Long = CursorUtil . requireLong ( cursor , " message_id " )
val rangeStart : Int = CursorUtil . requireInt ( cursor , " range_start " )
val rangeLength : Int = CursorUtil . requireInt ( cursor , " range_length " )
val body : String ? = CursorUtil . requireString ( cursor , " body " )
if ( ( body != null ) && ( rangeStart < body . length ) && ( body . get ( rangeStart ) != ' \uFFFC ' ) ) {
idsToDelete . add ( mentionId )
} else {
val tuple : Triple < Long , Int , Int > = Triple ( messageId , rangeStart , rangeLength )
if ( mentionTuples . contains ( tuple ) ) {
idsToDelete . add ( mentionId )
} else {
mentionTuples . add ( tuple )
}
}
}
if ( Util . hasItems ( idsToDelete ) ) {
val ids : String = TextUtils . join ( " , " , idsToDelete )
db . delete ( " mention " , " _id in ( " + ids + " ) " , null )
}
}
}
if ( oldVersion < REACTION _CLEANUP ) {
val values = ContentValues ( ) . apply {
putNull ( " reactions " )
}
db . update ( " sms " , values , " remote_deleted = ? " , arrayOf ( " 1 " ) )
}
if ( oldVersion < CAPABILITIES _REFACTOR ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN capabilities INTEGER DEFAULT 0 " )
db . execSQL ( " UPDATE recipient SET capabilities = 1 WHERE gv2_capability = 1 " )
db . execSQL ( " UPDATE recipient SET capabilities = 2 WHERE gv2_capability = -1 " )
}
if ( oldVersion < GV1 _MIGRATION ) {
db . execSQL ( " ALTER TABLE groups ADD COLUMN expected_v2_id TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE groups ADD COLUMN former_v1_members TEXT DEFAULT NULL " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON groups (expected_v2_id) " )
var count = 0
db . rawQuery ( " SELECT * FROM groups WHERE group_id LIKE '__textsecure_group__!%' AND LENGTH(group_id) = 53 " , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val gv1 : String = CursorUtil . requireString ( cursor , " group_id " )
val gv2 : String = GroupId . parseOrThrow ( gv1 ) . requireV1 ( ) . deriveV2MigrationGroupId ( ) . toString ( )
val values = ContentValues ( ) . apply {
put ( " expected_v2_id " , gv2 )
}
count += db . update ( " groups " , values , " group_id = ? " , SqlUtil . buildArgs ( gv1 ) )
}
}
Log . i ( TAG , " Updated $count GV1 groups with expected GV2 IDs. " )
}
if ( oldVersion < NOTIFIED _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN notified_timestamp INTEGER DEFAULT 0 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN notified_timestamp INTEGER DEFAULT 0 " )
}
if ( oldVersion < GV1 _MIGRATION _LAST _SEEN ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN last_gv1_migrate_reminder INTEGER DEFAULT 0 " )
}
if ( oldVersion < VIEWED _RECEIPTS ) {
db . execSQL ( " ALTER TABLE mms ADD COLUMN viewed_receipt_count INTEGER DEFAULT 0 " )
}
if ( oldVersion < CLEAN _UP _GV1 _IDS ) {
val deletableRecipients : MutableList < String > = LinkedList ( )
db . rawQuery (
// language=sql
"""
SELECT _id , group _id
FROM recipient
WHERE group _id NOT IN ( SELECT group _id FROM groups )
AND group _id LIKE ' _ _textsecure _group _ _ ! % ' AND length ( group _id ) < > 53
AND ( _id NOT IN ( SELECT recipient _ids FROM thread ) OR _id IN ( SELECT recipient _ids FROM thread WHERE message _count = 0 ) )
""" .trimIndent(),
null
) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val recipientId : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " _id " ) )
val groupIdV1 : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " group_id " ) )
deletableRecipients . add ( recipientId )
Log . d ( TAG , String . format ( Locale . US , " Found invalid GV1 on %s with no or empty thread %s length %d " , recipientId , groupIdV1 , groupIdV1 . length ) )
}
}
for ( recipientId : String in deletableRecipients ) {
db . delete ( " recipient " , " _id = ? " , arrayOf ( recipientId ) )
Log . d ( TAG , " Deleted recipient $recipientId " )
}
val orphanedThreads : MutableList < String > = LinkedList ( )
db . rawQuery ( " SELECT _id FROM thread WHERE message_count = 0 AND recipient_ids NOT IN (SELECT _id FROM recipient) " , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
orphanedThreads . add ( cursor . getString ( cursor . getColumnIndexOrThrow ( " _id " ) ) )
}
}
for ( orphanedThreadId : String in orphanedThreads ) {
db . delete ( " thread " , " _id = ? " , arrayOf ( orphanedThreadId ) )
Log . d ( TAG , " Deleted orphaned thread $orphanedThreadId " )
}
val remainingInvalidGV1Recipients : MutableList < String > = LinkedList ( )
db . rawQuery (
// language=sql
"""
SELECT _id , group _id FROM recipient
WHERE group _id NOT IN ( SELECT group _id FROM groups )
AND group _id LIKE ' _ _textsecure _group _ _ ! % ' AND length ( group _id ) < > 53
AND _id IN ( SELECT recipient _ids FROM thread )
""" .trimIndent(),
null
) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val recipientId : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " _id " ) )
val groupIdV1 : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " group_id " ) )
remainingInvalidGV1Recipients . add ( recipientId )
Log . d ( TAG , String . format ( Locale . US , " Found invalid GV1 on %s with non-empty thread %s length %d " , recipientId , groupIdV1 , groupIdV1 . length ) )
}
}
for ( recipientId : String in remainingInvalidGV1Recipients ) {
val newId = " __textsecure_group__! " + Hex . toStringCondensed ( Util . getSecretBytes ( 16 ) )
val values = ContentValues ( 1 )
values . put ( " group_id " , newId )
db . update ( " recipient " , values , " _id = ? " , arrayOf ( recipientId ) )
Log . d ( TAG , String . format ( " Replaced group id on recipient %s now %s " , recipientId , newId ) )
}
}
if ( oldVersion < GV1 _MIGRATION _REFACTOR ) {
val values = ContentValues ( 1 )
values . putNull ( " former_v1_members " )
val count = db . update ( " groups " , values , " former_v1_members NOT NULL " , null )
Log . i ( TAG , " Cleared former_v1_members for $count rows " )
}
if ( oldVersion < CLEAR _PROFILE _KEY _CREDENTIALS ) {
val values = ContentValues ( 1 )
values . putNull ( " profile_key_credential " )
val count = db . update ( " recipient " , values , " profile_key_credential NOT NULL " , null )
Log . i ( TAG , " Cleared profile key credentials for $count rows " )
}
if ( oldVersion < LAST _RESET _SESSION _TIME ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN last_session_reset BLOB DEFAULT NULL " )
}
if ( oldVersion < WALLPAPER ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN wallpaper BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN wallpaper_file TEXT DEFAULT NULL " )
}
if ( oldVersion < ABOUT ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN about TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN about_emoji TEXT DEFAULT NULL " )
}
if ( oldVersion < SPLIT _SYSTEM _NAMES ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN system_family_name TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN system_given_name TEXT DEFAULT NULL " )
db . execSQL ( " UPDATE recipient SET system_given_name = system_display_name " )
}
if ( oldVersion < PAYMENTS ) {
db . execSQL (
// language=sql
"""
CREATE TABLE payments (
_id INTEGER PRIMARY KEY ,
uuid TEXT DEFAULT NULL ,
recipient INTEGER DEFAULT 0 ,
recipient _address TEXT DEFAULT NULL ,
timestamp INTEGER ,
note TEXT DEFAULT NULL ,
direction INTEGER ,
state INTEGER ,
failure _reason INTEGER ,
amount BLOB NOT NULL ,
fee BLOB NOT NULL ,
transaction _record BLOB DEFAULT NULL ,
receipt BLOB DEFAULT NULL ,
payment _metadata BLOB DEFAULT NULL ,
receipt _public _key TEXT DEFAULT NULL ,
block _index INTEGER DEFAULT 0 ,
block _timestamp INTEGER DEFAULT 0 ,
seen INTEGER ,
UNIQUE ( uuid ) ON CONFLICT ABORT
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX IF NOT EXISTS timestamp_direction_index ON payments (timestamp, direction); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS timestamp_index ON payments (timestamp); " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS receipt_public_key_index ON payments (receipt_public_key); " )
}
if ( oldVersion < CLEAN _STORAGE _IDS ) {
val values = ContentValues ( )
values . putNull ( " storage_service_key " )
val count = db . update ( " recipient " , values , " storage_service_key NOT NULL AND ((phone NOT NULL AND INSTR(phone, '+') = 0) OR (group_id NOT NULL AND (LENGTH(group_id) != 85 and LENGTH(group_id) != 53))) " , null )
Log . i ( TAG , " There were $count bad rows that had their storageID removed. " )
}
if ( oldVersion < MP4 _GIF _SUPPORT ) {
db . execSQL ( " ALTER TABLE part ADD COLUMN video_gif INTEGER DEFAULT 0 " )
}
if ( oldVersion < BLUR _AVATARS ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN extras BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN groups_in_common INTEGER DEFAULT 0 " )
val secureOutgoingSms = " EXISTS(SELECT 1 FROM sms WHERE thread_id = t._id AND (type & 31) = 23 AND (type & 10485760) AND (type & 131072 = 0)) "
val secureOutgoingMms = " EXISTS(SELECT 1 FROM mms WHERE thread_id = t._id AND (msg_box & 31) = 23 AND (msg_box & 10485760) AND (msg_box & 131072 = 0)) "
val selectIdsToUpdateProfileSharing = " SELECT r._id FROM recipient AS r INNER JOIN thread AS t ON r._id = t.recipient_ids WHERE profile_sharing = 0 AND ( $secureOutgoingSms OR $secureOutgoingMms ) "
db . execSQL ( " UPDATE recipient SET profile_sharing = 1 WHERE _id IN ( $selectIdsToUpdateProfileSharing ) " )
val selectIdsWithGroupsInCommon =
// language=sql
"""
SELECT r . _id FROM recipient AS r WHERE EXISTS (
SELECT 1
FROM groups AS g
INNER JOIN recipient AS gr ON ( g . recipient _id = gr . _id AND gr . profile _sharing = 1 )
WHERE g . active = 1 AND ( g . members LIKE r . _id || ' , % ' OR g . members LIKE ' % , ' || r . _id || ' , % ' OR g . members LIKE ' % , ' || r . _id )
)
""" .trimIndent()
db . execSQL ( " UPDATE recipient SET groups_in_common = 1 WHERE _id IN ( $selectIdsWithGroupsInCommon ) " )
}
if ( oldVersion < CLEAN _STORAGE _IDS _WITHOUT _INFO ) {
val values = ContentValues ( )
values . putNull ( " storage_service_key " )
val count = db . update ( " recipient " , values , " storage_service_key NOT NULL AND phone IS NULL AND uuid IS NULL AND group_id IS NULL " , null )
Log . i ( TAG , " There were $count bad rows that had their storageID removed due to not having any other identifier. " )
}
if ( oldVersion < CLEAN _REACTION _NOTIFICATIONS ) {
val values = ContentValues ( 1 )
values . put ( " notified " , 1 )
var count = 0
count += db . update ( " sms " , values , " notified = 0 AND read = 1 AND reactions_unread = 1 AND NOT ((type & 31) = 23 AND (type & 10485760) AND (type & 131072 = 0)) " , null )
count += db . update ( " mms " , values , " notified = 0 AND read = 1 AND reactions_unread = 1 AND NOT ((msg_box & 31) = 23 AND (msg_box & 10485760) AND (msg_box & 131072 = 0)) " , null )
Log . d ( TAG , " Resetting notified for $count read incoming messages that were incorrectly flipped when receiving reactions " )
val smsIds : MutableList < Long > = ArrayList ( )
db . query ( " sms " , arrayOf ( " _id " , " reactions " , " notified_timestamp " ) , " notified = 0 AND reactions_unread = 1 " , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val reactions : ByteArray ? = cursor . getBlob ( cursor . getColumnIndexOrThrow ( " reactions " ) )
val notifiedTimestamp : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " notified_timestamp " ) )
if ( reactions == null ) {
continue
}
try {
val hasReceiveLaterThanNotified : Boolean = ReactionList . parseFrom ( reactions )
. getReactionsList ( )
. stream ( )
. anyMatch { r : ReactionList . Reaction -> r . getReceivedTime ( ) > notifiedTimestamp }
if ( ! hasReceiveLaterThanNotified ) {
smsIds . add ( cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) ) )
}
} catch ( e : InvalidProtocolBufferException ) {
Log . e ( TAG , e )
}
}
}
if ( smsIds . size > 0 ) {
Log . d ( TAG , " Updating " + smsIds . size + " records in sms " )
db . execSQL ( " UPDATE sms SET reactions_last_seen = notified_timestamp WHERE _id in ( " + Util . join ( smsIds , " , " ) + " ) " )
}
val mmsIds : MutableList < Long > = ArrayList ( )
db . query ( " mms " , arrayOf ( " _id " , " reactions " , " notified_timestamp " ) , " notified = 0 AND reactions_unread = 1 " , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val reactions : ByteArray ? = cursor . getBlob ( cursor . getColumnIndexOrThrow ( " reactions " ) )
val notifiedTimestamp : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " notified_timestamp " ) )
if ( reactions == null ) {
continue
}
try {
val hasReceiveLaterThanNotified : Boolean = ReactionList . parseFrom ( reactions )
. getReactionsList ( )
. stream ( )
. anyMatch { r : ReactionList . Reaction -> r . getReceivedTime ( ) > notifiedTimestamp }
if ( ! hasReceiveLaterThanNotified ) {
mmsIds . add ( cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) ) )
}
} catch ( e : InvalidProtocolBufferException ) {
Log . e ( TAG , e )
}
}
}
if ( mmsIds . size > 0 ) {
Log . d ( TAG , " Updating " + mmsIds . size + " records in mms " )
db . execSQL ( " UPDATE mms SET reactions_last_seen = notified_timestamp WHERE _id in ( ${Util.join(mmsIds, ",")} ) " )
}
}
if ( oldVersion < STORAGE _SERVICE _REFACTOR ) {
val deleteCount : Int
var insertCount : Int
var updateCount : Int
val dirtyCount : Int
val deleteValues = ContentValues ( )
deleteValues . putNull ( " storage_service_key " )
deleteCount = db . update ( " recipient " , deleteValues , " storage_service_key NOT NULL AND (dirty = 3 OR group_type = 1 OR (group_type = 0 AND registered = 2)) " , null )
db . query ( " recipient " , arrayOf ( " _id " ) , " storage_service_key IS NULL AND (dirty = 2 OR registered = 1) " , null , null , null , null ) . use { cursor ->
insertCount = cursor . getCount ( )
while ( cursor . moveToNext ( ) ) {
val insertValues = ContentValues ( ) . apply {
put ( " storage_service_key " , Base64 . encodeBytes ( StorageSyncHelper . generateKey ( ) ) )
}
val id : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) )
db . update ( " recipient " , insertValues , " _id = ? " , SqlUtil . buildArgs ( id ) )
}
}
db . query ( " recipient " , arrayOf ( " _id " ) , " storage_service_key NOT NULL AND dirty = 1 " , null , null , null , null ) . use { cursor ->
updateCount = cursor . getCount ( )
while ( cursor . moveToNext ( ) ) {
val updateValues = ContentValues ( ) . apply {
put ( " storage_service_key " , Base64 . encodeBytes ( StorageSyncHelper . generateKey ( ) ) )
}
val id : Long = cursor . getLong ( cursor . getColumnIndexOrThrow ( " _id " ) )
db . update ( " recipient " , updateValues , " _id = ? " , SqlUtil . buildArgs ( id ) )
}
}
val clearDirtyValues = ContentValues ( ) . apply {
put ( " dirty " , 0 )
}
dirtyCount = db . update ( " recipient " , clearDirtyValues , " dirty != 0 " , null )
Log . d ( TAG , String . format ( Locale . US , " For storage service refactor migration, there were %d inserts, %d updated, and %d deletes. Cleared the dirty status on %d rows. " , insertCount , updateCount , deleteCount , dirtyCount ) )
}
if ( oldVersion < CLEAR _MMS _STORAGE _IDS ) {
val deleteValues = ContentValues ( ) . apply {
putNull ( " storage_service_key " )
}
val deleteCount = db . update ( " recipient " , deleteValues , " storage_service_key NOT NULL AND (group_type = 1 OR (group_type = 0 AND phone IS NULL AND uuid IS NULL)) " , null )
Log . d ( TAG , " Cleared storageIds from $deleteCount rows. They were either MMS groups or empty contacts. " )
}
if ( oldVersion < SERVER _GUID ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN server_guid TEXT DEFAULT NULL " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN server_guid TEXT DEFAULT NULL " )
}
if ( oldVersion < CHAT _COLORS ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN chat_colors BLOB DEFAULT NULL " )
db . execSQL ( " ALTER TABLE recipient ADD COLUMN custom_chat_colors_id INTEGER DEFAULT 0 " )
db . execSQL (
// language=sql
"""
CREATE TABLE chat _colors (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
chat _colors BLOB
)
"""
)
val entrySet : Set < Map . Entry < MaterialColor , ChatColors > > = entrySet
val where = " color = ? AND group_id is NULL "
for ( entry : Map . Entry < MaterialColor , ChatColors > in entrySet ) {
val whereArgs = SqlUtil . buildArgs ( entry . key . serialize ( ) )
val values = ContentValues ( 2 )
values . put ( " chat_colors " , entry . value . serialize ( ) . toByteArray ( ) )
values . put ( " custom_chat_colors_id " , entry . value . id . longValue )
db . update ( " recipient " , values , where , whereArgs )
}
}
if ( oldVersion < AVATAR _COLORS ) {
db . query ( " recipient " , arrayOf ( " _id " ) , " color IS NULL " , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val id : Long = cursor . getInt ( cursor . getColumnIndexOrThrow ( " _id " ) ) . toLong ( )
val values : ContentValues = ContentValues ( 1 )
values . put ( " color " , AvatarColor . random ( ) . serialize ( ) )
db . update ( " recipient " , values , " _id = ? " , arrayOf ( id . toString ( ) ) )
}
}
}
if ( oldVersion < EMOJI _SEARCH ) {
// language=text
db . execSQL ( " CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED) " )
}
if ( oldVersion < SENDER _KEY && ! SqlUtil . tableExists ( db , " sender_keys " ) ) {
db . execSQL (
// language=sql
"""
CREATE TABLE sender _keys (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
recipient _id INTEGER NOT NULL ,
device INTEGER NOT NULL ,
distribution _id TEXT NOT NULL ,
record BLOB NOT NULL ,
created _at INTEGER NOT NULL ,
UNIQUE ( recipient _id , device , distribution _id ) ON CONFLICT REPLACE
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE sender _key _shared (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
distribution _id TEXT NOT NULL ,
address TEXT NOT NULL ,
device INTEGER NOT NULL ,
UNIQUE ( distribution _id , address , device ) ON CONFLICT REPLACE
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE pending _retry _receipts (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
author TEXT NOT NULL ,
device INTEGER NOT NULL ,
sent _timestamp INTEGER NOT NULL ,
received _timestamp TEXT NOT NULL ,
thread _id INTEGER NOT NULL ,
UNIQUE ( author , sent _timestamp ) ON CONFLICT REPLACE
) ;
""" .trimIndent()
)
db . execSQL ( " ALTER TABLE groups ADD COLUMN distribution_id TEXT DEFAULT NULL " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON groups (distribution_id) " )
db . query ( " groups " , arrayOf ( " group_id " ) , " LENGTH(group_id) = 85 " , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val groupId : String = cursor . getString ( cursor . getColumnIndexOrThrow ( " group_id " ) )
val values = ContentValues ( ) . apply {
put ( " distribution_id " , DistributionId . create ( ) . toString ( ) )
}
db . update ( " groups " , values , " group_id = ? " , arrayOf ( groupId ) )
}
}
}
if ( oldVersion < MESSAGE _DUPE _INDEX ) {
db . execSQL ( " DROP INDEX sms_date_sent_index " )
db . execSQL ( " CREATE INDEX sms_date_sent_index on sms(date_sent, address, thread_id) " )
db . execSQL ( " DROP INDEX mms_date_sent_index " )
db . execSQL ( " CREATE INDEX mms_date_sent_index on mms(date, address, thread_id) " )
}
if ( oldVersion < MESSAGE _LOG ) {
db . execSQL (
// language=sql
"""
CREATE TABLE message _send _log (
_id INTEGER PRIMARY KEY ,
date _sent INTEGER NOT NULL ,
content BLOB NOT NULL ,
related _message _id INTEGER DEFAULT - 1 ,
is _related _message _mms INTEGER DEFAULT 0 ,
content _hint INTEGER NOT NULL ,
group _id BLOB DEFAULT NULL
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX message_log_date_sent_index ON message_send_log (date_sent) " )
db . execSQL ( " CREATE INDEX message_log_related_message_index ON message_send_log (related_message_id, is_related_message_mms) " )
db . execSQL ( " CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 0; END " )
db . execSQL ( " CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 1; END " )
db . execSQL (
// language=sql
"""
CREATE TABLE message _send _log _recipients (
_id INTEGER PRIMARY KEY ,
message _send _log _id INTEGER NOT NULL REFERENCES message _send _log ( _id ) ON DELETE CASCADE ,
recipient _id INTEGER NOT NULL ,
device INTEGER NOT NULL
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX message_send_log_recipients_recipient_index ON message_send_log_recipients (recipient_id, device) " )
}
if ( oldVersion < MESSAGE _LOG _2 ) {
db . execSQL ( " DROP TABLE message_send_log " )
db . execSQL ( " DROP INDEX IF EXISTS message_log_date_sent_index " )
db . execSQL ( " DROP INDEX IF EXISTS message_log_related_message_index " )
db . execSQL ( " DROP TRIGGER msl_sms_delete " )
db . execSQL ( " DROP TRIGGER msl_mms_delete " )
db . execSQL ( " DROP TABLE message_send_log_recipients " )
db . execSQL ( " DROP INDEX IF EXISTS message_send_log_recipients_recipient_index " )
db . execSQL (
// language=sql
"""
CREATE TABLE msl _payload (
_id INTEGER PRIMARY KEY ,
date _sent INTEGER NOT NULL ,
content BLOB NOT NULL ,
content _hint INTEGER NOT NULL
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent) " )
db . execSQL (
// language=sql
"""
CREATE TABLE msl _recipient (
_id INTEGER PRIMARY KEY ,
payload _id INTEGER NOT NULL REFERENCES msl _payload ( _id ) ON DELETE CASCADE ,
recipient _id INTEGER NOT NULL ,
device INTEGER NOT NULL
)
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id) " )
db . execSQL ( " CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id) " )
db . execSQL (
// language=sql
"""
CREATE TABLE msl _message (
_id INTEGER PRIMARY KEY ,
payload _id INTEGER NOT NULL REFERENCES msl _payload ( _id ) ON DELETE CASCADE ,
message _id INTEGER NOT NULL ,
is _mms INTEGER NOT NULL
)
"""
)
db . execSQL ( " CREATE INDEX msl_message_message_index ON msl_message (message_id, is_mms, payload_id) " )
db . execSQL ( " CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END " )
db . execSQL ( " CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END " )
db . execSQL ( " CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid AND is_mms = 1); END " )
}
if ( oldVersion < ABANDONED _MESSAGE _CLEANUP ) {
val start = System . currentTimeMillis ( )
val smsDeleteCount = db . delete ( " sms " , " thread_id NOT IN (SELECT _id FROM thread) " , null )
val mmsDeleteCount = db . delete ( " mms " , " thread_id NOT IN (SELECT _id FROM thread) " , null )
Log . i ( TAG , " Deleted " + smsDeleteCount + " sms and " + mmsDeleteCount + " mms in " + ( System . currentTimeMillis ( ) - start ) + " ms " )
}
if ( oldVersion < THREAD _AUTOINCREMENT ) {
val stopwatch = Stopwatch ( " thread-autoincrement " )
db . execSQL (
// language=sql
"""
CREATE TABLE thread _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
date INTEGER DEFAULT 0 ,
thread _recipient _id INTEGER ,
message _count INTEGER DEFAULT 0 ,
snippet TEXT ,
snippet _charset INTEGER DEFAULT 0 ,
snippet _type INTEGER DEFAULT 0 ,
snippet _uri TEXT DEFAULT NULL ,
snippet _content _type INTEGER DEFAULT NULL ,
snippet _extras TEXT DEFAULT NULL ,
read INTEGER DEFAULT 1 ,
type INTEGER DEFAULT 0 ,
error INTEGER DEFAULT 0 ,
archived INTEGER DEFAULT 0 ,
status INTEGER DEFAULT 0 ,
expires _in INTEGER DEFAULT 0 ,
last _seen INTEGER DEFAULT 0 ,
has _sent INTEGER DEFAULT 0 ,
delivery _receipt _count INTEGER DEFAULT 0 ,
read _receipt _count INTEGER DEFAULT 0 ,
unread _count INTEGER DEFAULT 0 ,
last _scrolled INTEGER DEFAULT 0 ,
pinned INTEGER DEFAULT 0
)
""" .trimIndent()
)
stopwatch . split ( " table-create " )
db . execSQL (
// language=sql
"""
INSERT INTO thread _tmp
SELECT
_id ,
date ,
recipient _ids ,
message _count ,
snippet ,
snippet _cs ,
snippet _type ,
snippet _uri ,
snippet _content _type ,
snippet _extras ,
read ,
type ,
error ,
archived ,
status ,
expires _in ,
last _seen ,
has _sent ,
delivery _receipt _count ,
read _receipt _count ,
unread _count ,
last _scrolled ,
pinned
FROM thread
""" .trimIndent()
)
stopwatch . split ( " table-copy " )
db . execSQL ( " DROP TABLE thread " )
db . execSQL ( " ALTER TABLE thread_tmp RENAME TO thread " )
stopwatch . split ( " table-rename " )
db . execSQL ( " CREATE INDEX thread_recipient_id_index ON thread (thread_recipient_id) " )
db . execSQL ( " CREATE INDEX archived_count_index ON thread (archived, message_count) " )
db . execSQL ( " CREATE INDEX thread_pinned_index ON thread (pinned) " )
stopwatch . split ( " indexes " )
db . execSQL ( " DELETE FROM remapped_threads " )
stopwatch . split ( " delete-remap " )
stopwatch . stop ( TAG )
}
if ( oldVersion < MMS _AUTOINCREMENT ) {
val mmsStopwatch = Stopwatch ( " mms-autoincrement " )
db . execSQL (
// language=sql
"""
CREATE TABLE mms _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
thread _id INTEGER ,
date INTEGER ,
date _received INTEGER ,
date _server INTEGER DEFAULT - 1 ,
msg _box INTEGER ,
read INTEGER DEFAULT 0 ,
body TEXT ,
part _count INTEGER ,
ct _l TEXT ,
address INTEGER ,
address _device _id INTEGER ,
exp INTEGER ,
m _type INTEGER ,
m _size INTEGER ,
st INTEGER ,
tr _id TEXT ,
delivery _receipt _count INTEGER DEFAULT 0 ,
mismatched _identities TEXT DEFAULT NULL ,
network _failures TEXT DEFAULT NULL ,
subscription _id INTEGER DEFAULT - 1 ,
expires _in INTEGER DEFAULT 0 ,
expire _started INTEGER DEFAULT 0 ,
notified INTEGER DEFAULT 0 ,
read _receipt _count INTEGER DEFAULT 0 ,
quote _id INTEGER DEFAULT 0 ,
quote _author TEXT ,
quote _body TEXT ,
quote _attachment INTEGER DEFAULT - 1 ,
quote _missing INTEGER DEFAULT 0 ,
quote _mentions BLOB DEFAULT NULL ,
shared _contacts TEXT ,
unidentified INTEGER DEFAULT 0 ,
previews TEXT ,
reveal _duration INTEGER DEFAULT 0 ,
reactions BLOB DEFAULT NULL ,
reactions _unread INTEGER DEFAULT 0 ,
reactions _last _seen INTEGER DEFAULT - 1 ,
remote _deleted INTEGER DEFAULT 0 ,
mentions _self INTEGER DEFAULT 0 ,
notified _timestamp INTEGER DEFAULT 0 ,
viewed _receipt _count INTEGER DEFAULT 0 ,
server _guid TEXT DEFAULT NULL
) ;
""" .trimIndent()
)
mmsStopwatch . split ( " table-create " )
db . execSQL (
// language=sql
"""
INSERT INTO mms _tmp
SELECT
_id ,
thread _id ,
date ,
date _received ,
date _server ,
msg _box ,
read ,
body ,
part _count ,
ct _l ,
address ,
address _device _id ,
exp ,
m _type ,
m _size ,
st ,
tr _id ,
delivery _receipt _count ,
mismatched _identities ,
network _failures ,
subscription _id ,
expires _in ,
expire _started ,
notified ,
read _receipt _count ,
quote _id ,
quote _author ,
quote _body ,
quote _attachment ,
quote _missing ,
quote _mentions ,
shared _contacts ,
unidentified ,
previews ,
reveal _duration ,
reactions ,
reactions _unread ,
reactions _last _seen ,
remote _deleted ,
mentions _self ,
notified _timestamp ,
viewed _receipt _count ,
server _guid
FROM mms
""" .trimIndent()
)
mmsStopwatch . split ( " table-copy " )
db . execSQL ( " DROP TABLE mms " )
db . execSQL ( " ALTER TABLE mms_tmp RENAME TO mms " )
mmsStopwatch . split ( " table-rename " )
db . execSQL ( " CREATE INDEX mms_read_and_notified_and_thread_id_index ON mms(read, notified, thread_id) " )
db . execSQL ( " CREATE INDEX mms_message_box_index ON mms (msg_box) " )
db . execSQL ( " CREATE INDEX mms_date_sent_index ON mms (date, address, thread_id) " )
db . execSQL ( " CREATE INDEX mms_date_server_index ON mms (date_server) " )
db . execSQL ( " CREATE INDEX mms_thread_date_index ON mms (thread_id, date_received) " )
db . execSQL ( " CREATE INDEX mms_reactions_unread_index ON mms (reactions_unread) " )
mmsStopwatch . split ( " indexes " )
db . execSQL ( " CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END " )
db . execSQL ( " CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END " )
// language=text
db . execSQL ( " CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END " )
db . execSQL ( " CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END " )
mmsStopwatch . split ( " triggers " )
mmsStopwatch . stop ( TAG )
val smsStopwatch = Stopwatch ( " sms-autoincrement " )
db . execSQL (
// language=sql
"""
CREATE TABLE sms _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
thread _id INTEGER ,
address INTEGER ,
address _device _id INTEGER DEFAULT 1 ,
person INTEGER ,
date INTEGER ,
date _sent INTEGER ,
date _server INTEGER DEFAULT - 1 ,
protocol INTEGER ,
read INTEGER DEFAULT 0 ,
status INTEGER DEFAULT - 1 ,
type INTEGER ,
reply _path _present INTEGER ,
delivery _receipt _count INTEGER DEFAULT 0 ,
subject TEXT ,
body TEXT ,
mismatched _identities TEXT DEFAULT NULL ,
service _center TEXT ,
subscription _id INTEGER DEFAULT - 1 ,
expires _in INTEGER DEFAULT 0 ,
expire _started INTEGER DEFAULT 0 ,
notified DEFAULT 0 ,
read _receipt _count INTEGER DEFAULT 0 ,
unidentified INTEGER DEFAULT 0 ,
reactions BLOB DEFAULT NULL ,
reactions _unread INTEGER DEFAULT 0 ,
reactions _last _seen INTEGER DEFAULT - 1 ,
remote _deleted INTEGER DEFAULT 0 ,
notified _timestamp INTEGER DEFAULT 0 ,
server _guid TEXT DEFAULT NULL
)
""" .trimIndent()
)
smsStopwatch . split ( " table-create " )
db . execSQL (
// language=sql
"""
INSERT INTO sms _tmp
SELECT
_id ,
thread _id ,
address ,
address _device _id ,
person ,
date ,
date _sent ,
date _server ,
protocol ,
read ,
status ,
type ,
reply _path _present ,
delivery _receipt _count ,
subject ,
body ,
mismatched _identities ,
service _center ,
subscription _id ,
expires _in ,
expire _started ,
notified ,
read _receipt _count ,
unidentified ,
reactions BLOB ,
reactions _unread ,
reactions _last _seen ,
remote _deleted ,
notified _timestamp ,
server _guid
FROM sms
""" .trimIndent()
)
smsStopwatch . split ( " table-copy " )
db . execSQL ( " DROP TABLE sms " )
db . execSQL ( " ALTER TABLE sms_tmp RENAME TO sms " )
smsStopwatch . split ( " table-rename " )
db . execSQL ( " CREATE INDEX sms_read_and_notified_and_thread_id_index ON sms(read, notified, thread_id) " )
db . execSQL ( " CREATE INDEX sms_type_index ON sms (type) " )
db . execSQL ( " CREATE INDEX sms_date_sent_index ON sms (date_sent, address, thread_id) " )
db . execSQL ( " CREATE INDEX sms_date_server_index ON sms (date_server) " )
db . execSQL ( " CREATE INDEX sms_thread_date_index ON sms (thread_id, date) " )
db . execSQL ( " CREATE INDEX sms_reactions_unread_index ON sms (reactions_unread) " )
smsStopwatch . split ( " indexes " )
db . execSQL ( " CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END; " )
db . execSQL ( " CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END; " )
// language=text
db . execSQL ( " CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id); END; " )
db . execSQL ( " CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END " )
smsStopwatch . split ( " triggers " )
smsStopwatch . stop ( TAG )
}
if ( oldVersion < ABANDONED _ATTACHMENT _CLEANUP ) {
db . delete ( " part " , " mid != -8675309 AND mid NOT IN (SELECT _id FROM mms) " , null )
}
if ( oldVersion < AVATAR _PICKER ) {
db . execSQL (
// language=sql
"""
CREATE TABLE avatar _picker (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
last _used INTEGER DEFAULT 0 ,
group _id TEXT DEFAULT NULL ,
avatar BLOB NOT NULL
)
""" .trimIndent()
)
db . query ( " recipient " , arrayOf ( " _id " ) , " color IS NULL " , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val id : Long = cursor . getInt ( cursor . getColumnIndexOrThrow ( " _id " ) ) . toLong ( )
val values = ContentValues ( 1 ) . apply {
put ( " color " , AvatarColor . random ( ) . serialize ( ) )
}
db . update ( " recipient " , values , " _id = ? " , arrayOf ( id . toString ( ) ) )
}
}
}
if ( oldVersion < THREAD _CLEANUP ) {
db . delete ( " mms " , " thread_id NOT IN (SELECT _id FROM thread) " , null )
db . delete ( " part " , " mid != -8675309 AND mid NOT IN (SELECT _id FROM mms) " , null )
}
if ( oldVersion < SESSION _MIGRATION ) {
val start = System . currentTimeMillis ( )
db . execSQL (
// language=sql
"""
CREATE TABLE sessions _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
address TEXT NOT NULL ,
device INTEGER NOT NULL ,
record BLOB NOT NULL ,
UNIQUE ( address , device ) )
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
INSERT INTO sessions _tmp ( address , device , record )
SELECT
COALESCE ( recipient . uuid , recipient . phone ) AS new _address ,
sessions . device ,
sessions . record
FROM sessions INNER JOIN recipient ON sessions . address = recipient . _id
WHERE new _address NOT NULL
""" .trimIndent()
)
db . execSQL ( " DROP TABLE sessions " )
db . execSQL ( " ALTER TABLE sessions_tmp RENAME TO sessions " )
Log . d ( TAG , " Session migration took " + ( System . currentTimeMillis ( ) - start ) + " ms " )
}
if ( oldVersion < IDENTITY _MIGRATION ) {
val start = System . currentTimeMillis ( )
db . execSQL (
// language=sql
"""
CREATE TABLE identities _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
address TEXT UNIQUE NOT NULL ,
identity _key TEXT ,
first _use INTEGER DEFAULT 0 ,
timestamp INTEGER DEFAULT 0 ,
verified INTEGER DEFAULT 0 ,
nonblocking _approval INTEGER DEFAULT 0 )
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
INSERT INTO identities _tmp ( address , identity _key , first _use , timestamp , verified , nonblocking _approval )
SELECT
COALESCE ( recipient . uuid , recipient . phone ) AS new _address ,
identities . key ,
identities . first _use ,
identities . timestamp ,
identities . verified ,
identities . nonblocking _approval
FROM identities INNER JOIN recipient ON identities . address = recipient . _id
WHERE new _address NOT NULL
""" .trimIndent()
)
db . execSQL ( " DROP TABLE identities " )
db . execSQL ( " ALTER TABLE identities_tmp RENAME TO identities " )
Log . d ( TAG , " Identity migration took " + ( System . currentTimeMillis ( ) - start ) + " ms " )
}
if ( oldVersion < GROUP _CALL _RING _TABLE ) {
db . execSQL ( " CREATE TABLE group_call_ring (_id INTEGER PRIMARY KEY, ring_id INTEGER UNIQUE, date_received INTEGER, ring_state INTEGER) " )
db . execSQL ( " CREATE INDEX date_received_index on group_call_ring (date_received) " )
}
if ( oldVersion < CLEANUP _SESSION _MIGRATION ) {
val sessionCount = db . delete ( " sessions " , " address LIKE '+%' " , null )
Log . i ( TAG , " Cleaned up $sessionCount sessions. " )
val storageValues = ContentValues ( )
storageValues . putNull ( " storage_service_key " )
val storageCount = db . update ( " recipient " , storageValues , " storage_service_key NOT NULL AND group_id IS NULL AND uuid IS NULL " , null )
Log . i ( TAG , " Cleaned up $storageCount storageIds. " )
}
if ( oldVersion < RECEIPT _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE sms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1 " )
db . execSQL ( " ALTER TABLE mms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1 " )
}
if ( oldVersion < BADGES ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN badges BLOB DEFAULT NULL " )
}
if ( oldVersion < SENDER _KEY _UUID ) {
val start = System . currentTimeMillis ( )
db . execSQL (
// language=sql
"""
CREATE TABLE sender _keys _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
address TEXT NOT NULL ,
device INTEGER NOT NULL ,
distribution _id TEXT NOT NULL ,
record BLOB NOT NULL ,
created _at INTEGER NOT NULL ,
UNIQUE ( address , device , distribution _id ) ON CONFLICT REPLACE
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
INSERT INTO sender _keys _tmp ( address , device , distribution _id , record , created _at )
SELECT
recipient . uuid AS new _address ,
sender _keys . device ,
sender _keys . distribution _id ,
sender _keys . record ,
sender _keys . created _at
FROM sender _keys INNER JOIN recipient ON sender _keys . recipient _id = recipient . _id
WHERE new _address NOT NULL
""" .trimIndent()
)
db . execSQL ( " DROP TABLE sender_keys " )
db . execSQL ( " ALTER TABLE sender_keys_tmp RENAME TO sender_keys " )
Log . d ( TAG , " Sender key migration took " + ( System . currentTimeMillis ( ) - start ) + " ms " )
}
if ( oldVersion < SENDER _KEY _SHARED _TIMESTAMP ) {
db . execSQL ( " ALTER TABLE sender_key_shared ADD COLUMN timestamp INTEGER DEFAULT 0 " )
}
if ( oldVersion < REACTION _REFACTOR ) {
db . execSQL (
// language=sql
"""
CREATE TABLE reaction (
_id INTEGER PRIMARY KEY ,
message _id INTEGER NOT NULL ,
is _mms INTEGER NOT NULL ,
author _id INTEGER NOT NULL REFERENCES recipient ( _id ) ON DELETE CASCADE ,
emoji TEXT NOT NULL ,
date _sent INTEGER NOT NULL ,
date _received INTEGER NOT NULL ,
UNIQUE ( message _id , is _mms , author _id ) ON CONFLICT REPLACE
)
""" .trimIndent()
)
db . rawQuery ( " SELECT _id, reactions FROM sms WHERE reactions NOT NULL " , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
migrateReaction ( db , cursor , false )
}
}
db . rawQuery ( " SELECT _id, reactions FROM mms WHERE reactions NOT NULL " , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
migrateReaction ( db , cursor , true )
}
}
db . execSQL ( " UPDATE reaction SET author_id = IFNULL((SELECT new_id FROM remapped_recipients WHERE author_id = old_id), author_id) " )
db . execSQL ( " CREATE TRIGGER reactions_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END " )
db . execSQL ( " CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END " )
db . execSQL ( " UPDATE sms SET reactions = NULL WHERE reactions NOT NULL " )
db . execSQL ( " UPDATE mms SET reactions = NULL WHERE reactions NOT NULL " )
}
2021-12-06 17:18:42 +00:00
if ( oldVersion < PNI ) {
db . execSQL ( " ALTER TABLE recipient ADD COLUMN pni TEXT DEFAULT NULL " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON recipient (pni) " )
}
2021-12-08 18:22:36 +00:00
if ( oldVersion < NOTIFICATION _PROFILES ) {
db . execSQL (
// language=sql
"""
CREATE TABLE notification _profile (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
name TEXT NOT NULL UNIQUE ,
emoji TEXT NOT NULL ,
color TEXT NOT NULL ,
created _at INTEGER NOT NULL ,
allow _all _calls INTEGER NOT NULL DEFAULT 0 ,
allow _all _mentions INTEGER NOT NULL DEFAULT 0
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE notification _profile _schedule (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
notification _profile _id INTEGER NOT NULL REFERENCES notification _profile ( _id ) ON DELETE CASCADE ,
enabled INTEGER NOT NULL DEFAULT 0 ,
start INTEGER NOT NULL ,
end INTEGER NOT NULL ,
days _enabled TEXT NOT NULL
)
""" .trimIndent()
)
db . execSQL (
// language=sql
"""
CREATE TABLE notification _profile _allowed _members (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
notification _profile _id INTEGER NOT NULL REFERENCES notification _profile ( _id ) ON DELETE CASCADE ,
recipient _id INTEGER NOT NULL ,
UNIQUE ( notification _profile _id , recipient _id ) ON CONFLICT REPLACE )
""" .trimIndent()
)
db . execSQL ( " CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id) " )
db . execSQL ( " CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id) " )
}
2021-12-20 18:16:31 +00:00
if ( oldVersion < NOTIFICATION _PROFILES _END _FIX ) {
db . execSQL (
// language=sql
"""
UPDATE notification _profile _schedule SET end = 2400 WHERE end = 0
""" .trimIndent()
2022-01-18 22:08:38 +00:00
)
}
if ( oldVersion < REACTION _BACKUP _CLEANUP ) {
db . execSQL (
// language=sql
"""
DELETE FROM reaction
WHERE
( is _mms = 0 AND message _id NOT IN ( SELECT _id FROM sms ) )
OR
( is _mms = 1 AND message _id NOT IN ( SELECT _id FROM mms ) )
""" .trimIndent()
2022-01-19 16:12:07 +00:00
)
}
if ( oldVersion < REACTION _REMOTE _DELETE _CLEANUP ) {
db . execSQL (
// language=sql
"""
DELETE FROM reaction
WHERE
( is _mms = 0 AND message _id IN ( SELECT _id from sms WHERE remote _deleted = 1 ) )
OR
( is _mms = 1 AND message _id IN ( SELECT _id from mms WHERE remote _deleted = 1 ) )
""" .trimIndent()
2021-12-20 18:16:31 +00:00
)
}
2022-01-28 17:16:30 +00:00
if ( oldVersion < PNI _CLEANUP ) {
db . execSQL ( " UPDATE recipient SET pni = NULL WHERE phone IS NULL " )
}
2021-11-18 17:36:52 +00:00
}
@JvmStatic
fun migratePostTransaction ( context : Context , oldVersion : Int ) {
if ( oldVersion < MIGRATE _PREKEYS _VERSION ) {
PreKeyMigrationHelper . cleanUpPreKeys ( context )
}
}
private fun migrateReaction ( db : SQLiteDatabase , cursor : Cursor , isMms : Boolean ) {
try {
val messageId = CursorUtil . requireLong ( cursor , " _id " )
val reactionList = ReactionList . parseFrom ( CursorUtil . requireBlob ( cursor , " reactions " ) )
for ( reaction in reactionList . reactionsList ) {
val contentValues = ContentValues ( ) . apply {
put ( " message_id " , messageId )
put ( " is_mms " , if ( isMms ) 1 else 0 )
put ( " author_id " , reaction . author )
put ( " emoji " , reaction . emoji )
put ( " date_sent " , reaction . sentTime )
put ( " date_received " , reaction . receivedTime )
}
db . insert ( " reaction " , null , contentValues )
}
} catch ( e : InvalidProtocolBufferException ) {
Log . w ( TAG , " Failed to parse reaction! " )
}
}
}