2021-11-18 17:36:52 +00:00
package org.thoughtcrime.securesms.database.helpers
2022-02-01 19:09:04 +00:00
import android.app.Application
2021-11-18 17:36:52 +00:00
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
2022-02-01 19:09:04 +00:00
import org.thoughtcrime.securesms.database.KeyValueDatabase
2021-11-18 17:36:52 +00:00
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
2022-02-01 19:09:04 +00:00
import org.thoughtcrime.securesms.database.requireString
2021-11-18 17:36:52 +00:00
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
2022-02-01 19:09:04 +00:00
import org.whispersystems.signalservice.api.push.ACI
2021-11-18 17:36:52 +00:00
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
2022-01-31 17:46:44 +00:00
private const val MESSAGE _RANGES = 128
2022-02-07 20:47:46 +00:00
private const val REACTION _TRIGGER _FIX = 129
2022-02-01 19:09:04 +00:00
private const val PNI _STORES = 130
2021-11-18 17:36:52 +00:00
2022-02-01 19:09:04 +00:00
const val DATABASE _VERSION = 130
2021-11-18 17:36:52 +00:00
@JvmStatic
2022-02-01 19:09:04 +00:00
fun migrate ( context : Application , db : SQLiteDatabase , oldVersion : Int , newVersion : Int ) {
2021-11-18 17:36:52 +00:00
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 " )
}
2022-01-31 17:46:44 +00:00
if ( oldVersion < MESSAGE _RANGES ) {
db . execSQL ( " ALTER TABLE mms ADD COLUMN ranges BLOB DEFAULT NULL " )
}
2022-02-07 20:47:46 +00:00
if ( oldVersion < REACTION _TRIGGER _FIX ) {
db . execSQL ( " DROP TRIGGER reactions_mms_delete " )
db . execSQL ( " CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 1; END " )
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-02-01 19:09:04 +00:00
if ( oldVersion < PNI _STORES ) {
val localAci : ACI ? = getLocalAci ( context )
// One-Time Prekeys
db . execSQL (
"""
CREATE TABLE one _time _prekeys _tmp (
_id INTEGER PRIMARY KEY ,
account _id TEXT NOT NULL ,
key _id INTEGER ,
public _key TEXT NOT NULL ,
private _key TEXT NOT NULL ,
UNIQUE ( account _id , key _id )
)
""" .trimIndent()
)
if ( localAci != null ) {
db . execSQL (
"""
INSERT INTO one _time _prekeys _tmp ( account _id , key _id , public _key , private _key )
SELECT
' $ localAci ' AS account _id ,
one _time _prekeys . key _id ,
one _time _prekeys . public _key ,
one _time _prekeys . private _key
FROM one _time _prekeys
""" .trimIndent()
)
} else {
Log . w ( TAG , " No local ACI set. Not migrating any existing one-time prekeys. " )
}
db . execSQL ( " DROP TABLE one_time_prekeys " )
db . execSQL ( " ALTER TABLE one_time_prekeys_tmp RENAME TO one_time_prekeys " )
// Signed Prekeys
db . execSQL (
"""
CREATE TABLE signed _prekeys _tmp (
_id INTEGER PRIMARY KEY ,
account _id TEXT NOT NULL ,
key _id INTEGER ,
public _key TEXT NOT NULL ,
private _key TEXT NOT NULL ,
signature TEXT NOT NULL ,
timestamp INTEGER DEFAULT 0 ,
UNIQUE ( account _id , key _id )
)
""" .trimIndent()
)
if ( localAci != null ) {
db . execSQL (
"""
INSERT INTO signed _prekeys _tmp ( account _id , key _id , public _key , private _key , signature , timestamp )
SELECT
' $ localAci ' AS account _id ,
signed _prekeys . key _id ,
signed _prekeys . public _key ,
signed _prekeys . private _key ,
signed _prekeys . signature ,
signed _prekeys . timestamp
FROM signed _prekeys
""" .trimIndent()
)
} else {
Log . w ( TAG , " No local ACI set. Not migrating any existing signed prekeys. " )
}
db . execSQL ( " DROP TABLE signed_prekeys " )
db . execSQL ( " ALTER TABLE signed_prekeys_tmp RENAME TO signed_prekeys " )
2022-02-02 16:53:26 +00:00
// Sessions
db . execSQL (
"""
CREATE TABLE sessions _tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT ,
account _id TEXT NOT NULL ,
address TEXT NOT NULL ,
device INTEGER NOT NULL ,
record BLOB NOT NULL ,
UNIQUE ( account _id , address , device )
)
""" .trimIndent()
)
if ( localAci != null ) {
db . execSQL (
"""
INSERT INTO sessions _tmp ( account _id , address , device , record )
SELECT
' $ localAci ' AS account _id ,
sessions . address ,
sessions . device ,
sessions . record
FROM sessions
""" .trimIndent()
)
} else {
Log . w ( TAG , " No local ACI set. Not migrating any existing sessions. " )
}
db . execSQL ( " DROP TABLE sessions " )
db . execSQL ( " ALTER TABLE sessions_tmp RENAME TO sessions " )
2022-02-01 19:09:04 +00:00
}
2021-11-18 17:36:52 +00:00
}
@JvmStatic
fun migratePostTransaction ( context : Context , oldVersion : Int ) {
if ( oldVersion < MIGRATE _PREKEYS _VERSION ) {
PreKeyMigrationHelper . cleanUpPreKeys ( context )
}
}
2022-02-01 19:09:04 +00:00
/ * *
* Important : You can ' t change this method , or you risk breaking existing migrations . If you need to change this , make a new method .
* /
2021-11-18 17:36:52 +00:00
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! " )
}
}
2022-02-01 19:09:04 +00:00
/ * *
* Important : You can ' t change this method , or you risk breaking existing migrations . If you need to change this , make a new method .
* /
private fun getLocalAci ( context : Application ) : ACI ? {
if ( KeyValueDatabase . exists ( context ) ) {
val keyValueDatabase = KeyValueDatabase . getInstance ( context ) . readableDatabase
keyValueDatabase . query ( " key_value " , arrayOf ( " value " ) , " key = ? " , SqlUtil . buildArgs ( " account.aci " ) , null , null , null ) . use { cursor ->
return if ( cursor . moveToFirst ( ) ) {
ACI . parseOrNull ( cursor . requireString ( " value " ) )
} else {
null
}
}
} else {
return ACI . parseOrNull ( PreferenceManager . getDefaultSharedPreferences ( context ) . getString ( " pref_local_uuid " , null ) )
}
}
2021-11-18 17:36:52 +00:00
}