2017-11-14 02:01:05 +00:00
/ *
2011-12-20 18:20:44 +00:00
* Copyright ( C ) 2011 Whisper Systems
2017-11-14 02:01:05 +00:00
* Copyright ( C ) 2013 - 2017 Open Whisper Systems
2012-09-09 23:10:46 +00:00
*
2011-12-20 18:20:44 +00:00
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
2012-09-09 23:10:46 +00:00
*
2011-12-20 18:20:44 +00:00
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
package org.thoughtcrime.securesms.database ;
import android.content.ContentValues ;
import android.content.Context ;
import android.database.Cursor ;
2014-11-12 19:15:05 +00:00
import android.text.TextUtils ;
2011-12-20 18:20:44 +00:00
2020-02-19 22:08:34 +00:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
2017-10-02 21:54:55 +00:00
import com.annimon.stream.Stream ;
2020-08-24 20:40:47 +00:00
import com.google.android.mms.pdu_alt.NotificationInd ;
2017-10-02 21:54:55 +00:00
2021-09-01 16:21:59 +00:00
import net.zetetic.database.sqlcipher.SQLiteStatement ;
2018-01-25 03:17:44 +00:00
2020-12-04 23:31:58 +00:00
import org.signal.core.util.logging.Log ;
2015-01-15 21:35:35 +00:00
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch ;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList ;
2020-08-24 20:40:47 +00:00
import org.thoughtcrime.securesms.database.documents.NetworkFailure ;
2018-01-25 03:17:44 +00:00
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper ;
2020-11-20 20:42:46 +00:00
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil ;
2021-06-30 21:26:40 +00:00
import org.thoughtcrime.securesms.database.model.MessageId ;
2017-04-22 23:29:26 +00:00
import org.thoughtcrime.securesms.database.model.MessageRecord ;
2019-12-03 21:57:21 +00:00
import org.thoughtcrime.securesms.database.model.ReactionRecord ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
import org.thoughtcrime.securesms.database.model.SmsMessageRecord ;
2020-11-20 20:42:46 +00:00
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails ;
2020-07-15 20:15:15 +00:00
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails ;
2019-10-15 19:47:54 +00:00
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies ;
2020-11-24 15:54:41 +00:00
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange ;
2021-08-24 15:11:48 +00:00
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob ;
2014-11-25 06:50:32 +00:00
import org.thoughtcrime.securesms.jobs.TrimThreadJob ;
2020-08-24 20:40:47 +00:00
import org.thoughtcrime.securesms.mms.IncomingMediaMessage ;
import org.thoughtcrime.securesms.mms.MmsException ;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage ;
2017-08-01 15:56:00 +00:00
import org.thoughtcrime.securesms.recipients.Recipient ;
2019-08-07 18:22:51 +00:00
import org.thoughtcrime.securesms.recipients.RecipientId ;
2020-08-24 20:40:47 +00:00
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo ;
2020-05-06 16:42:54 +00:00
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
import org.thoughtcrime.securesms.sms.IncomingTextMessage ;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage ;
2020-07-15 20:15:15 +00:00
import org.thoughtcrime.securesms.util.Base64 ;
2020-11-11 19:49:48 +00:00
import org.thoughtcrime.securesms.util.CursorUtil ;
2015-01-15 21:35:35 +00:00
import org.thoughtcrime.securesms.util.JsonUtils ;
2020-08-13 14:01:54 +00:00
import org.thoughtcrime.securesms.util.SqlUtil ;
2017-09-16 05:38:53 +00:00
import org.thoughtcrime.securesms.util.TextSecurePreferences ;
2020-11-20 20:42:46 +00:00
import org.thoughtcrime.securesms.util.Util ;
2020-08-20 20:50:14 +00:00
import org.whispersystems.libsignal.util.Pair ;
2017-01-22 21:52:36 +00:00
import org.whispersystems.libsignal.util.guava.Optional ;
2012-09-09 23:10:46 +00:00
2020-10-15 16:55:08 +00:00
import java.io.Closeable ;
2015-01-15 21:35:35 +00:00
import java.io.IOException ;
2020-10-15 16:55:08 +00:00
import java.util.ArrayList ;
2020-06-07 18:46:03 +00:00
import java.util.Collection ;
2019-12-03 21:57:21 +00:00
import java.util.Collections ;
2021-03-26 00:10:53 +00:00
import java.util.HashSet ;
2015-01-15 21:35:35 +00:00
import java.util.LinkedList ;
import java.util.List ;
2017-10-02 21:54:55 +00:00
import java.util.Map ;
2021-09-08 17:38:39 +00:00
import java.util.Objects ;
2012-09-09 23:10:46 +00:00
import java.util.Set ;
2020-11-20 20:42:46 +00:00
import java.util.UUID ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
/ * *
* Database for storage of SMS messages .
2012-09-09 23:10:46 +00:00
*
2011-12-20 18:20:44 +00:00
* @author Moxie Marlinspike
* /
2020-08-20 20:50:14 +00:00
public class SmsDatabase extends MessageDatabase {
2015-01-15 21:35:35 +00:00
2021-03-29 22:37:22 +00:00
private static final String TAG = Log . tag ( SmsDatabase . class ) ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
public static final String TABLE_NAME = "sms" ;
public static final String PERSON = "person" ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
static final String DATE_RECEIVED = "date" ;
static final String DATE_SENT = "date_sent" ;
2011-12-20 18:20:44 +00:00
public static final String PROTOCOL = "protocol" ;
public static final String STATUS = "status" ;
public static final String TYPE = "type" ;
public static final String REPLY_PATH_PRESENT = "reply_path_present" ;
public static final String SUBJECT = "subject" ;
public static final String SERVICE_CENTER = "service_center" ;
2012-09-09 23:10:46 +00:00
2021-07-15 17:17:53 +00:00
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
2019-12-03 21:57:21 +00:00
THREAD_ID + " INTEGER, " +
RECIPIENT_ID + " INTEGER, " +
ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " +
PERSON + " INTEGER, " +
DATE_RECEIVED + " INTEGER, " +
DATE_SENT + " INTEGER, " +
2020-04-13 23:02:50 +00:00
DATE_SERVER + " INTEGER DEFAULT -1, " +
2019-12-03 21:57:21 +00:00
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, " +
2020-04-15 18:56:58 +00:00
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
2020-11-11 19:49:48 +00:00
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
2021-08-26 19:33:13 +00:00
NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " +
SERVER_GUID + " TEXT DEFAULT NULL, " +
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1);" ;
2012-09-09 23:10:46 +00:00
2012-10-30 00:41:06 +00:00
public static final String [ ] CREATE_INDEXS = {
2017-03-09 01:38:55 +00:00
"CREATE INDEX IF NOT EXISTS sms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");" ,
2014-07-25 22:14:29 +00:00
"CREATE INDEX IF NOT EXISTS sms_type_index ON " + TABLE_NAME + " (" + TYPE + ");" ,
2021-06-21 13:51:51 +00:00
"CREATE INDEX IF NOT EXISTS sms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ", " + RECIPIENT_ID + ", " + THREAD_ID + ");" ,
2020-04-13 23:02:50 +00:00
"CREATE INDEX IF NOT EXISTS sms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");" ,
2019-12-06 00:10:37 +00:00
"CREATE INDEX IF NOT EXISTS sms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");" ,
"CREATE INDEX IF NOT EXISTS sms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");"
2012-10-30 00:41:06 +00:00
} ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
private static final String [ ] MESSAGE_PROJECTION = new String [ ] {
2019-08-07 18:22:51 +00:00
ID , THREAD_ID , RECIPIENT_ID , ADDRESS_DEVICE_ID , PERSON ,
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED ,
DATE_SENT + " AS " + NORMALIZED_DATE_SENT ,
2020-04-13 23:02:50 +00:00
DATE_SERVER ,
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
PROTOCOL , READ , STATUS , TYPE ,
2017-09-16 05:38:53 +00:00
REPLY_PATH_PRESENT , SUBJECT , BODY , SERVICE_CENTER , DELIVERY_RECEIPT_COUNT ,
2017-03-09 01:38:55 +00:00
MISMATCHED_IDENTITIES , SUBSCRIPTION_ID , EXPIRES_IN , EXPIRE_STARTED ,
2020-04-15 18:56:58 +00:00
NOTIFIED , READ_RECEIPT_COUNT , UNIDENTIFIED , REACTIONS , REACTIONS_UNREAD , REACTIONS_LAST_SEEN ,
2021-08-30 20:52:46 +00:00
REMOTE_DELETED , NOTIFIED_TIMESTAMP , RECEIPT_TIMESTAMP
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
} ;
2021-08-03 14:28:08 +00:00
private static final long IGNORABLE_TYPESMASK_WHEN_COUNTING = Types . END_SESSION_BIT | Types . KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types . KEY_EXCHANGE_IDENTITY_VERIFIED_BIT ;
2019-11-12 14:18:57 +00:00
2019-08-07 18:22:51 +00:00
private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache ( "SmsDelivery" ) ;
2017-09-16 05:38:53 +00:00
2018-01-25 03:17:44 +00:00
public SmsDatabase ( Context context , SQLCipherOpenHelper databaseHelper ) {
2011-12-20 18:20:44 +00:00
super ( context , databaseHelper ) ;
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2015-01-15 21:35:35 +00:00
protected String getTableName ( ) {
return TABLE_NAME ;
}
2019-11-12 14:18:57 +00:00
@Override
protected String getDateSentColumnName ( ) {
return DATE_SENT ;
}
2020-08-04 17:13:59 +00:00
@Override
protected String getDateReceivedColumnName ( ) {
return DATE_RECEIVED ;
}
2019-11-12 14:18:57 +00:00
@Override
protected String getTypeField ( ) {
return TYPE ;
}
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
private void updateTypeBitmask ( long id , long maskOff , long maskOn ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2012-09-09 23:10:46 +00:00
2021-08-24 15:11:48 +00:00
long threadId ;
db . beginTransaction ( ) ;
try {
db . execSQL ( "UPDATE " + TABLE_NAME +
" SET " + TYPE + " = (" + TYPE + " & " + ( Types . TOTAL_MASK - maskOff ) + " | " + maskOn + " )" +
" WHERE " + ID + " = ?" , SqlUtil . buildArgs ( id ) ) ;
threadId = getThreadIdForMessage ( id ) ;
DatabaseFactory . getThreadDatabase ( context ) . updateSnippetTypeSilently ( threadId ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2012-09-09 23:10:46 +00:00
2021-08-30 19:08:38 +00:00
ApplicationDependencies . getDatabaseObserver ( ) . notifyMessageUpdateObservers ( new MessageId ( id , false ) ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2020-02-19 22:08:34 +00:00
public @Nullable RecipientId getOldestGroupUpdateSender ( long threadId , long minimumDateReceived ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2020-02-19 22:08:34 +00:00
String [ ] columns = new String [ ] { RECIPIENT_ID } ;
String query = THREAD_ID + " = ? AND " + TYPE + " & ? AND " + DATE_RECEIVED + " >= ?" ;
long type = Types . SECURE_MESSAGE_BIT | Types . PUSH_MESSAGE_BIT | Types . GROUP_UPDATE_BIT | Types . BASE_INBOX_TYPE ;
String [ ] args = new String [ ] { String . valueOf ( threadId ) , String . valueOf ( type ) , String . valueOf ( minimumDateReceived ) } ;
String limit = "1" ;
try ( Cursor cursor = db . query ( TABLE_NAME , columns , query , args , null , null , limit ) ) {
if ( cursor . moveToFirst ( ) ) {
2021-02-24 20:07:56 +00:00
return RecipientId . from ( CursorUtil . requireLong ( cursor , RECIPIENT_ID ) ) ;
2020-02-19 22:08:34 +00:00
}
}
return null ;
}
2020-08-20 20:50:14 +00:00
@Override
2011-12-20 18:20:44 +00:00
public long getThreadIdForMessage ( long id ) {
2021-08-18 14:16:54 +00:00
try ( Cursor cursor = databaseHelper . getSignalReadableDatabase ( ) . query ( TABLE_NAME , THREAD_ID_PROJECTION , ID_WHERE , SqlUtil . buildArgs ( id ) , null , null , null ) ) {
2021-08-02 21:46:56 +00:00
if ( cursor . moveToFirst ( ) ) {
return CursorUtil . requireLong ( cursor , THREAD_ID ) ;
}
2011-12-20 18:20:44 +00:00
}
2021-08-02 21:46:56 +00:00
return - 1 ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2011-12-20 18:20:44 +00:00
public int getMessageCountForThread ( long threadId ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2012-09-09 23:10:46 +00:00
2021-07-29 18:07:39 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , COUNT , THREAD_ID_WHERE , SqlUtil . buildArgs ( threadId ) , null , null , null ) ) {
2020-02-21 18:52:27 +00:00
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
2012-10-01 02:56:29 +00:00
return cursor . getInt ( 0 ) ;
2020-02-21 18:52:27 +00:00
}
}
return 0 ;
}
2020-08-20 20:50:14 +00:00
@Override
2020-02-21 18:52:27 +00:00
public int getMessageCountForThread ( long threadId , long beforeTime ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2020-02-21 18:52:27 +00:00
String [ ] cols = new String [ ] { "COUNT(*)" } ;
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ?" ;
String [ ] args = new String [ ] { String . valueOf ( threadId ) , String . valueOf ( beforeTime ) } ;
try ( Cursor cursor = db . query ( TABLE_NAME , cols , query , args , null , null , null ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return cursor . getInt ( 0 ) ;
}
2011-12-20 18:20:44 +00:00
}
return 0 ;
}
2012-09-09 23:10:46 +00:00
2021-05-18 19:19:33 +00:00
@Override
public boolean hasMeaningfulMessage ( long threadId ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2021-05-18 19:19:33 +00:00
SqlUtil . Query query = buildMeaningfulMessagesQuery ( threadId ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { "1" } , query . getWhere ( ) , query . getWhereArgs ( ) , null , null , null , "1" ) ) {
return cursor ! = null & & cursor . moveToFirst ( ) ;
}
}
private @NonNull SqlUtil . Query buildMeaningfulMessagesQuery ( long threadId ) {
2021-09-08 17:38:39 +00:00
String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ? AND TYPE != ?)" ;
return SqlUtil . buildQuery ( query , threadId , IGNORABLE_TYPESMASK_WHEN_COUNTING , Types . PROFILE_CHANGE_TYPE , Types . CHANGE_NUMBER_TYPE ) ;
2021-05-18 19:19:33 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2015-01-15 21:35:35 +00:00
public void markAsEndSession ( long id ) {
updateTypeBitmask ( id , Types . KEY_EXCHANGE_MASK , Types . END_SESSION_BIT ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2013-11-26 01:00:20 +00:00
public void markAsInvalidVersionKeyExchange ( long id ) {
updateTypeBitmask ( id , 0 , Types . KEY_EXCHANGE_INVALID_VERSION_BIT ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2013-11-18 21:16:18 +00:00
public void markAsSecure ( long id ) {
updateTypeBitmask ( id , 0 , Types . SECURE_MESSAGE_BIT ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2014-04-01 01:47:24 +00:00
public void markAsInsecure ( long id ) {
updateTypeBitmask ( id , Types . SECURE_MESSAGE_BIT , 0 ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2014-02-21 07:00:38 +00:00
public void markAsPush ( long id ) {
updateTypeBitmask ( id , 0 , Types . PUSH_MESSAGE_BIT ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2014-03-01 22:17:55 +00:00
public void markAsForcedSms ( long id ) {
2015-06-03 22:51:27 +00:00
updateTypeBitmask ( id , Types . PUSH_MESSAGE_BIT , Types . MESSAGE_FORCE_SMS_BIT ) ;
2014-03-01 22:17:55 +00:00
}
2021-05-05 16:49:18 +00:00
@Override
public void markAsRateLimited ( long id ) {
updateTypeBitmask ( id , 0 , Types . MESSAGE_RATE_LIMITED_BIT ) ;
}
@Override
public void clearRateLimitStatus ( @NonNull Collection < Long > ids ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2021-05-05 16:49:18 +00:00
db . beginTransaction ( ) ;
try {
for ( long id : ids ) {
updateTypeBitmask ( id , Types . MESSAGE_RATE_LIMITED_BIT , 0 ) ;
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
}
2020-08-20 20:50:14 +00:00
@Override
2011-12-20 18:20:44 +00:00
public void markAsDecryptFailed ( long id ) {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
updateTypeBitmask ( id , Types . ENCRYPTION_MASK , Types . ENCRYPTION_REMOTE_FAILED_BIT ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2011-12-20 18:20:44 +00:00
public void markAsNoSession ( long id ) {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
updateTypeBitmask ( id , Types . ENCRYPTION_MASK , Types . ENCRYPTION_REMOTE_NO_SESSION_BIT ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2019-05-28 19:21:19 +00:00
public void markAsUnsupportedProtocolVersion ( long id ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . UNSUPPORTED_MESSAGE_TYPE ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2019-06-11 06:18:45 +00:00
public void markAsInvalidMessage ( long id ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . INVALID_MESSAGE_TYPE ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2014-04-10 22:20:43 +00:00
public void markAsLegacyVersion ( long id ) {
updateTypeBitmask ( id , Types . ENCRYPTION_MASK , Types . ENCRYPTION_REMOTE_LEGACY_BIT ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2013-05-06 02:11:11 +00:00
public void markAsOutbox ( long id ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . BASE_OUTBOX_TYPE ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2014-04-01 01:47:24 +00:00
public void markAsPendingInsecureSmsFallback ( long id ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . BASE_PENDING_INSECURE_SMS_FALLBACK ) ;
2014-03-01 22:17:55 +00:00
}
2017-10-02 21:54:55 +00:00
@Override
2016-12-21 17:49:01 +00:00
public void markAsSent ( long id , boolean isSecure ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . BASE_SENT_TYPE | ( isSecure ? Types . PUSH_MESSAGE_BIT | Types . SECURE_MESSAGE_BIT : 0 ) ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-04-15 18:56:58 +00:00
@Override
2018-10-11 23:45:22 +00:00
public void markAsSending ( long id ) {
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . BASE_SENDING_TYPE ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2020-11-04 14:42:45 +00:00
public void markAsMissedCall ( long id , boolean isVideoOffer ) {
updateTypeBitmask ( id , Types . TOTAL_MASK , isVideoOffer ? Types . MISSED_VIDEO_CALL_TYPE : Types . MISSED_AUDIO_CALL_TYPE ) ;
2016-11-09 17:37:40 +00:00
}
2020-04-15 18:56:58 +00:00
@Override
public void markAsRemoteDelete ( long id ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-04-15 18:56:58 +00:00
2021-06-24 15:22:20 +00:00
long threadId ;
2020-04-15 18:56:58 +00:00
2021-06-24 15:22:20 +00:00
db . beginTransaction ( ) ;
try {
ContentValues values = new ContentValues ( ) ;
values . put ( REMOTE_DELETED , 1 ) ;
values . putNull ( BODY ) ;
values . putNull ( REACTIONS ) ;
db . update ( TABLE_NAME , values , ID_WHERE , new String [ ] { String . valueOf ( id ) } ) ;
2020-04-15 18:56:58 +00:00
2021-06-24 15:22:20 +00:00
threadId = getThreadIdForMessage ( id ) ;
DatabaseFactory . getThreadDatabase ( context ) . update ( threadId , false ) ;
DatabaseFactory . getMessageLogDatabase ( context ) . deleteAllRelatedToMessage ( id , false ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2020-04-15 18:56:58 +00:00
notifyConversationListeners ( threadId ) ;
}
2018-10-11 23:45:22 +00:00
@Override
2018-05-22 09:13:10 +00:00
public void markUnidentified ( long id , boolean unidentified ) {
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( UNIDENTIFIED , unidentified ? 1 : 0 ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2018-05-22 09:13:10 +00:00
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { String . valueOf ( id ) } ) ;
}
2017-10-02 21:54:55 +00:00
@Override
2016-08-16 03:23:56 +00:00
public void markExpireStarted ( long id ) {
markExpireStarted ( id , System . currentTimeMillis ( ) ) ;
}
2017-10-02 21:54:55 +00:00
@Override
2016-08-16 03:23:56 +00:00
public void markExpireStarted ( long id , long startedAtTimestamp ) {
2020-06-07 18:46:03 +00:00
markExpireStarted ( Collections . singleton ( id ) , startedAtTimestamp ) ;
}
2016-08-16 03:23:56 +00:00
2020-06-07 18:46:03 +00:00
@Override
public void markExpireStarted ( Collection < Long > ids , long startedAtTimestamp ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-06-07 18:46:03 +00:00
long threadId = - 1 ;
2016-08-16 03:23:56 +00:00
2020-06-07 18:46:03 +00:00
db . beginTransaction ( ) ;
try {
2020-06-19 14:17:23 +00:00
String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)" ;
2020-06-07 18:46:03 +00:00
for ( long id : ids ) {
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( EXPIRE_STARTED , startedAtTimestamp ) ;
2020-06-19 14:17:23 +00:00
db . update ( TABLE_NAME , contentValues , query , new String [ ] { String . valueOf ( id ) , String . valueOf ( startedAtTimestamp ) } ) ;
2020-06-07 18:46:03 +00:00
if ( threadId < 0 ) {
threadId = getThreadIdForMessage ( id ) ;
}
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2016-08-16 03:23:56 +00:00
DatabaseFactory . getThreadDatabase ( context ) . update ( threadId , false ) ;
notifyConversationListeners ( threadId ) ;
}
2020-08-20 20:50:14 +00:00
@Override
public void markSmsStatus ( long id , int status ) {
2020-05-11 14:03:42 +00:00
Log . i ( TAG , "Updating ID: " + id + " to status: " + status ) ;
2013-01-07 05:38:36 +00:00
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( STATUS , status ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2013-01-07 05:38:36 +00:00
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { id + "" } ) ;
2015-12-09 18:53:19 +00:00
long threadId = getThreadIdForMessage ( id ) ;
DatabaseFactory . getThreadDatabase ( context ) . update ( threadId , false ) ;
notifyConversationListeners ( threadId ) ;
2013-01-07 05:38:36 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2011-12-20 18:20:44 +00:00
public void markAsSentFailed ( long id ) {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
updateTypeBitmask ( id , Types . BASE_TYPE_MASK , Types . BASE_SENT_FAILED_TYPE ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2017-03-09 01:38:55 +00:00
public void markAsNotified ( long id ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase database = databaseHelper . getSignalWritableDatabase ( ) ;
2017-03-09 01:38:55 +00:00
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( NOTIFIED , 1 ) ;
2021-04-26 19:51:23 +00:00
contentValues . put ( REACTIONS_LAST_SEEN , System . currentTimeMillis ( ) ) ;
2017-03-09 01:38:55 +00:00
database . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { String . valueOf ( id ) } ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2021-03-26 00:10:53 +00:00
public @NonNull Set < ThreadUpdate > incrementReceiptCount ( SyncMessageId messageId , long timestamp , @NonNull ReceiptType receiptType ) {
2020-11-20 13:16:37 +00:00
if ( receiptType = = ReceiptType . VIEWED ) {
2021-03-26 00:10:53 +00:00
return Collections . emptySet ( ) ;
2020-11-20 13:16:37 +00:00
}
2021-08-18 14:16:54 +00:00
SQLiteDatabase database = databaseHelper . getSignalWritableDatabase ( ) ;
2021-03-26 00:10:53 +00:00
Set < ThreadUpdate > threadUpdates = new HashSet < > ( ) ;
2014-07-25 22:14:29 +00:00
2021-08-26 19:33:13 +00:00
try ( Cursor cursor = database . query ( TABLE_NAME , new String [ ] { ID , THREAD_ID , RECIPIENT_ID , TYPE , DELIVERY_RECEIPT_COUNT , READ_RECEIPT_COUNT , RECEIPT_TIMESTAMP } ,
2016-02-20 01:07:41 +00:00
DATE_SENT + " = ?" , new String [ ] { String . valueOf ( messageId . getTimetamp ( ) ) } ,
2020-06-09 15:52:58 +00:00
null , null , null , null ) ) {
2014-07-25 22:14:29 +00:00
while ( cursor . moveToNext ( ) ) {
2021-08-26 19:33:13 +00:00
if ( Types . isOutgoingMessageType ( CursorUtil . requireLong ( cursor , TYPE ) ) ) {
2019-08-07 18:22:51 +00:00
RecipientId theirRecipientId = messageId . getRecipientId ( ) ;
2021-08-26 19:33:13 +00:00
RecipientId outRecipientId = RecipientId . from ( CursorUtil . requireLong ( cursor , RECIPIENT_ID ) ) ;
2017-07-26 16:59:15 +00:00
2019-08-07 18:22:51 +00:00
if ( outRecipientId . equals ( theirRecipientId ) ) {
2021-08-26 19:33:13 +00:00
long id = CursorUtil . requireLong ( cursor , ID ) ;
long threadId = CursorUtil . requireLong ( cursor , THREAD_ID ) ;
2021-03-26 00:10:53 +00:00
String columnName = receiptType . getColumnName ( ) ;
2021-08-26 19:33:13 +00:00
boolean isFirstIncrement = CursorUtil . requireLong ( cursor , columnName ) = = 0 ;
long savedTimestamp = CursorUtil . requireLong ( cursor , RECEIPT_TIMESTAMP ) ;
long updatedTimestamp = isFirstIncrement ? Math . max ( savedTimestamp , timestamp ) : savedTimestamp ;
2017-07-26 16:59:15 +00:00
database . execSQL ( "UPDATE " + TABLE_NAME +
2021-08-26 19:33:13 +00:00
" SET " + columnName + " = " + columnName + " + 1, " +
RECEIPT_TIMESTAMP + " = ? WHERE " +
2017-07-26 16:59:15 +00:00
ID + " = ?" ,
2021-08-26 19:33:13 +00:00
SqlUtil . buildArgs ( updatedTimestamp , id ) ) ;
2017-07-26 16:59:15 +00:00
2021-03-26 00:10:53 +00:00
threadUpdates . add ( new ThreadUpdate ( threadId , ! isFirstIncrement ) ) ;
2014-07-25 22:14:29 +00:00
}
}
}
2015-12-09 04:32:54 +00:00
2021-03-26 00:10:53 +00:00
if ( threadUpdates . size ( ) > 0 & & receiptType = = ReceiptType . DELIVERY ) {
2021-08-26 19:33:13 +00:00
earlyDeliveryReceiptCache . increment ( messageId . getTimetamp ( ) , messageId . getRecipientId ( ) , timestamp ) ;
2015-12-09 04:32:54 +00:00
}
2021-03-26 00:10:53 +00:00
return threadUpdates ;
2014-07-25 22:14:29 +00:00
}
}
2020-08-20 20:50:14 +00:00
@Override
2021-04-14 19:42:34 +00:00
public List < Pair < Long , Long > > setTimestampRead ( SyncMessageId messageId , long proposedExpireStarted , @NonNull Map < Long , Long > threadToLatestRead ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase database = databaseHelper . getSignalWritableDatabase ( ) ;
2016-10-02 19:08:30 +00:00
List < Pair < Long , Long > > expiring = new LinkedList < > ( ) ;
Cursor cursor = null ;
2016-02-20 01:07:41 +00:00
try {
2019-08-07 18:22:51 +00:00
cursor = database . query ( TABLE_NAME , new String [ ] { ID , THREAD_ID , RECIPIENT_ID , TYPE , EXPIRES_IN , EXPIRE_STARTED } ,
2016-02-20 01:07:41 +00:00
DATE_SENT + " = ?" , new String [ ] { String . valueOf ( messageId . getTimetamp ( ) ) } ,
null , null , null , null ) ;
while ( cursor . moveToNext ( ) ) {
2019-08-07 18:22:51 +00:00
RecipientId theirRecipientId = messageId . getRecipientId ( ) ;
RecipientId outRecipientId = RecipientId . from ( cursor . getLong ( cursor . getColumnIndexOrThrow ( RECIPIENT_ID ) ) ) ;
2016-02-20 01:07:41 +00:00
2021-02-05 21:15:05 +00:00
if ( outRecipientId . equals ( theirRecipientId ) | | theirRecipientId . equals ( Recipient . self ( ) . getId ( ) ) ) {
2018-06-28 22:51:27 +00:00
long id = cursor . getLong ( cursor . getColumnIndexOrThrow ( ID ) ) ;
long threadId = cursor . getLong ( cursor . getColumnIndexOrThrow ( THREAD_ID ) ) ;
long expiresIn = cursor . getLong ( cursor . getColumnIndexOrThrow ( EXPIRES_IN ) ) ;
long expireStarted = cursor . getLong ( cursor . getColumnIndexOrThrow ( EXPIRE_STARTED ) ) ;
expireStarted = expireStarted > 0 ? Math . min ( proposedExpireStarted , expireStarted ) : proposedExpireStarted ;
2016-02-20 01:07:41 +00:00
2017-07-26 16:59:15 +00:00
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( READ , 1 ) ;
2021-02-05 21:15:05 +00:00
contentValues . put ( REACTIONS_UNREAD , 0 ) ;
contentValues . put ( REACTIONS_LAST_SEEN , System . currentTimeMillis ( ) ) ;
2012-09-09 23:10:46 +00:00
2017-07-26 16:59:15 +00:00
if ( expiresIn > 0 ) {
contentValues . put ( EXPIRE_STARTED , expireStarted ) ;
expiring . add ( new Pair < > ( id , expiresIn ) ) ;
}
2016-10-02 19:08:30 +00:00
2017-07-26 16:59:15 +00:00
database . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { cursor . getLong ( cursor . getColumnIndexOrThrow ( ID ) ) + "" } ) ;
2016-02-20 01:07:41 +00:00
2017-07-26 16:59:15 +00:00
DatabaseFactory . getThreadDatabase ( context ) . updateReadState ( threadId ) ;
DatabaseFactory . getThreadDatabase ( context ) . setLastSeen ( threadId ) ;
notifyConversationListeners ( threadId ) ;
2021-04-14 19:42:34 +00:00
Long latest = threadToLatestRead . get ( threadId ) ;
threadToLatestRead . put ( threadId , ( latest ! = null ) ? Math . max ( latest , messageId . getTimetamp ( ) ) : messageId . getTimetamp ( ) ) ;
2016-02-20 01:07:41 +00:00
}
}
} finally {
if ( cursor ! = null ) cursor . close ( ) ;
}
2016-10-02 19:08:30 +00:00
return expiring ;
2013-05-06 20:59:40 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2019-11-19 16:01:07 +00:00
public List < MarkedMessageInfo > setEntireThreadRead ( long threadId ) {
return setMessagesRead ( THREAD_ID + " = ?" , new String [ ] { String . valueOf ( threadId ) } ) ;
}
2020-08-20 20:50:14 +00:00
@Override
2020-08-04 17:13:59 +00:00
public List < MarkedMessageInfo > setMessagesReadSince ( long threadId , long sinceTimestamp ) {
if ( sinceTimestamp = = - 1 ) {
2021-02-05 21:15:05 +00:00
return setMessagesRead ( THREAD_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause ( ) + ")))" , new String [ ] { String . valueOf ( threadId ) } ) ;
2020-08-04 17:13:59 +00:00
} else {
2021-02-05 21:15:05 +00:00
return setMessagesRead ( THREAD_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause ( ) + " ))) AND " + DATE_RECEIVED + " <= ?" , new String [ ] { String . valueOf ( threadId ) , String . valueOf ( sinceTimestamp ) } ) ;
2020-08-04 17:13:59 +00:00
}
2017-10-09 01:09:46 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2017-10-09 01:09:46 +00:00
public List < MarkedMessageInfo > setAllMessagesRead ( ) {
2021-02-05 21:15:05 +00:00
return setMessagesRead ( READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause ( ) + "))" , null ) ;
2017-10-09 01:09:46 +00:00
}
private List < MarkedMessageInfo > setMessagesRead ( String where , String [ ] arguments ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase database = databaseHelper . getSignalWritableDatabase ( ) ;
2017-10-09 01:09:46 +00:00
List < MarkedMessageInfo > results = new LinkedList < > ( ) ;
Cursor cursor = null ;
database . beginTransaction ( ) ;
try {
2020-02-19 22:08:34 +00:00
cursor = database . query ( TABLE_NAME , new String [ ] { ID , RECIPIENT_ID , DATE_SENT , TYPE , EXPIRES_IN , EXPIRE_STARTED , THREAD_ID } , where , arguments , null , null , null ) ;
2017-10-09 01:09:46 +00:00
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
2021-02-05 21:15:05 +00:00
if ( Types . isSecureType ( CursorUtil . requireLong ( cursor , TYPE ) ) ) {
2021-02-24 20:07:56 +00:00
long threadId = CursorUtil . requireLong ( cursor , THREAD_ID ) ;
RecipientId recipientId = RecipientId . from ( CursorUtil . requireLong ( cursor , RECIPIENT_ID ) ) ;
long dateSent = CursorUtil . requireLong ( cursor , DATE_SENT ) ;
long messageId = CursorUtil . requireLong ( cursor , ID ) ;
long expiresIn = CursorUtil . requireLong ( cursor , EXPIRES_IN ) ;
long expireStarted = CursorUtil . requireLong ( cursor , EXPIRE_STARTED ) ;
2020-02-19 22:08:34 +00:00
SyncMessageId syncMessageId = new SyncMessageId ( recipientId , dateSent ) ;
ExpirationInfo expirationInfo = new ExpirationInfo ( messageId , expiresIn , expireStarted , false ) ;
2021-06-30 21:26:40 +00:00
results . add ( new MarkedMessageInfo ( threadId , syncMessageId , new MessageId ( messageId , false ) , expirationInfo ) ) ;
2017-10-09 01:09:46 +00:00
}
}
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( READ , 1 ) ;
2021-02-05 21:15:05 +00:00
contentValues . put ( REACTIONS_UNREAD , 0 ) ;
contentValues . put ( REACTIONS_LAST_SEEN , System . currentTimeMillis ( ) ) ;
2012-09-09 23:10:46 +00:00
2017-10-09 01:09:46 +00:00
database . update ( TABLE_NAME , contentValues , where , arguments ) ;
database . setTransactionSuccessful ( ) ;
} finally {
if ( cursor ! = null ) cursor . close ( ) ;
database . endTransaction ( ) ;
}
return results ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2021-06-30 18:36:00 +00:00
public InsertResult updateBundleMessageBody ( long messageId , String body ) {
2018-01-25 03:17:44 +00:00
long type = Types . BASE_INBOX_TYPE | Types . SECURE_MESSAGE_BIT | Types . PUSH_MESSAGE_BIT ;
return updateMessageBodyAndType ( messageId , body , Types . TOTAL_MASK , type ) ;
}
2020-11-20 13:16:37 +00:00
@Override
public @NonNull List < MarkedMessageInfo > getViewedIncomingMessages ( long threadId ) {
return Collections . emptyList ( ) ;
}
@Override
public @Nullable MarkedMessageInfo setIncomingMessageViewed ( long messageId ) {
return null ;
}
2021-04-28 19:21:34 +00:00
@Override
public @NonNull List < MarkedMessageInfo > setIncomingMessagesViewed ( @NonNull List < Long > messageIds ) {
return Collections . emptyList ( ) ;
}
2021-06-30 18:36:00 +00:00
private InsertResult updateMessageBodyAndType ( long messageId , String body , long maskOff , long maskOn ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
db . execSQL ( "UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +
2015-01-15 21:35:35 +00:00
TYPE + " = (" + TYPE + " & " + ( Types . TOTAL_MASK - maskOff ) + " | " + maskOn + ") " +
"WHERE " + ID + " = ?" ,
new String [ ] { body , messageId + "" } ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
long threadId = getThreadIdForMessage ( messageId ) ;
2012-09-09 23:10:46 +00:00
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
notifyConversationListeners ( threadId ) ;
2015-05-18 22:16:27 +00:00
2021-06-30 18:36:00 +00:00
return new InsertResult ( messageId , threadId ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
public boolean hasReceivedAnyCallsSince ( long threadId , long timestamp ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2020-11-04 14:42:45 +00:00
String [ ] projection = SqlUtil . buildArgs ( SmsDatabase . TYPE ) ;
2020-11-05 17:41:22 +00:00
String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)" ;
2020-11-04 14:42:45 +00:00
String [ ] selectionArgs = SqlUtil . buildArgs ( threadId ,
timestamp ,
2020-11-05 17:41:22 +00:00
Types . INCOMING_AUDIO_CALL_TYPE ,
Types . INCOMING_VIDEO_CALL_TYPE ,
2020-11-04 14:42:45 +00:00
Types . MISSED_AUDIO_CALL_TYPE ,
Types . MISSED_VIDEO_CALL_TYPE ) ;
2019-10-16 16:26:26 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , projection , selection , selectionArgs , null , null , null ) ) {
return cursor ! = null & & cursor . moveToFirst ( ) ;
}
}
2020-08-20 20:50:14 +00:00
@Override
2020-11-05 17:41:22 +00:00
public @NonNull Pair < Long , Long > insertReceivedCall ( @NonNull RecipientId address , boolean isVideoOffer ) {
return insertCallLog ( address , isVideoOffer ? Types . INCOMING_VIDEO_CALL_TYPE : Types . INCOMING_AUDIO_CALL_TYPE , false , System . currentTimeMillis ( ) ) ;
2015-09-21 17:54:23 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2020-11-05 17:41:22 +00:00
public @NonNull Pair < Long , Long > insertOutgoingCall ( @NonNull RecipientId address , boolean isVideoOffer ) {
return insertCallLog ( address , isVideoOffer ? Types . OUTGOING_VIDEO_CALL_TYPE : Types . OUTGOING_AUDIO_CALL_TYPE , false , System . currentTimeMillis ( ) ) ;
2015-09-21 17:54:23 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2020-11-05 17:41:22 +00:00
public @NonNull Pair < Long , Long > insertMissedCall ( @NonNull RecipientId address , long timestamp , boolean isVideoOffer ) {
return insertCallLog ( address , isVideoOffer ? Types . MISSED_VIDEO_CALL_TYPE : Types . MISSED_AUDIO_CALL_TYPE , true , timestamp ) ;
2015-09-21 17:54:23 +00:00
}
2020-11-20 20:42:46 +00:00
@Override
public void insertOrUpdateGroupCall ( @NonNull RecipientId groupRecipientId ,
2020-11-24 18:23:25 +00:00
@NonNull RecipientId sender ,
long timestamp ,
@Nullable String peekGroupCallEraId ,
2020-12-02 18:20:38 +00:00
@NonNull Collection < UUID > peekJoinedUuids ,
boolean isCallFull )
2020-11-20 20:42:46 +00:00
{
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-12-08 21:43:43 +00:00
Recipient recipient = Recipient . resolved ( groupRecipientId ) ;
2021-07-27 17:52:49 +00:00
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( recipient ) ;
2020-12-08 21:43:43 +00:00
boolean peerEraIdSameAsPrevious = updatePreviousGroupCall ( threadId , peekGroupCallEraId , peekJoinedUuids , isCallFull ) ;
2020-11-20 20:42:46 +00:00
try {
db . beginTransaction ( ) ;
if ( ! peerEraIdSameAsPrevious & & ! Util . isEmpty ( peekGroupCallEraId ) ) {
2020-11-24 18:23:25 +00:00
Recipient self = Recipient . self ( ) ;
boolean markRead = peekJoinedUuids . contains ( self . requireUuid ( ) ) | | self . getId ( ) . equals ( sender ) ;
2020-11-20 20:42:46 +00:00
byte [ ] updateDetails = GroupCallUpdateDetails . newBuilder ( )
. setEraId ( Util . emptyIfNull ( peekGroupCallEraId ) )
. setStartedCallUuid ( Recipient . resolved ( sender ) . requireUuid ( ) . toString ( ) )
. setStartedCallTimestamp ( timestamp )
. addAllInCallUuids ( Stream . of ( peekJoinedUuids ) . map ( UUID : : toString ) . toList ( ) )
2020-12-02 18:20:38 +00:00
. setIsCallFull ( isCallFull )
2020-11-20 20:42:46 +00:00
. build ( )
. toByteArray ( ) ;
String body = Base64 . encodeBytes ( updateDetails ) ;
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , sender . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , timestamp ) ;
values . put ( DATE_SENT , timestamp ) ;
2020-11-24 18:23:25 +00:00
values . put ( READ , markRead ? 1 : 0 ) ;
2020-11-20 20:42:46 +00:00
values . put ( BODY , body ) ;
values . put ( TYPE , Types . GROUP_CALL_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
db . insert ( TABLE_NAME , null , values ) ;
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
}
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2020-11-20 20:42:46 +00:00
db . setTransactionSuccessful ( ) ;
} finally {
2020-12-07 22:27:35 +00:00
db . endTransaction ( ) ;
}
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2020-12-07 22:27:35 +00:00
}
@Override
public void insertOrUpdateGroupCall ( @NonNull RecipientId groupRecipientId ,
@NonNull RecipientId sender ,
long timestamp ,
@Nullable String messageGroupCallEraId )
{
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-12-07 22:27:35 +00:00
long threadId ;
try {
db . beginTransaction ( ) ;
Recipient recipient = Recipient . resolved ( groupRecipientId ) ;
2021-07-27 17:52:49 +00:00
threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( recipient ) ;
2020-12-07 22:27:35 +00:00
String where = TYPE + " = ? AND " + THREAD_ID + " = ?" ;
String [ ] args = SqlUtil . buildArgs ( Types . GROUP_CALL_TYPE , threadId ) ;
boolean sameEraId = false ;
try ( Reader reader = new Reader ( db . query ( TABLE_NAME , MESSAGE_PROJECTION , where , args , null , null , DATE_RECEIVED + " DESC" , "1" ) ) ) {
MessageRecord record = reader . getNext ( ) ;
if ( record ! = null ) {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil . parse ( record . getBody ( ) ) ;
sameEraId = groupCallUpdateDetails . getEraId ( ) . equals ( messageGroupCallEraId ) & & ! Util . isEmpty ( messageGroupCallEraId ) ;
if ( ! sameEraId ) {
String body = GroupCallUpdateDetailsUtil . createUpdatedBody ( groupCallUpdateDetails , Collections . emptyList ( ) , false ) ;
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( BODY , body ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , SqlUtil . buildArgs ( record . getId ( ) ) ) ;
}
}
}
if ( ! sameEraId & & ! Util . isEmpty ( messageGroupCallEraId ) ) {
byte [ ] updateDetails = GroupCallUpdateDetails . newBuilder ( )
. setEraId ( Util . emptyIfNull ( messageGroupCallEraId ) )
. setStartedCallUuid ( Recipient . resolved ( sender ) . requireUuid ( ) . toString ( ) )
. setStartedCallTimestamp ( timestamp )
. addAllInCallUuids ( Collections . emptyList ( ) )
. setIsCallFull ( false )
. build ( )
. toByteArray ( ) ;
String body = Base64 . encodeBytes ( updateDetails ) ;
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , sender . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , timestamp ) ;
values . put ( DATE_SENT , timestamp ) ;
values . put ( READ , 0 ) ;
values . put ( BODY , body ) ;
values . put ( TYPE , Types . GROUP_CALL_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
db . insert ( TABLE_NAME , null , values ) ;
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
}
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2020-12-07 22:27:35 +00:00
db . setTransactionSuccessful ( ) ;
} finally {
2020-11-20 20:42:46 +00:00
db . endTransaction ( ) ;
}
2020-12-03 17:50:56 +00:00
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2020-11-20 20:42:46 +00:00
}
2020-12-02 18:20:38 +00:00
@Override
public boolean updatePreviousGroupCall ( long threadId , @Nullable String peekGroupCallEraId , @NonNull Collection < UUID > peekJoinedUuids , boolean isCallFull ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-12-08 21:43:43 +00:00
String where = TYPE + " = ? AND " + THREAD_ID + " = ?" ;
String [ ] args = SqlUtil . buildArgs ( Types . GROUP_CALL_TYPE , threadId ) ;
boolean sameEraId = false ;
2020-11-20 20:42:46 +00:00
try ( Reader reader = new Reader ( db . query ( TABLE_NAME , MESSAGE_PROJECTION , where , args , null , null , DATE_RECEIVED + " DESC" , "1" ) ) ) {
MessageRecord record = reader . getNext ( ) ;
if ( record = = null ) {
return false ;
}
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil . parse ( record . getBody ( ) ) ;
2020-11-24 18:23:25 +00:00
boolean containsSelf = peekJoinedUuids . contains ( Recipient . self ( ) . requireUuid ( ) ) ;
2020-11-20 20:42:46 +00:00
2020-12-08 21:43:43 +00:00
sameEraId = groupCallUpdateDetails . getEraId ( ) . equals ( peekGroupCallEraId ) & & ! Util . isEmpty ( peekGroupCallEraId ) ;
2020-11-20 20:42:46 +00:00
List < String > inCallUuids = sameEraId ? Stream . of ( peekJoinedUuids ) . map ( UUID : : toString ) . toList ( )
: Collections . emptyList ( ) ;
2020-12-02 18:20:38 +00:00
String body = GroupCallUpdateDetailsUtil . createUpdatedBody ( groupCallUpdateDetails , inCallUuids , isCallFull ) ;
2020-11-20 20:42:46 +00:00
ContentValues contentValues = new ContentValues ( ) ;
contentValues . put ( BODY , body ) ;
2020-11-24 18:23:25 +00:00
if ( sameEraId & & containsSelf ) {
contentValues . put ( READ , 1 ) ;
}
2021-06-17 14:51:09 +00:00
SqlUtil . Query query = SqlUtil . buildTrueUpdateQuery ( ID_WHERE , SqlUtil . buildArgs ( record . getId ( ) ) , contentValues ) ;
boolean updated = db . update ( TABLE_NAME , contentValues , query . getWhere ( ) , query . getWhereArgs ( ) ) > 0 ;
2020-12-08 21:43:43 +00:00
2021-06-17 14:51:09 +00:00
if ( updated ) {
notifyConversationListeners ( threadId ) ;
}
}
2020-12-08 21:43:43 +00:00
return sameEraId ;
2020-11-20 20:42:46 +00:00
}
2020-09-17 06:52:48 +00:00
private @NonNull Pair < Long , Long > insertCallLog ( @NonNull RecipientId recipientId , long type , boolean unread , long timestamp ) {
2019-08-07 18:22:51 +00:00
Recipient recipient = Recipient . resolved ( recipientId ) ;
2021-07-27 17:52:49 +00:00
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( recipient ) ;
2015-09-21 17:54:23 +00:00
ContentValues values = new ContentValues ( 6 ) ;
2019-08-07 18:22:51 +00:00
values . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
2015-09-21 17:54:23 +00:00
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
2020-09-17 06:52:48 +00:00
values . put ( DATE_SENT , timestamp ) ;
2015-09-21 17:54:23 +00:00
values . put ( READ , unread ? 0 : 1 ) ;
values . put ( TYPE , type ) ;
values . put ( THREAD_ID , threadId ) ;
2021-08-24 15:11:48 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
long messageId = db . insert ( TABLE_NAME , null , values ) ;
2015-09-21 17:54:23 +00:00
if ( unread ) {
2017-11-14 02:01:05 +00:00
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
2015-09-21 17:54:23 +00:00
}
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2020-07-06 20:27:03 +00:00
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2020-07-06 20:27:03 +00:00
2015-09-21 17:54:23 +00:00
return new Pair < > ( messageId , threadId ) ;
}
2021-05-05 16:49:18 +00:00
@Override
public Set < Long > getAllRateLimitedMessageIds ( ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2021-05-05 16:49:18 +00:00
String where = "(" + TYPE + " & " + Types . TOTAL_MASK + " & " + Types . MESSAGE_RATE_LIMITED_BIT + ") > 0" ;
Set < Long > ids = new HashSet < > ( ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { ID } , where , null , null , null , null ) ) {
while ( cursor . moveToNext ( ) ) {
ids . add ( CursorUtil . requireLong ( cursor , ID ) ) ;
}
}
return ids ;
}
2020-11-04 20:00:12 +00:00
@Override
public List < MessageRecord > getProfileChangeDetailsRecords ( long threadId , long afterTimestamp ) {
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?" ;
String [ ] args = SqlUtil . buildArgs ( threadId , afterTimestamp , Types . PROFILE_CHANGE_TYPE ) ;
try ( Reader reader = readerFor ( queryMessages ( where , args , true , - 1 ) ) ) {
List < MessageRecord > results = new ArrayList < > ( reader . getCount ( ) ) ;
while ( reader . getNext ( ) ! = null ) {
results . add ( reader . getCurrent ( ) ) ;
}
return results ;
}
}
2020-08-20 20:50:14 +00:00
@Override
2020-07-15 20:15:15 +00:00
public void insertProfileNameChangeMessages ( @NonNull Recipient recipient , @NonNull String newProfileName , @NonNull String previousProfileName ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
List < GroupDatabase . GroupRecord > groupRecords = DatabaseFactory . getGroupDatabase ( context ) . getGroupsContainingMember ( recipient . getId ( ) , false ) ;
List < Long > threadIdsToUpdate = new LinkedList < > ( ) ;
byte [ ] profileChangeDetails = ProfileChangeDetails . newBuilder ( )
. setProfileNameChange ( ProfileChangeDetails . StringChange . newBuilder ( )
. setNew ( newProfileName )
. setPrevious ( previousProfileName ) )
. build ( )
. toByteArray ( ) ;
String body = Base64 . encodeBytes ( profileChangeDetails ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-07-15 20:15:15 +00:00
db . beginTransaction ( ) ;
try {
threadIdsToUpdate . add ( threadDatabase . getThreadIdFor ( recipient . getId ( ) ) ) ;
for ( GroupDatabase . GroupRecord groupRecord : groupRecords ) {
2020-07-17 19:18:54 +00:00
if ( groupRecord . isActive ( ) ) {
threadIdsToUpdate . add ( threadDatabase . getThreadIdFor ( groupRecord . getRecipientId ( ) ) ) ;
}
2020-07-15 20:15:15 +00:00
}
Stream . of ( threadIdsToUpdate )
. withoutNulls ( )
. forEach ( threadId - > {
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , recipient . getId ( ) . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
values . put ( DATE_SENT , System . currentTimeMillis ( ) ) ;
values . put ( READ , 1 ) ;
values . put ( TYPE , Types . PROFILE_CHANGE_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
values . put ( BODY , body ) ;
db . insert ( TABLE_NAME , null , values ) ;
2020-07-16 13:27:25 +00:00
notifyConversationListeners ( threadId ) ;
} ) ;
2020-07-15 20:15:15 +00:00
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2020-12-03 22:33:04 +00:00
Stream . of ( threadIdsToUpdate )
. withoutNulls ( )
2021-08-13 16:50:45 +00:00
. forEach ( TrimThreadJob : : enqueueAsync ) ;
2020-07-15 20:15:15 +00:00
}
2020-10-15 19:49:09 +00:00
@Override
2020-11-24 15:54:41 +00:00
public void insertGroupV1MigrationEvents ( @NonNull RecipientId recipientId ,
long threadId ,
@NonNull GroupMigrationMembershipChange membershipChange )
{
2020-11-04 19:19:14 +00:00
insertGroupV1MigrationNotification ( recipientId , threadId ) ;
2020-11-24 15:54:41 +00:00
if ( ! membershipChange . isEmpty ( ) ) {
insertGroupV1MigrationMembershipChanges ( recipientId , threadId , membershipChange ) ;
2020-11-04 19:19:14 +00:00
}
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2020-11-04 19:19:14 +00:00
}
private void insertGroupV1MigrationNotification ( @NonNull RecipientId recipientId , long threadId ) {
2020-11-24 15:54:41 +00:00
insertGroupV1MigrationMembershipChanges ( recipientId , threadId , GroupMigrationMembershipChange . empty ( ) ) ;
2020-11-04 19:19:14 +00:00
}
2020-11-24 15:54:41 +00:00
private void insertGroupV1MigrationMembershipChanges ( @NonNull RecipientId recipientId ,
long threadId ,
@NonNull GroupMigrationMembershipChange membershipChange )
{
2020-10-15 19:49:09 +00:00
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
values . put ( DATE_SENT , System . currentTimeMillis ( ) ) ;
values . put ( READ , 1 ) ;
values . put ( TYPE , Types . GV1_MIGRATION_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
2020-11-24 15:54:41 +00:00
if ( ! membershipChange . isEmpty ( ) ) {
values . put ( BODY , membershipChange . serialize ( ) ) ;
2020-11-04 19:19:14 +00:00
}
2020-10-15 19:49:09 +00:00
2021-08-18 14:16:54 +00:00
databaseHelper . getSignalWritableDatabase ( ) . insert ( TABLE_NAME , null , values ) ;
2020-10-15 19:49:09 +00:00
}
2021-09-08 17:38:39 +00:00
@Override
public void insertNumberChangeMessages ( @NonNull Recipient recipient ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
List < GroupDatabase . GroupRecord > groupRecords = DatabaseFactory . getGroupDatabase ( context ) . getGroupsContainingMember ( recipient . getId ( ) , false ) ;
List < Long > threadIdsToUpdate = new LinkedList < > ( ) ;
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
threadIdsToUpdate . add ( threadDatabase . getThreadIdFor ( recipient . getId ( ) ) ) ;
for ( GroupDatabase . GroupRecord groupRecord : groupRecords ) {
if ( groupRecord . isActive ( ) ) {
threadIdsToUpdate . add ( threadDatabase . getThreadIdFor ( groupRecord . getRecipientId ( ) ) ) ;
}
}
threadIdsToUpdate . stream ( )
. filter ( Objects : : nonNull )
. forEach ( threadId - > {
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , recipient . getId ( ) . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , 1 ) ;
values . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
values . put ( DATE_SENT , System . currentTimeMillis ( ) ) ;
values . put ( READ , 1 ) ;
values . put ( TYPE , Types . CHANGE_NUMBER_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
values . putNull ( BODY ) ;
db . insert ( TABLE_NAME , null , values ) ;
} ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
threadIdsToUpdate . stream ( )
. filter ( Objects : : nonNull )
. forEach ( threadId - > {
TrimThreadJob . enqueueAsync ( threadId ) ;
DatabaseFactory . getThreadDatabase ( context ) . update ( threadId , true ) ;
notifyConversationListeners ( threadId ) ;
} ) ;
}
2020-08-20 20:50:14 +00:00
@Override
public Optional < InsertResult > insertMessageInbox ( IncomingTextMessage message , long type ) {
2015-10-27 19:18:02 +00:00
if ( message . isJoined ( ) ) {
type = ( type & ( Types . TOTAL_MASK - Types . BASE_TYPE_MASK ) ) | Types . JOINED_TYPE ;
} else if ( message . isPreKeyBundle ( ) ) {
2015-03-11 21:23:45 +00:00
type | = Types . KEY_EXCHANGE_BIT | Types . KEY_EXCHANGE_BUNDLE_BIT ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
} else if ( message . isSecureMessage ( ) ) {
type | = Types . SECURE_MESSAGE_BIT ;
2014-02-20 05:06:54 +00:00
} else if ( message . isGroup ( ) ) {
2020-05-13 16:36:57 +00:00
IncomingGroupUpdateMessage incomingGroupUpdateMessage = ( IncomingGroupUpdateMessage ) message ;
2014-02-20 05:06:54 +00:00
type | = Types . SECURE_MESSAGE_BIT ;
2020-05-13 16:36:57 +00:00
if ( incomingGroupUpdateMessage . isGroupV2 ( ) ) type | = Types . GROUP_V2_BIT | Types . GROUP_UPDATE_BIT ;
else if ( incomingGroupUpdateMessage . isUpdate ( ) ) type | = Types . GROUP_UPDATE_BIT ;
else if ( incomingGroupUpdateMessage . isQuit ( ) ) type | = Types . GROUP_QUIT_BIT ;
2014-02-19 21:46:49 +00:00
} else if ( message . isEndSession ( ) ) {
type | = Types . SECURE_MESSAGE_BIT ;
2014-02-20 05:06:54 +00:00
type | = Types . END_SESSION_BIT ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
}
2012-09-09 23:10:46 +00:00
2016-11-09 17:37:40 +00:00
if ( message . isPush ( ) ) type | = Types . PUSH_MESSAGE_BIT ;
if ( message . isIdentityUpdate ( ) ) type | = Types . KEY_EXCHANGE_IDENTITY_UPDATE_BIT ;
if ( message . isContentPreKeyBundle ( ) ) type | = Types . KEY_EXCHANGE_CONTENT_FORMAT ;
2014-02-21 07:00:38 +00:00
2017-06-07 01:03:09 +00:00
if ( message . isIdentityVerified ( ) ) type | = Types . KEY_EXCHANGE_IDENTITY_VERIFIED_BIT ;
else if ( message . isIdentityDefault ( ) ) type | = Types . KEY_EXCHANGE_IDENTITY_DEFAULT_BIT ;
2019-08-07 18:22:51 +00:00
Recipient recipient = Recipient . resolved ( message . getSender ( ) ) ;
2014-02-03 03:38:06 +00:00
2017-08-01 15:56:00 +00:00
Recipient groupRecipient ;
2014-02-03 03:38:06 +00:00
2015-03-03 19:44:49 +00:00
if ( message . getGroupId ( ) = = null ) {
2017-08-01 15:56:00 +00:00
groupRecipient = null ;
2015-03-03 19:44:49 +00:00
} else {
2020-10-15 19:49:09 +00:00
RecipientId id = DatabaseFactory . getRecipientDatabase ( context ) . getOrInsertFromPossiblyMigratedGroupId ( message . getGroupId ( ) ) ;
2019-08-07 18:22:51 +00:00
groupRecipient = Recipient . resolved ( id ) ;
2014-02-03 03:38:06 +00:00
}
2021-01-22 18:50:48 +00:00
boolean silent = message . isIdentityUpdate ( ) | |
message . isIdentityVerified ( ) | |
message . isIdentityDefault ( ) | |
message . isJustAGroupLeave ( ) ;
boolean unread = ! silent & & ( Util . isDefaultSmsProvider ( context ) | |
message . isSecureMessage ( ) | |
message . isGroup ( ) | |
message . isPreKeyBundle ( ) ) ;
2012-09-09 23:10:46 +00:00
2014-01-14 08:26:43 +00:00
long threadId ;
2021-07-27 17:52:49 +00:00
if ( groupRecipient = = null ) threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( recipient ) ;
else threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( groupRecipient ) ;
2014-01-14 08:26:43 +00:00
2021-01-08 15:23:46 +00:00
ContentValues values = new ContentValues ( ) ;
2019-08-07 18:22:51 +00:00
values . put ( RECIPIENT_ID , message . getSender ( ) . serialize ( ) ) ;
2014-02-03 03:38:06 +00:00
values . put ( ADDRESS_DEVICE_ID , message . getSenderDeviceId ( ) ) ;
2021-06-29 19:13:31 +00:00
values . put ( DATE_RECEIVED , message . getReceivedTimestampMillis ( ) ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
values . put ( DATE_SENT , message . getSentTimestampMillis ( ) ) ;
2020-04-13 23:02:50 +00:00
values . put ( DATE_SERVER , message . getServerTimestampMillis ( ) ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
values . put ( PROTOCOL , message . getProtocol ( ) ) ;
2013-12-07 19:00:20 +00:00
values . put ( READ , unread ? 0 : 1 ) ;
2016-02-06 00:10:33 +00:00
values . put ( SUBSCRIPTION_ID , message . getSubscriptionId ( ) ) ;
2016-08-16 03:23:56 +00:00
values . put ( EXPIRES_IN , message . getExpiresIn ( ) ) ;
2018-05-22 09:13:10 +00:00
values . put ( UNIDENTIFIED , message . isUnidentified ( ) ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
2014-11-12 19:15:05 +00:00
if ( ! TextUtils . isEmpty ( message . getPseudoSubject ( ) ) )
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
values . put ( SUBJECT , message . getPseudoSubject ( ) ) ;
values . put ( REPLY_PATH_PRESENT , message . isReplyPathPresent ( ) ) ;
values . put ( SERVICE_CENTER , message . getServiceCenterAddress ( ) ) ;
values . put ( BODY , message . getMessageBody ( ) ) ;
values . put ( TYPE , type ) ;
values . put ( THREAD_ID , threadId ) ;
2021-05-17 13:43:37 +00:00
values . put ( SERVER_GUID , message . getServerGuid ( ) ) ;
2012-09-09 23:10:46 +00:00
2017-01-22 21:52:36 +00:00
if ( message . isPush ( ) & & isDuplicate ( message , threadId ) ) {
Log . w ( TAG , "Duplicate message (" + message . getSentTimestampMillis ( ) + "), ignoring..." ) ;
return Optional . absent ( ) ;
} else {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2017-01-22 21:52:36 +00:00
long messageId = db . insert ( TABLE_NAME , null , values ) ;
2011-12-20 18:20:44 +00:00
2017-01-22 21:52:36 +00:00
if ( unread ) {
2017-11-14 02:01:05 +00:00
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
2017-01-22 21:52:36 +00:00
}
2013-12-07 19:00:20 +00:00
2021-01-22 18:50:48 +00:00
if ( ! silent ) {
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2017-01-22 21:52:36 +00:00
}
2016-08-30 00:49:49 +00:00
2017-02-18 04:43:24 +00:00
if ( message . getSubscriptionId ( ) ! = - 1 ) {
2019-08-07 18:22:51 +00:00
DatabaseFactory . getRecipientDatabase ( context ) . setDefaultSubscriptionId ( recipient . getId ( ) , message . getSubscriptionId ( ) ) ;
2017-02-18 04:43:24 +00:00
}
2017-01-22 21:52:36 +00:00
notifyConversationListeners ( threadId ) ;
2017-06-19 18:20:15 +00:00
2021-01-22 18:50:48 +00:00
if ( ! silent ) {
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2017-06-19 18:20:15 +00:00
}
2013-01-10 05:06:56 +00:00
2017-01-22 21:52:36 +00:00
return Optional . of ( new InsertResult ( messageId , threadId ) ) ;
}
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2017-01-22 21:52:36 +00:00
public Optional < InsertResult > insertMessageInbox ( IncomingTextMessage message ) {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
return insertMessageInbox ( message , Types . BASE_INBOX_TYPE ) ;
}
2021-01-08 15:23:46 +00:00
@Override
2021-05-14 18:03:35 +00:00
public @NonNull InsertResult insertChatSessionRefreshedMessage ( @NonNull RecipientId recipientId , long senderDeviceId , long sentTimestamp ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2021-07-27 17:52:49 +00:00
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getOrCreateThreadIdFor ( Recipient . resolved ( recipientId ) ) ;
2021-01-08 15:23:46 +00:00
long type = Types . SECURE_MESSAGE_BIT | Types . PUSH_MESSAGE_BIT ;
type = type & ( Types . TOTAL_MASK - Types . ENCRYPTION_MASK ) | Types . ENCRYPTION_REMOTE_FAILED_BIT ;
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , senderDeviceId ) ;
values . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
values . put ( DATE_SENT , sentTimestamp ) ;
values . put ( DATE_SERVER , - 1 ) ;
values . put ( READ , 0 ) ;
values . put ( TYPE , type ) ;
values . put ( THREAD_ID , threadId ) ;
long messageId = db . insert ( TABLE_NAME , null , values ) ;
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2021-01-08 15:23:46 +00:00
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2021-01-08 15:23:46 +00:00
return new InsertResult ( messageId , threadId ) ;
}
2021-05-14 18:03:35 +00:00
@Override
public void insertBadDecryptMessage ( @NonNull RecipientId recipientId , int senderDevice , long sentTimestamp , long receivedTimestamp , long threadId ) {
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
values . put ( ADDRESS_DEVICE_ID , senderDevice ) ;
values . put ( DATE_SENT , sentTimestamp ) ;
values . put ( DATE_RECEIVED , receivedTimestamp ) ;
values . put ( DATE_SERVER , - 1 ) ;
values . put ( READ , 0 ) ;
values . put ( TYPE , Types . BAD_DECRYPT_TYPE ) ;
values . put ( THREAD_ID , threadId ) ;
2021-08-18 14:16:54 +00:00
databaseHelper . getSignalWritableDatabase ( ) . insert ( TABLE_NAME , null , values ) ;
2021-05-14 18:03:35 +00:00
DatabaseFactory . getThreadDatabase ( context ) . incrementUnread ( threadId , 1 ) ;
2021-08-24 15:11:48 +00:00
ThreadUpdateJob . enqueue ( threadId ) ;
2021-05-14 18:03:35 +00:00
notifyConversationListeners ( threadId ) ;
2021-08-13 16:50:45 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2021-05-14 18:03:35 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2018-01-25 03:17:44 +00:00
public long insertMessageOutbox ( long threadId , OutgoingTextMessage message ,
boolean forceSms , long date , InsertListener insertListener )
2014-06-11 22:31:59 +00:00
{
2021-08-23 14:05:48 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2018-01-25 03:17:44 +00:00
long type = Types . BASE_SENDING_TYPE ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
if ( message . isKeyExchange ( ) ) type | = Types . KEY_EXCHANGE_BIT ;
2016-12-21 17:49:01 +00:00
else if ( message . isSecureMessage ( ) ) type | = ( Types . SECURE_MESSAGE_BIT | Types . PUSH_MESSAGE_BIT ) ;
2014-02-19 21:46:49 +00:00
else if ( message . isEndSession ( ) ) type | = Types . END_SESSION_BIT ;
2014-06-11 22:31:59 +00:00
if ( forceSms ) type | = Types . MESSAGE_FORCE_SMS_BIT ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
2017-06-07 01:03:09 +00:00
if ( message . isIdentityVerified ( ) ) type | = Types . KEY_EXCHANGE_IDENTITY_VERIFIED_BIT ;
else if ( message . isIdentityDefault ( ) ) type | = Types . KEY_EXCHANGE_IDENTITY_DEFAULT_BIT ;
2021-08-26 19:33:13 +00:00
RecipientId recipientId = message . getRecipient ( ) . getId ( ) ;
Map < RecipientId , EarlyReceiptCache . Receipt > earlyDeliveryReceipts = earlyDeliveryReceiptCache . remove ( date ) ;
2015-12-09 04:32:54 +00:00
2014-11-08 19:35:58 +00:00
ContentValues contentValues = new ContentValues ( 6 ) ;
2019-08-07 18:22:51 +00:00
contentValues . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
2014-11-08 19:35:58 +00:00
contentValues . put ( THREAD_ID , threadId ) ;
contentValues . put ( BODY , message . getMessageBody ( ) ) ;
2015-10-16 17:07:50 +00:00
contentValues . put ( DATE_RECEIVED , System . currentTimeMillis ( ) ) ;
2014-11-08 19:35:58 +00:00
contentValues . put ( DATE_SENT , date ) ;
contentValues . put ( READ , 1 ) ;
contentValues . put ( TYPE , type ) ;
2016-02-06 00:10:33 +00:00
contentValues . put ( SUBSCRIPTION_ID , message . getSubscriptionId ( ) ) ;
2016-08-16 03:23:56 +00:00
contentValues . put ( EXPIRES_IN , message . getExpiresIn ( ) ) ;
2021-08-26 19:33:13 +00:00
contentValues . put ( DELIVERY_RECEIPT_COUNT , Stream . of ( earlyDeliveryReceipts . values ( ) ) . mapToLong ( EarlyReceiptCache . Receipt : : getCount ) . sum ( ) ) ;
contentValues . put ( RECEIPT_TIMESTAMP , Stream . of ( earlyDeliveryReceipts . values ( ) ) . mapToLong ( EarlyReceiptCache . Receipt : : getTimestamp ) . max ( ) . orElse ( - 1 ) ) ;
2015-12-09 04:32:54 +00:00
2021-08-26 15:59:45 +00:00
long messageId = db . insert ( TABLE_NAME , null , contentValues ) ;
2014-11-08 19:35:58 +00:00
2021-08-26 15:59:45 +00:00
if ( insertListener ! = null ) {
insertListener . onComplete ( ) ;
}
2017-06-19 18:20:15 +00:00
2021-08-26 15:59:45 +00:00
if ( ! message . isIdentityVerified ( ) & & ! message . isIdentityDefault ( ) ) {
DatabaseFactory . getThreadDatabase ( context ) . setLastScrolled ( threadId , 0 ) ;
DatabaseFactory . getThreadDatabase ( context ) . setLastSeenSilently ( threadId ) ;
2021-08-23 14:05:48 +00:00
}
2017-08-19 00:28:56 +00:00
2021-08-26 15:59:45 +00:00
DatabaseFactory . getThreadDatabase ( context ) . setHasSentSilently ( threadId , true ) ;
2021-08-30 19:08:38 +00:00
ApplicationDependencies . getDatabaseObserver ( ) . notifyMessageInsertObservers ( threadId , new MessageId ( messageId , false ) ) ;
2017-06-19 18:20:15 +00:00
if ( ! message . isIdentityVerified ( ) & & ! message . isIdentityDefault ( ) ) {
2021-07-29 18:07:39 +00:00
TrimThreadJob . enqueueAsync ( threadId ) ;
2017-06-19 18:20:15 +00:00
}
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
2014-11-08 19:35:58 +00:00
return messageId ;
2011-12-20 18:20:44 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2016-08-16 03:23:56 +00:00
public Cursor getExpirationStartedMessages ( ) {
String where = EXPIRE_STARTED + " > 0" ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2016-08-16 03:23:56 +00:00
return db . query ( TABLE_NAME , MESSAGE_PROJECTION , where , null , null , null , null ) ;
}
2020-07-30 20:02:17 +00:00
@Override
2020-08-20 20:50:14 +00:00
public SmsMessageRecord getSmsMessage ( long messageId ) throws NoSuchMessageException {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2018-01-25 03:17:44 +00:00
Cursor cursor = db . query ( TABLE_NAME , MESSAGE_PROJECTION , ID_WHERE , new String [ ] { messageId + "" } , null , null , null ) ;
Reader reader = new Reader ( cursor ) ;
SmsMessageRecord record = reader . getNext ( ) ;
reader . close ( ) ;
if ( record = = null ) throw new NoSuchMessageException ( "No message for ID: " + messageId ) ;
else return record ;
}
2020-08-20 20:50:14 +00:00
@Override
2020-06-09 15:52:58 +00:00
public Cursor getVerboseMessageCursor ( long messageId ) {
2020-08-20 20:50:14 +00:00
Cursor cursor = getMessageCursor ( messageId ) ;
2020-06-09 15:52:58 +00:00
setNotifyVerboseConversationListeners ( cursor , getThreadIdForMessage ( messageId ) ) ;
return cursor ;
}
2020-08-20 20:50:14 +00:00
@Override
public Cursor getMessageCursor ( long messageId ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2020-08-20 20:50:14 +00:00
return db . query ( TABLE_NAME , MESSAGE_PROJECTION , ID_WHERE , new String [ ] { messageId + "" } , null , null , null ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2015-03-31 20:36:04 +00:00
public boolean deleteMessage ( long messageId ) {
2020-11-09 13:17:23 +00:00
Log . d ( TAG , "deleteMessage(" + messageId + ")" ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-08-20 20:50:14 +00:00
2021-06-24 15:22:20 +00:00
long threadId ;
boolean threadDeleted ;
2020-08-20 20:50:14 +00:00
2021-06-24 15:22:20 +00:00
db . beginTransaction ( ) ;
try {
threadId = getThreadIdForMessage ( messageId ) ;
db . delete ( TABLE_NAME , ID_WHERE , new String [ ] { messageId + "" } ) ;
2021-08-03 14:28:08 +00:00
DatabaseFactory . getThreadDatabase ( context ) . setLastScrolled ( threadId , 0 ) ;
2021-06-24 15:22:20 +00:00
threadDeleted = DatabaseFactory . getThreadDatabase ( context ) . update ( threadId , false , true ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2020-08-20 20:50:14 +00:00
2011-12-20 18:20:44 +00:00
notifyConversationListeners ( threadId ) ;
2015-03-31 20:36:04 +00:00
return threadDeleted ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2018-02-26 22:02:12 +00:00
public void ensureMigration ( ) {
2021-08-18 14:16:54 +00:00
databaseHelper . getSignalWritableDatabase ( ) ;
2018-02-26 22:02:12 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
public MessageRecord getMessageRecord ( long messageId ) throws NoSuchMessageException {
return getSmsMessage ( messageId ) ;
}
2021-06-24 15:22:20 +00:00
@Override
public @Nullable MessageRecord getMessageRecordOrNull ( long messageId ) {
try {
return getSmsMessage ( messageId ) ;
} catch ( NoSuchMessageException e ) {
return null ;
}
}
2017-01-22 21:52:36 +00:00
private boolean isDuplicate ( IncomingTextMessage message , long threadId ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2021-06-21 13:51:51 +00:00
String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?" ;
String [ ] args = SqlUtil . buildArgs ( message . getSentTimestampMillis ( ) , message . getSender ( ) . serialize ( ) , threadId ) ;
2017-01-22 21:52:36 +00:00
2021-06-21 13:51:51 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { "1" } , query , args , null , null , null , "1" ) ) {
return cursor . moveToFirst ( ) ;
2017-01-22 21:52:36 +00:00
}
}
2020-08-20 20:50:14 +00:00
@Override
void deleteThread ( long threadId ) {
2020-11-09 13:17:23 +00:00
Log . d ( TAG , "deleteThread(" + threadId + ")" ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2021-06-24 15:22:20 +00:00
2011-12-20 18:20:44 +00:00
db . delete ( TABLE_NAME , THREAD_ID + " = ?" , new String [ ] { threadId + "" } ) ;
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2021-08-13 16:50:45 +00:00
int deleteMessagesInThreadBeforeDate ( long threadId , long date ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-09-03 21:52:44 +00:00
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date ;
2013-01-10 05:06:56 +00:00
2021-08-13 16:50:45 +00:00
return db . delete ( TABLE_NAME , where , SqlUtil . buildArgs ( threadId ) ) ;
2020-09-03 21:52:44 +00:00
}
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
@Override
void deleteAbandonedMessages ( ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2020-09-03 21:52:44 +00:00
String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadDatabase . TABLE_NAME + ")" ;
2013-01-10 05:06:56 +00:00
2021-08-13 16:50:45 +00:00
int deletes = db . delete ( TABLE_NAME , where , null ) ;
if ( deletes > 0 ) {
Log . i ( TAG , "Deleted " + deletes + " abandoned messages" ) ;
}
2013-01-10 05:06:56 +00:00
}
2011-12-20 18:20:44 +00:00
2020-10-15 16:55:08 +00:00
@Override
public List < MessageRecord > getMessagesInThreadAfterInclusive ( long threadId , long timestamp , long limit ) {
String where = TABLE_NAME + "." + MmsSmsColumns . THREAD_ID + " = ? AND " +
TABLE_NAME + "." + getDateReceivedColumnName ( ) + " >= ?" ;
String [ ] args = SqlUtil . buildArgs ( threadId , timestamp ) ;
try ( Reader reader = readerFor ( queryMessages ( where , args , false , limit ) ) ) {
List < MessageRecord > results = new ArrayList < > ( reader . cursor . getCount ( ) ) ;
while ( reader . getNext ( ) ! = null ) {
results . add ( reader . getCurrent ( ) ) ;
}
return results ;
}
}
private Cursor queryMessages ( @NonNull String where , @NonNull String [ ] args , boolean reverse , long limit ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalReadableDatabase ( ) ;
2020-10-15 16:55:08 +00:00
return db . query ( TABLE_NAME ,
MESSAGE_PROJECTION ,
where ,
args ,
null ,
null ,
reverse ? ID + " DESC" : null ,
limit > 0 ? String . valueOf ( limit ) : null ) ;
}
2020-08-20 20:50:14 +00:00
@Override
void deleteThreads ( @NonNull Set < Long > threadIds ) {
2020-11-09 13:17:23 +00:00
Log . d ( TAG , "deleteThreads(count: " + threadIds . size ( ) + ")" ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2011-12-20 18:20:44 +00:00
String where = "" ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
for ( long threadId : threadIds ) {
where + = THREAD_ID + " = '" + threadId + "' OR " ;
}
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
where = where . substring ( 0 , where . length ( ) - 4 ) ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
db . delete ( TABLE_NAME , where , null ) ;
}
2020-08-20 20:50:14 +00:00
@Override
void deleteAllThreads ( ) {
2020-11-09 13:17:23 +00:00
Log . d ( TAG , "deleteAllThreads()" ) ;
2021-08-18 14:16:54 +00:00
SQLiteDatabase db = databaseHelper . getSignalWritableDatabase ( ) ;
2012-09-09 23:10:46 +00:00
db . delete ( TABLE_NAME , null , null ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-20 20:50:14 +00:00
@Override
2020-08-24 20:40:47 +00:00
public SQLiteDatabase beginTransaction ( ) {
2021-08-18 14:16:54 +00:00
SQLiteDatabase database = databaseHelper . getSignalWritableDatabase ( ) ;
2011-12-20 18:20:44 +00:00
database . beginTransaction ( ) ;
return database ;
}
2020-08-20 20:50:14 +00:00
@Override
2020-08-24 20:40:47 +00:00
public void setTransactionSuccessful ( ) {
2021-08-18 14:16:54 +00:00
databaseHelper . getSignalWritableDatabase ( ) . setTransactionSuccessful ( ) ;
2020-08-24 20:40:47 +00:00
}
@Override
public void endTransaction ( SQLiteDatabase database ) {
2011-12-20 18:20:44 +00:00
database . setTransactionSuccessful ( ) ;
database . endTransaction ( ) ;
2012-09-09 23:10:46 +00:00
}
2020-08-20 20:50:14 +00:00
@Override
2020-08-24 20:40:47 +00:00
public void endTransaction ( ) {
2021-08-18 14:16:54 +00:00
databaseHelper . getSignalWritableDatabase ( ) . endTransaction ( ) ;
2020-08-24 20:40:47 +00:00
}
@Override
public SQLiteStatement createInsertStatement ( SQLiteDatabase database ) {
2019-08-07 18:22:51 +00:00
return database . compileStatement ( "INSERT INTO " + TABLE_NAME + " (" + RECIPIENT_ID + ", " +
PERSON + ", " +
DATE_SENT + ", " +
DATE_RECEIVED + ", " +
PROTOCOL + ", " +
READ + ", " +
STATUS + ", " +
TYPE + ", " +
REPLY_PATH_PRESENT + ", " +
SUBJECT + ", " +
BODY + ", " +
SERVICE_CENTER +
", " + THREAD_ID + ") " +
2013-01-06 21:13:14 +00:00
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-08-24 20:40:47 +00:00
@Override
public @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage ( ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public boolean isSent ( long messageId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public long getLatestGroupQuitTimestamp ( long threadId , long quitTimeBarrier ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public boolean isGroupQuitMessage ( long messageId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public @Nullable Pair < RecipientId , Long > getOldestUnreadMentionDetails ( long threadId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public int getUnreadMentionCount ( long threadId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public void addFailures ( long messageId , List < NetworkFailure > failure ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public void removeFailure ( long messageId , NetworkFailure failure ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public void markDownloadState ( long messageId , long state ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public Optional < MmsNotificationInfo > getNotification ( long messageId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public OutgoingMediaMessage getOutgoingMessage ( long messageId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public Optional < InsertResult > insertMessageInbox ( IncomingMediaMessage retrieved , String contentLocation , long threadId ) throws MmsException {
throw new UnsupportedOperationException ( ) ;
}
@Override
public Pair < Long , Long > insertMessageInbox ( @NonNull NotificationInd notification , int subscriptionId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public Optional < InsertResult > insertSecureDecryptedMessageInbox ( IncomingMediaMessage retrieved , long threadId ) throws MmsException {
throw new UnsupportedOperationException ( ) ;
}
@Override
public long insertMessageOutbox ( @NonNull OutgoingMediaMessage message , long threadId , boolean forceSms , @Nullable InsertListener insertListener ) throws MmsException {
throw new UnsupportedOperationException ( ) ;
}
@Override
public long insertMessageOutbox ( @NonNull OutgoingMediaMessage message , long threadId , boolean forceSms , int defaultReceiptStatus , @Nullable InsertListener insertListener ) throws MmsException {
throw new UnsupportedOperationException ( ) ;
}
@Override
public void markIncomingNotificationReceived ( long threadId ) {
throw new UnsupportedOperationException ( ) ;
}
@Override
public MessageDatabase . Reader getMessages ( Collection < Long > messageIds ) {
throw new UnsupportedOperationException ( ) ;
}
2013-01-07 05:38:36 +00:00
public static class Status {
public static final int STATUS_NONE = - 1 ;
2014-02-21 00:14:58 +00:00
public static final int STATUS_COMPLETE = 0 ;
public static final int STATUS_PENDING = 0x20 ;
public static final int STATUS_FAILED = 0x40 ;
2013-01-07 05:38:36 +00:00
}
2020-08-20 20:50:14 +00:00
public static Reader readerFor ( Cursor cursor ) {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
return new Reader ( cursor ) ;
}
2021-07-29 18:07:39 +00:00
public static OutgoingMessageReader readerFor ( OutgoingTextMessage message , long threadId , long messageId ) {
return new OutgoingMessageReader ( message , threadId , messageId ) ;
2017-04-22 23:29:26 +00:00
}
2020-04-13 23:02:50 +00:00
public static class OutgoingMessageReader {
2017-04-22 23:29:26 +00:00
private final OutgoingTextMessage message ;
private final long id ;
private final long threadId ;
2021-07-29 18:07:39 +00:00
public OutgoingMessageReader ( OutgoingTextMessage message , long threadId , long messageId ) {
2019-03-15 22:31:52 +00:00
this . message = message ;
this . threadId = threadId ;
2021-07-29 18:07:39 +00:00
this . id = messageId ;
2017-04-22 23:29:26 +00:00
}
public MessageRecord getCurrent ( ) {
2020-04-13 23:02:50 +00:00
return new SmsMessageRecord ( id ,
message . getMessageBody ( ) ,
message . getRecipient ( ) ,
message . getRecipient ( ) ,
1 ,
System . currentTimeMillis ( ) ,
System . currentTimeMillis ( ) ,
- 1 ,
0 ,
message . isSecureMessage ( ) ? MmsSmsColumns . Types . getOutgoingEncryptedMessageType ( ) : MmsSmsColumns . Types . getOutgoingSmsMessageType ( ) ,
threadId ,
0 ,
new LinkedList < > ( ) ,
message . getSubscriptionId ( ) ,
message . getExpiresIn ( ) ,
System . currentTimeMillis ( ) ,
0 ,
false ,
2020-04-15 18:56:58 +00:00
Collections . emptyList ( ) ,
2020-11-11 19:49:48 +00:00
false ,
2021-08-30 20:52:46 +00:00
0 ,
- 1 ) ;
2017-04-22 23:29:26 +00:00
}
}
2020-10-15 16:55:08 +00:00
public static class Reader implements Closeable {
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
2020-08-20 20:50:14 +00:00
private final Cursor cursor ;
private final Context context ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
public Reader ( Cursor cursor ) {
2020-08-20 20:50:14 +00:00
this . cursor = cursor ;
this . context = ApplicationDependencies . getApplication ( ) ;
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
}
public SmsMessageRecord getNext ( ) {
if ( cursor = = null | | ! cursor . moveToNext ( ) )
return null ;
return getCurrent ( ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2013-06-25 04:02:30 +00:00
public int getCount ( ) {
if ( cursor = = null ) return 0 ;
else return cursor . getCount ( ) ;
}
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
public SmsMessageRecord getCurrent ( ) {
2020-04-13 23:02:50 +00:00
long messageId = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . ID ) ) ;
long recipientId = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . RECIPIENT_ID ) ) ;
int addressDeviceId = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . ADDRESS_DEVICE_ID ) ) ;
long type = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . TYPE ) ) ;
2019-12-03 21:57:21 +00:00
long dateReceived = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . NORMALIZED_DATE_RECEIVED ) ) ;
2020-04-13 23:02:50 +00:00
long dateSent = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . NORMALIZED_DATE_SENT ) ) ;
long dateServer = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . DATE_SERVER ) ) ;
long threadId = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . THREAD_ID ) ) ;
int status = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . STATUS ) ) ;
2019-12-03 21:57:21 +00:00
int deliveryReceiptCount = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . DELIVERY_RECEIPT_COUNT ) ) ;
2020-04-13 23:02:50 +00:00
int readReceiptCount = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . READ_RECEIPT_COUNT ) ) ;
2019-12-03 21:57:21 +00:00
String mismatchDocument = cursor . getString ( cursor . getColumnIndexOrThrow ( SmsDatabase . MISMATCHED_IDENTITIES ) ) ;
2020-04-13 23:02:50 +00:00
int subscriptionId = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . SUBSCRIPTION_ID ) ) ;
long expiresIn = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . EXPIRES_IN ) ) ;
long expireStarted = cursor . getLong ( cursor . getColumnIndexOrThrow ( SmsDatabase . EXPIRE_STARTED ) ) ;
String body = cursor . getString ( cursor . getColumnIndexOrThrow ( SmsDatabase . BODY ) ) ;
2019-12-03 21:57:21 +00:00
boolean unidentified = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . UNIDENTIFIED ) ) = = 1 ;
2020-04-15 18:56:58 +00:00
boolean remoteDelete = cursor . getInt ( cursor . getColumnIndexOrThrow ( SmsDatabase . REMOTE_DELETED ) ) = = 1 ;
2019-12-03 21:57:21 +00:00
List < ReactionRecord > reactions = parseReactions ( cursor ) ;
2020-11-11 19:49:48 +00:00
long notifiedTimestamp = CursorUtil . requireLong ( cursor , NOTIFIED_TIMESTAMP ) ;
2021-08-30 20:52:46 +00:00
long receiptTimestamp = CursorUtil . requireLong ( cursor , RECEIPT_TIMESTAMP ) ;
2017-09-16 05:38:53 +00:00
if ( ! TextSecurePreferences . isReadReceiptsEnabled ( context ) ) {
readReceiptCount = 0 ;
}
2015-01-15 21:35:35 +00:00
List < IdentityKeyMismatch > mismatches = getMismatches ( mismatchDocument ) ;
2019-08-07 18:22:51 +00:00
Recipient recipient = Recipient . live ( RecipientId . from ( recipientId ) ) . get ( ) ;
2015-01-15 21:35:35 +00:00
2019-03-13 21:28:16 +00:00
return new SmsMessageRecord ( messageId , body , recipient ,
2017-08-01 15:56:00 +00:00
recipient ,
2015-01-15 21:35:35 +00:00
addressDeviceId ,
2020-04-13 23:02:50 +00:00
dateSent , dateReceived , dateServer , deliveryReceiptCount , type ,
2016-08-16 03:23:56 +00:00
threadId , status , mismatches , subscriptionId ,
2019-06-11 06:18:45 +00:00
expiresIn , expireStarted ,
2020-11-11 19:49:48 +00:00
readReceiptCount , unidentified , reactions , remoteDelete ,
2021-08-30 20:52:46 +00:00
notifiedTimestamp , receiptTimestamp ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2015-01-15 21:35:35 +00:00
private List < IdentityKeyMismatch > getMismatches ( String document ) {
try {
if ( ! TextUtils . isEmpty ( document ) ) {
return JsonUtils . fromJson ( document , IdentityKeyMismatchList . class ) . getList ( ) ;
}
} catch ( IOException e ) {
Log . w ( TAG , e ) ;
}
return new LinkedList < > ( ) ;
}
2020-10-15 16:55:08 +00:00
@Override
Major storage layer refactoring to set the stage for clean GCM.
1) We now try to hand out cursors at a minimum. There has always been
a fairly clean insertion layer that handles encrypting message bodies,
but the process of decrypting message bodies has always been less than
ideal. Here we introduce a "Reader" interface that will decrypt message
bodies when appropriate and return objects that encapsulate record state.
No more MessageDisplayHelper. The MmsSmsDatabase interface is also more
sane.
2) We finally rid ourselves of the technical debt associated with TextSecure's
initial usage of the default SMS DB. In that world, we weren't able to use
anything other than the default "Inbox, Outbox, Sent" types to describe a
message, and had to overload the message content itself with a set of
local "prefixes" to describe what it was (encrypted, asymetric encrypted,
remote encrypted, a key exchange, procssed key exchange), and so on.
This includes a major schema update that transforms the "type" field into
a bitmask that describes everything that used to be encoded in a prefix,
and prefixes have been completely eliminated from the system.
No more Prefix.java
3) Refactoring of the MultipartMessageHandler code. It's less of a mess, and
hopefully more clear as to what's going on.
The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
2013-04-20 19:22:04 +00:00
public void close ( ) {
cursor . close ( ) ;
}
2011-12-20 18:20:44 +00:00
}
2017-01-22 21:52:36 +00:00
2011-12-20 18:20:44 +00:00
}