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 ;
2015-04-02 17:02:19 +00:00
import android.database.MergeCursor ;
2015-10-16 20:59:40 +00:00
import android.net.Uri ;
2019-11-19 16:01:07 +00:00
2019-06-05 19:47:14 +00:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
2011-12-20 18:20:44 +00:00
2017-08-07 04:43:11 +00:00
import com.annimon.stream.Stream ;
2019-06-11 06:18:45 +00:00
import com.fasterxml.jackson.annotation.JsonProperty ;
2017-08-07 04:43:11 +00:00
2020-08-13 12:31:55 +00:00
import org.jsoup.helper.StringUtil ;
2020-12-04 23:31:58 +00:00
import org.signal.core.util.logging.Log ;
2020-10-13 01:06:42 +00:00
import org.signal.zkgroup.InvalidInputException ;
import org.signal.zkgroup.groups.GroupMasterKey ;
2020-08-20 20:50:14 +00:00
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo ;
2020-09-03 21:52:44 +00:00
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings ;
2018-01-25 03:17:44 +00:00
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper ;
2015-10-16 20:59:40 +00:00
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord ;
2013-04-26 18:23:43 +00:00
import org.thoughtcrime.securesms.database.model.MessageRecord ;
2018-06-27 16:43:02 +00:00
import org.thoughtcrime.securesms.database.model.MmsMessageRecord ;
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.ThreadRecord ;
2020-10-13 01:06:42 +00:00
import org.thoughtcrime.securesms.groups.BadGroupIdException ;
import org.thoughtcrime.securesms.groups.GroupId ;
2015-10-16 20:59:40 +00:00
import org.thoughtcrime.securesms.mms.Slide ;
import org.thoughtcrime.securesms.mms.SlideDeck ;
2020-09-03 14:02:33 +00:00
import org.thoughtcrime.securesms.mms.StickerSlide ;
2017-08-01 15:56:00 +00:00
import org.thoughtcrime.securesms.recipients.Recipient ;
2020-07-02 15:19:52 +00:00
import org.thoughtcrime.securesms.recipients.RecipientDetails ;
2019-08-07 18:22:51 +00:00
import org.thoughtcrime.securesms.recipients.RecipientId ;
2020-02-19 22:08:34 +00:00
import org.thoughtcrime.securesms.recipients.RecipientUtil ;
2020-03-18 20:31:45 +00:00
import org.thoughtcrime.securesms.storage.StorageSyncHelper ;
2020-11-17 13:58:28 +00:00
import org.thoughtcrime.securesms.util.ConversationUtil ;
2020-07-02 15:19:52 +00:00
import org.thoughtcrime.securesms.util.CursorUtil ;
2019-06-11 06:18:45 +00:00
import org.thoughtcrime.securesms.util.JsonUtils ;
2020-07-15 22:03:18 +00:00
import org.thoughtcrime.securesms.util.SqlUtil ;
2017-09-16 05:38:53 +00:00
import org.thoughtcrime.securesms.util.TextSecurePreferences ;
2015-04-02 17:02:19 +00:00
import org.thoughtcrime.securesms.util.Util ;
2019-12-13 06:23:32 +00:00
import org.whispersystems.libsignal.util.guava.Optional ;
2020-10-07 17:32:59 +00:00
import org.whispersystems.signalservice.api.storage.SignalAccountRecord ;
import org.whispersystems.signalservice.api.storage.SignalContactRecord ;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record ;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record ;
2012-09-09 23:10:46 +00:00
2018-04-07 01:15:24 +00:00
import java.io.Closeable ;
2019-06-11 06:18:45 +00:00
import java.io.IOException ;
2020-10-07 17:32:59 +00:00
import java.util.ArrayList ;
2021-01-20 13:24:16 +00:00
import java.util.Arrays ;
2020-05-28 13:27:00 +00:00
import java.util.Collection ;
2020-03-18 20:31:45 +00:00
import java.util.Collections ;
2019-12-18 05:44:21 +00:00
import java.util.HashMap ;
import java.util.HashSet ;
2021-01-27 15:17:54 +00:00
import java.util.LinkedHashSet ;
2015-04-02 17:02:19 +00:00
import java.util.LinkedList ;
2012-09-09 23:10:46 +00:00
import java.util.List ;
2019-12-18 05:44:21 +00:00
import java.util.Map ;
2020-09-03 14:02:33 +00:00
import java.util.Objects ;
2012-09-09 23:10:46 +00:00
import java.util.Set ;
2011-12-20 18:20:44 +00:00
public class ThreadDatabase extends Database {
2015-10-16 20:59:40 +00:00
private static final String TAG = ThreadDatabase . class . getSimpleName ( ) ;
2020-09-03 21:52:44 +00:00
public static final long NO_TRIM_BEFORE_DATE_SET = 0 ;
public static final int NO_TRIM_MESSAGE_COUNT_SET = Integer . MAX_VALUE ;
2018-06-21 23:48:46 +00:00
public static final String TABLE_NAME = "thread" ;
2017-09-16 05:38:53 +00:00
public static final String ID = "_id" ;
public static final String DATE = "date" ;
public static final String MESSAGE_COUNT = "message_count" ;
2019-08-07 18:22:51 +00:00
public static final String RECIPIENT_ID = "recipient_ids" ;
2017-09-16 05:38:53 +00:00
public static final String SNIPPET = "snippet" ;
private static final String SNIPPET_CHARSET = "snippet_cs" ;
public static final String READ = "read" ;
2017-11-14 02:01:05 +00:00
public static final String UNREAD_COUNT = "unread_count" ;
2017-09-16 05:38:53 +00:00
public static final String TYPE = "type" ;
private static final String ERROR = "error" ;
public static final String SNIPPET_TYPE = "snippet_type" ;
public static final String SNIPPET_URI = "snippet_uri" ;
2019-06-11 06:18:45 +00:00
public static final String SNIPPET_CONTENT_TYPE = "snippet_content_type" ;
public static final String SNIPPET_EXTRAS = "snippet_extras" ;
2017-09-16 05:38:53 +00:00
public static final String ARCHIVED = "archived" ;
public static final String STATUS = "status" ;
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count" ;
public static final String READ_RECEIPT_COUNT = "read_receipt_count" ;
public static final String EXPIRES_IN = "expires_in" ;
public static final String LAST_SEEN = "last_seen" ;
2019-11-12 14:18:57 +00:00
public static final String HAS_SENT = "has_sent" ;
2020-06-11 20:55:34 +00:00
private static final String LAST_SCROLLED = "last_scrolled" ;
2020-10-07 17:32:59 +00:00
static final String PINNED = "pinned" ;
2015-11-24 15:06:41 +00:00
2020-05-28 13:27:00 +00:00
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " +
MESSAGE_COUNT + " INTEGER DEFAULT 0, " +
RECIPIENT_ID + " INTEGER, " +
SNIPPET + " TEXT, " +
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " +
READ + " INTEGER DEFAULT " + ReadStatus . READ . serialize ( ) + ", " +
TYPE + " INTEGER DEFAULT 0, " +
ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " +
SNIPPET_URI + " TEXT DEFAULT NULL, " +
SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " +
SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT 0, " +
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
EXPIRES_IN + " INTEGER DEFAULT 0, " +
LAST_SEEN + " INTEGER DEFAULT 0, " +
HAS_SENT + " INTEGER DEFAULT 0, " +
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
2020-06-11 20:55:34 +00:00
UNREAD_COUNT + " INTEGER DEFAULT 0, " +
2020-08-13 12:31:55 +00:00
LAST_SCROLLED + " INTEGER DEFAULT 0, " +
PINNED + " INTEGER DEFAULT 0);" ;
2011-12-20 18:20:44 +00:00
2018-01-25 03:17:44 +00:00
public static final String [ ] CREATE_INDEXS = {
2019-08-07 18:22:51 +00:00
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");" ,
2017-05-20 01:01:40 +00:00
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");" ,
2020-08-13 12:31:55 +00:00
"CREATE INDEX IF NOT EXISTS thread_pinned_index ON " + TABLE_NAME + " (" + PINNED + ");" ,
2012-10-30 00:41:06 +00:00
} ;
2017-08-07 04:43:11 +00:00
private static final String [ ] THREAD_PROJECTION = {
2019-08-07 18:22:51 +00:00
ID , DATE , MESSAGE_COUNT , RECIPIENT_ID , SNIPPET , SNIPPET_CHARSET , READ , UNREAD_COUNT , TYPE , ERROR , SNIPPET_TYPE ,
2020-08-13 12:31:55 +00:00
SNIPPET_URI , SNIPPET_CONTENT_TYPE , SNIPPET_EXTRAS , ARCHIVED , STATUS , DELIVERY_RECEIPT_COUNT , EXPIRES_IN , LAST_SEEN , READ_RECEIPT_COUNT , LAST_SCROLLED , PINNED
2017-08-07 04:43:11 +00:00
} ;
private static final List < String > TYPED_THREAD_PROJECTION = Stream . of ( THREAD_PROJECTION )
. map ( columnName - > TABLE_NAME + "." + columnName )
. toList ( ) ;
2017-08-07 23:47:38 +00:00
private static final List < String > COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream . concat ( Stream . concat ( Stream . of ( TYPED_THREAD_PROJECTION ) ,
2020-10-06 14:24:14 +00:00
Stream . of ( RecipientDatabase . TYPED_RECIPIENT_PROJECTION_NO_ID ) ) ,
2017-08-07 23:47:38 +00:00
Stream . of ( GroupDatabase . TYPED_GROUP_PROJECTION ) )
. toList ( ) ;
2017-08-07 04:43:11 +00:00
2020-08-14 19:50:23 +00:00
private static final String ORDER_BY_DEFAULT = TABLE_NAME + "." + DATE + " DESC" ;
2018-01-25 03:17:44 +00:00
public ThreadDatabase ( Context context , SQLCipherOpenHelper databaseHelper ) {
2011-12-20 18:20:44 +00:00
super ( context , databaseHelper ) ;
}
2019-08-07 18:22:51 +00:00
private long createThreadForRecipient ( @NonNull RecipientId recipientId , boolean group , int distributionType ) {
2019-10-09 02:14:52 +00:00
if ( recipientId . isUnknown ( ) ) {
throw new AssertionError ( "Cannot create a thread for an unknown recipient!" ) ;
}
2011-12-20 18:20:44 +00:00
ContentValues contentValues = new ContentValues ( 4 ) ;
long date = System . currentTimeMillis ( ) ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
contentValues . put ( DATE , date - date % 1000 ) ;
2019-08-07 18:22:51 +00:00
contentValues . put ( RECIPIENT_ID , recipientId . serialize ( ) ) ;
2012-09-09 23:10:46 +00:00
2017-08-01 15:56:00 +00:00
if ( group )
2013-04-26 01:59:49 +00:00
contentValues . put ( TYPE , distributionType ) ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
contentValues . put ( MESSAGE_COUNT , 0 ) ;
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
return db . insert ( TABLE_NAME , null , contentValues ) ;
}
2015-11-24 15:06:41 +00:00
private void updateThread ( long threadId , long count , String body , @Nullable Uri attachment ,
2019-06-11 06:18:45 +00:00
@Nullable String contentType , @Nullable Extra extra ,
2017-09-16 05:38:53 +00:00
long date , int status , int deliveryReceiptCount , long type , boolean unarchive ,
2020-08-12 15:08:48 +00:00
long expiresIn , int readReceiptCount )
2014-02-14 23:59:57 +00:00
{
2019-06-11 06:18:45 +00:00
String extraSerialized = null ;
if ( extra ! = null ) {
try {
extraSerialized = JsonUtils . toJson ( extra ) ;
} catch ( IOException e ) {
throw new AssertionError ( e ) ;
}
}
2020-07-15 20:15:15 +00:00
ContentValues contentValues = new ContentValues ( ) ;
2020-08-12 15:08:48 +00:00
contentValues . put ( DATE , date - date % 1000 ) ;
contentValues . put ( SNIPPET , body ) ;
contentValues . put ( SNIPPET_URI , attachment = = null ? null : attachment . toString ( ) ) ;
contentValues . put ( SNIPPET_TYPE , type ) ;
contentValues . put ( SNIPPET_CONTENT_TYPE , contentType ) ;
contentValues . put ( SNIPPET_EXTRAS , extraSerialized ) ;
2011-12-20 18:20:44 +00:00
contentValues . put ( MESSAGE_COUNT , count ) ;
2015-11-24 15:06:41 +00:00
contentValues . put ( STATUS , status ) ;
2017-09-16 05:38:53 +00:00
contentValues . put ( DELIVERY_RECEIPT_COUNT , deliveryReceiptCount ) ;
contentValues . put ( READ_RECEIPT_COUNT , readReceiptCount ) ;
2016-08-16 03:23:56 +00:00
contentValues . put ( EXPIRES_IN , expiresIn ) ;
2012-09-09 23:10:46 +00:00
2015-11-23 23:07:41 +00:00
if ( unarchive ) {
contentValues . put ( ARCHIVED , 0 ) ;
}
2020-07-10 16:02:42 +00:00
if ( count ! = getConversationMessageCount ( threadId ) ) {
contentValues . put ( LAST_SCROLLED , 0 ) ;
}
2011-12-20 18:20:44 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
2013-04-26 18:23:43 +00:00
db . update ( TABLE_NAME , contentValues , ID + " = ?" , new String [ ] { threadId + "" } ) ;
2011-12-20 18:20:44 +00:00
notifyConversationListListeners ( ) ;
}
2012-09-09 23:10:46 +00:00
2015-11-23 23:07:41 +00:00
public void updateSnippet ( long threadId , String snippet , @Nullable Uri attachment , long date , long type , boolean unarchive ) {
2020-11-12 14:52:21 +00:00
if ( isSilentType ( type ) ) {
2020-07-15 20:15:15 +00:00
return ;
}
2015-04-06 20:44:18 +00:00
2020-07-15 20:15:15 +00:00
ContentValues contentValues = new ContentValues ( ) ;
2015-04-06 20:44:18 +00:00
contentValues . put ( DATE , date - date % 1000 ) ;
2014-12-12 01:13:01 +00:00
contentValues . put ( SNIPPET , snippet ) ;
contentValues . put ( SNIPPET_TYPE , type ) ;
2015-10-16 20:59:40 +00:00
contentValues . put ( SNIPPET_URI , attachment = = null ? null : attachment . toString ( ) ) ;
2015-11-23 23:07:41 +00:00
if ( unarchive ) {
contentValues . put ( ARCHIVED , 0 ) ;
}
2014-12-12 01:13:01 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . update ( TABLE_NAME , contentValues , ID + " = ?" , new String [ ] { threadId + "" } ) ;
notifyConversationListListeners ( ) ;
}
2020-09-03 21:52:44 +00:00
public void trimAllThreads ( int length , long trimBeforeDate ) {
if ( length = = NO_TRIM_MESSAGE_COUNT_SET & & trimBeforeDate = = NO_TRIM_BEFORE_DATE_SET ) {
return ;
}
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
AttachmentDatabase attachmentDatabase = DatabaseFactory . getAttachmentDatabase ( context ) ;
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory . getGroupReceiptDatabase ( context ) ;
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory . getMmsSmsDatabase ( context ) ;
2020-09-30 18:35:02 +00:00
MentionDatabase mentionDatabase = DatabaseFactory . getMentionDatabase ( context ) ;
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
try ( Cursor cursor = databaseHelper . getReadableDatabase ( ) . query ( TABLE_NAME , new String [ ] { ID } , null , null , null , null , null ) ) {
2013-01-10 05:06:56 +00:00
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
2020-09-03 21:52:44 +00:00
trimThreadInternal ( CursorUtil . requireLong ( cursor , ID ) , length , trimBeforeDate ) ;
2013-01-10 05:06:56 +00:00
}
2020-09-03 21:52:44 +00:00
}
db . beginTransaction ( ) ;
try {
mmsSmsDatabase . deleteAbandonedMessages ( ) ;
attachmentDatabase . trimAllAbandonedAttachments ( ) ;
groupReceiptDatabase . deleteAbandonedRows ( ) ;
2020-09-30 18:35:02 +00:00
mentionDatabase . deleteAbandonedMentions ( ) ;
2020-09-24 20:51:15 +00:00
attachmentDatabase . deleteAbandonedAttachmentFiles ( ) ;
2020-09-03 21:52:44 +00:00
db . setTransactionSuccessful ( ) ;
2013-01-10 05:06:56 +00:00
} finally {
2020-09-03 21:52:44 +00:00
db . endTransaction ( ) ;
2013-01-10 05:06:56 +00:00
}
2020-09-03 21:52:44 +00:00
notifyAttachmentListeners ( ) ;
notifyStickerListeners ( ) ;
notifyStickerPackListeners ( ) ;
2013-01-10 05:06:56 +00:00
}
2020-09-03 21:52:44 +00:00
public void trimThread ( long threadId , int length , long trimBeforeDate ) {
2020-09-08 18:04:14 +00:00
if ( length = = NO_TRIM_MESSAGE_COUNT_SET & & trimBeforeDate = = NO_TRIM_BEFORE_DATE_SET ) {
return ;
}
2020-09-03 21:52:44 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
AttachmentDatabase attachmentDatabase = DatabaseFactory . getAttachmentDatabase ( context ) ;
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory . getGroupReceiptDatabase ( context ) ;
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory . getMmsSmsDatabase ( context ) ;
2020-09-30 18:35:02 +00:00
MentionDatabase mentionDatabase = DatabaseFactory . getMentionDatabase ( context ) ;
2020-09-03 21:52:44 +00:00
db . beginTransaction ( ) ;
2013-01-10 05:06:56 +00:00
try {
2020-09-03 21:52:44 +00:00
trimThreadInternal ( threadId , length , trimBeforeDate ) ;
mmsSmsDatabase . deleteAbandonedMessages ( ) ;
attachmentDatabase . trimAllAbandonedAttachments ( ) ;
groupReceiptDatabase . deleteAbandonedRows ( ) ;
2020-09-30 18:35:02 +00:00
mentionDatabase . deleteAbandonedMentions ( ) ;
2020-09-24 20:51:15 +00:00
attachmentDatabase . deleteAbandonedAttachmentFiles ( ) ;
2020-09-03 21:52:44 +00:00
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
notifyAttachmentListeners ( ) ;
notifyStickerListeners ( ) ;
notifyStickerPackListeners ( ) ;
}
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
private void trimThreadInternal ( long threadId , int length , long trimBeforeDate ) {
if ( length = = NO_TRIM_MESSAGE_COUNT_SET & & trimBeforeDate = = NO_TRIM_BEFORE_DATE_SET ) {
return ;
}
2013-01-10 05:06:56 +00:00
2020-09-03 21:52:44 +00:00
if ( length ! = NO_TRIM_MESSAGE_COUNT_SET ) {
try ( Cursor cursor = DatabaseFactory . getMmsSmsDatabase ( context ) . getConversation ( threadId ) ) {
if ( cursor ! = null & & length > 0 & & cursor . getCount ( ) > length ) {
cursor . moveToPosition ( length - 1 ) ;
2020-09-10 16:09:28 +00:00
trimBeforeDate = Math . max ( trimBeforeDate , cursor . getLong ( cursor . getColumnIndexOrThrow ( MmsSmsColumns . NORMALIZED_DATE_RECEIVED ) ) ) ;
2020-09-03 21:52:44 +00:00
}
2013-01-10 05:06:56 +00:00
}
2020-09-03 21:52:44 +00:00
}
2020-09-10 16:09:28 +00:00
if ( trimBeforeDate ! = NO_TRIM_BEFORE_DATE_SET ) {
2020-09-03 21:52:44 +00:00
Log . i ( TAG , "Trimming thread: " + threadId + " before: " + trimBeforeDate ) ;
DatabaseFactory . getMmsSmsDatabase ( context ) . deleteMessagesInThreadBeforeDate ( threadId , trimBeforeDate ) ;
update ( threadId , false ) ;
notifyConversationListeners ( threadId ) ;
2013-01-10 05:06:56 +00:00
}
}
2017-10-09 01:09:46 +00:00
public List < MarkedMessageInfo > setAllThreadsRead ( ) {
2013-05-06 20:59:40 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
2020-05-28 13:27:00 +00:00
contentValues . put ( READ , ReadStatus . READ . serialize ( ) ) ;
2017-11-14 02:01:05 +00:00
contentValues . put ( UNREAD_COUNT , 0 ) ;
2013-05-06 20:59:40 +00:00
db . update ( TABLE_NAME , contentValues , null , null ) ;
2017-10-09 01:09:46 +00:00
final List < MarkedMessageInfo > smsRecords = DatabaseFactory . getSmsDatabase ( context ) . setAllMessagesRead ( ) ;
final List < MarkedMessageInfo > mmsRecords = DatabaseFactory . getMmsDatabase ( context ) . setAllMessagesRead ( ) ;
2019-12-03 21:57:21 +00:00
DatabaseFactory . getSmsDatabase ( context ) . setAllReactionsSeen ( ) ;
DatabaseFactory . getMmsDatabase ( context ) . setAllReactionsSeen ( ) ;
2013-05-06 20:59:40 +00:00
notifyConversationListListeners ( ) ;
2017-10-09 01:09:46 +00:00
2019-11-19 16:01:07 +00:00
return Util . concatenatedList ( smsRecords , mmsRecords ) ;
2013-05-06 20:59:40 +00:00
}
2019-10-16 16:26:26 +00:00
public boolean hasCalledSince ( @NonNull Recipient recipient , long timestamp ) {
2020-07-06 20:27:03 +00:00
return hasReceivedAnyCallsSince ( getThreadIdFor ( recipient ) , timestamp ) ;
}
public boolean hasReceivedAnyCallsSince ( long threadId , long timestamp ) {
return DatabaseFactory . getMmsSmsDatabase ( context ) . hasReceivedAnyCallsSince ( threadId , timestamp ) ;
2019-10-16 16:26:26 +00:00
}
2019-11-19 16:01:07 +00:00
public List < MarkedMessageInfo > setEntireThreadRead ( long threadId ) {
setRead ( threadId , false ) ;
final List < MarkedMessageInfo > smsRecords = DatabaseFactory . getSmsDatabase ( context ) . setEntireThreadRead ( threadId ) ;
final List < MarkedMessageInfo > mmsRecords = DatabaseFactory . getMmsDatabase ( context ) . setEntireThreadRead ( threadId ) ;
return Util . concatenatedList ( smsRecords , mmsRecords ) ;
}
2017-02-22 23:05:35 +00:00
public List < MarkedMessageInfo > setRead ( long threadId , boolean lastSeen ) {
2020-08-04 17:13:59 +00:00
return setReadInternal ( Collections . singletonList ( threadId ) , lastSeen , - 1 ) ;
}
public List < MarkedMessageInfo > setReadSince ( long threadId , boolean lastSeen , long sinceTimestamp ) {
return setReadInternal ( Collections . singletonList ( threadId ) , lastSeen , sinceTimestamp ) ;
2020-05-28 13:27:00 +00:00
}
public List < MarkedMessageInfo > setRead ( Collection < Long > threadIds , boolean lastSeen ) {
2020-08-04 17:13:59 +00:00
return setReadInternal ( threadIds , lastSeen , - 1 ) ;
}
private List < MarkedMessageInfo > setReadInternal ( Collection < Long > threadIds , boolean lastSeen , long sinceTimestamp ) {
2020-05-28 13:27:00 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
List < MarkedMessageInfo > smsRecords = new LinkedList < > ( ) ;
List < MarkedMessageInfo > mmsRecords = new LinkedList < > ( ) ;
2020-10-07 17:32:59 +00:00
boolean needsSync = false ;
2020-05-28 13:27:00 +00:00
db . beginTransaction ( ) ;
try {
ContentValues contentValues = new ContentValues ( 2 ) ;
contentValues . put ( READ , ReadStatus . READ . serialize ( ) ) ;
if ( lastSeen ) {
2020-08-04 17:13:59 +00:00
contentValues . put ( LAST_SEEN , sinceTimestamp = = - 1 ? System . currentTimeMillis ( ) : sinceTimestamp ) ;
2020-05-28 13:27:00 +00:00
}
for ( long threadId : threadIds ) {
2020-10-07 17:32:59 +00:00
ThreadRecord previous = getThreadRecord ( threadId ) ;
2020-08-04 17:13:59 +00:00
smsRecords . addAll ( DatabaseFactory . getSmsDatabase ( context ) . setMessagesReadSince ( threadId , sinceTimestamp ) ) ;
mmsRecords . addAll ( DatabaseFactory . getMmsDatabase ( context ) . setMessagesReadSince ( threadId , sinceTimestamp ) ) ;
DatabaseFactory . getSmsDatabase ( context ) . setReactionsSeen ( threadId , sinceTimestamp ) ;
DatabaseFactory . getMmsDatabase ( context ) . setReactionsSeen ( threadId , sinceTimestamp ) ;
2020-05-28 13:27:00 +00:00
2020-08-04 17:13:59 +00:00
int unreadCount = DatabaseFactory . getMmsSmsDatabase ( context ) . getUnreadCount ( threadId ) ;
2011-12-20 18:20:44 +00:00
2020-08-04 17:13:59 +00:00
contentValues . put ( UNREAD_COUNT , unreadCount ) ;
2020-10-07 17:32:59 +00:00
db . update ( TABLE_NAME , contentValues , ID_WHERE , SqlUtil . buildArgs ( threadId ) ) ;
if ( previous ! = null & & previous . isForcedUnread ( ) ) {
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( previous . getRecipient ( ) . getId ( ) ) ;
needsSync = true ;
}
2020-05-28 13:27:00 +00:00
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
2017-02-22 23:05:35 +00:00
}
2020-08-13 18:37:15 +00:00
notifyConversationListeners ( new HashSet < > ( threadIds ) ) ;
2020-05-28 13:27:00 +00:00
notifyConversationListListeners ( ) ;
2020-10-07 17:32:59 +00:00
if ( needsSync ) {
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
}
2020-05-28 13:27:00 +00:00
return Util . concatenatedList ( smsRecords , mmsRecords ) ;
}
public void setForcedUnread ( @NonNull Collection < Long > threadIds ) {
2011-12-20 18:20:44 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
2019-11-19 16:01:07 +00:00
2020-05-28 13:27:00 +00:00
db . beginTransaction ( ) ;
try {
2020-10-07 17:32:59 +00:00
List < RecipientId > recipientIds = getRecipientIdsForThreadIds ( threadIds ) ;
SqlUtil . Query query = SqlUtil . buildCollectionQuery ( ID , threadIds ) ;
ContentValues contentValues = new ContentValues ( ) ;
2020-05-28 13:27:00 +00:00
contentValues . put ( READ , ReadStatus . FORCED_UNREAD . serialize ( ) ) ;
2012-09-09 23:10:46 +00:00
2020-10-07 17:32:59 +00:00
db . update ( TABLE_NAME , contentValues , query . getWhere ( ) , query . getWhereArgs ( ) ) ;
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( recipientIds ) ;
2016-02-20 01:07:41 +00:00
2020-05-28 13:27:00 +00:00
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
2019-12-03 21:57:21 +00:00
2020-10-07 17:32:59 +00:00
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
notifyConversationListListeners ( ) ;
}
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2020-05-28 13:27:00 +00:00
2017-11-14 02:01:05 +00:00
public void incrementUnread ( long threadId , int amount ) {
2013-04-26 01:59:49 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
2020-05-28 13:27:00 +00:00
db . execSQL ( "UPDATE " + TABLE_NAME + " SET " + READ + " = " + ReadStatus . UNREAD . serialize ( ) + ", " +
2017-11-14 02:01:05 +00:00
UNREAD_COUNT + " = " + UNREAD_COUNT + " + ? WHERE " + ID + " = ?" ,
new String [ ] { String . valueOf ( amount ) ,
String . valueOf ( threadId ) } ) ;
2013-04-26 01:59:49 +00:00
}
public void setDistributionType ( long threadId , int distributionType ) {
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( TYPE , distributionType ) ;
2011-12-20 18:20:44 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
2015-10-16 20:59:40 +00:00
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { threadId + "" } ) ;
2012-09-09 23:10:46 +00:00
notifyConversationListListeners ( ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2017-08-01 15:56:00 +00:00
public int getDistributionType ( long threadId ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
Cursor cursor = db . query ( TABLE_NAME , new String [ ] { TYPE } , ID_WHERE , new String [ ] { String . valueOf ( threadId ) } , null , null , null ) ;
try {
if ( cursor ! = null & & cursor . moveToNext ( ) ) {
return cursor . getInt ( cursor . getColumnIndexOrThrow ( TYPE ) ) ;
}
return DistributionTypes . DEFAULT ;
} finally {
if ( cursor ! = null ) cursor . close ( ) ;
}
}
2019-08-07 18:22:51 +00:00
public Cursor getFilteredConversationList ( @Nullable List < RecipientId > filter ) {
2011-12-20 18:20:44 +00:00
if ( filter = = null | | filter . size ( ) = = 0 )
return null ;
2012-09-09 23:10:46 +00:00
2019-08-07 18:22:51 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
List < List < RecipientId > > splitRecipientIds = Util . partition ( filter , 900 ) ;
List < Cursor > cursors = new LinkedList < > ( ) ;
2012-09-09 23:10:46 +00:00
2019-08-07 18:22:51 +00:00
for ( List < RecipientId > recipientIds : splitRecipientIds ) {
String selection = TABLE_NAME + "." + RECIPIENT_ID + " = ?" ;
String [ ] selectionArgs = new String [ recipientIds . size ( ) ] ;
2012-09-09 23:10:46 +00:00
2019-08-07 18:22:51 +00:00
for ( int i = 0 ; i < recipientIds . size ( ) - 1 ; i + + )
selection + = ( " OR " + TABLE_NAME + "." + RECIPIENT_ID + " = ?" ) ;
2015-04-02 17:02:19 +00:00
int i = 0 ;
2019-08-07 18:22:51 +00:00
for ( RecipientId recipientId : recipientIds ) {
selectionArgs [ i + + ] = recipientId . serialize ( ) ;
2015-04-02 17:02:19 +00:00
}
2017-11-16 23:21:46 +00:00
String query = createQuery ( selection , 0 ) ;
2017-09-07 20:52:25 +00:00
cursors . add ( db . rawQuery ( query , selectionArgs ) ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2015-04-02 17:02:19 +00:00
Cursor cursor = cursors . size ( ) > 1 ? new MergeCursor ( cursors . toArray ( new Cursor [ cursors . size ( ) ] ) ) : cursors . get ( 0 ) ;
2020-06-09 15:52:58 +00:00
setNotifyConversationListListeners ( cursor ) ;
2011-12-20 18:20:44 +00:00
return cursor ;
}
2012-09-09 23:10:46 +00:00
2020-11-12 17:32:10 +00:00
public Cursor getRecentConversationList ( int limit , boolean includeInactiveGroups , boolean hideV1Groups ) {
2021-01-23 22:11:30 +00:00
return getRecentConversationList ( limit , includeInactiveGroups , false , hideV1Groups , false ) ;
2020-06-09 16:13:13 +00:00
}
2021-01-23 22:11:30 +00:00
public Cursor getRecentConversationList ( int limit , boolean includeInactiveGroups , boolean groupsOnly , boolean hideV1Groups , boolean hideSms ) {
2017-11-16 23:21:46 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
2019-10-22 17:49:31 +00:00
String query = ! includeInactiveGroups ? MESSAGE_COUNT + " != 0 AND (" + GroupDatabase . TABLE_NAME + "." + GroupDatabase . ACTIVE + " IS NULL OR " + GroupDatabase . TABLE_NAME + "." + GroupDatabase . ACTIVE + " = 1)"
2019-10-22 16:09:27 +00:00
: MESSAGE_COUNT + " != 0" ;
2020-06-09 16:13:13 +00:00
if ( groupsOnly ) {
query + = " AND " + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . GROUP_ID + " NOT NULL" ;
}
2020-11-12 17:32:10 +00:00
if ( hideV1Groups ) {
query + = " AND " + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . GROUP_TYPE + " != " + RecipientDatabase . GroupType . SIGNAL_V1 . getId ( ) ;
}
2021-01-23 22:11:30 +00:00
if ( hideSms ) {
query + = " AND (" + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . GROUP_ID + " NOT NULL OR " + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . REGISTERED + " = " + RecipientDatabase . RegisteredState . REGISTERED . getId ( ) + ")" ;
query + = " AND " + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . FORCE_SMS_SELECTION + " = 0" ;
}
2021-01-05 01:42:14 +00:00
query + = " AND " + ARCHIVED + " = 0" ;
2020-08-17 15:12:00 +00:00
return db . rawQuery ( createQuery ( query , 0 , limit , true ) , null ) ;
2017-11-16 23:21:46 +00:00
}
2019-10-22 16:09:27 +00:00
public Cursor getRecentPushConversationList ( int limit , boolean includeInactiveGroups ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String activeGroupQuery = ! includeInactiveGroups ? " AND " + GroupDatabase . TABLE_NAME + "." + GroupDatabase . ACTIVE + " = 1" : "" ;
String where = MESSAGE_COUNT + " != 0 AND " +
"(" +
RecipientDatabase . REGISTERED + " = " + RecipientDatabase . RegisteredState . REGISTERED . getId ( ) + " OR " +
"(" +
GroupDatabase . TABLE_NAME + "." + GroupDatabase . GROUP_ID + " NOT NULL AND " +
GroupDatabase . TABLE_NAME + "." + GroupDatabase . MMS + " = 0" +
activeGroupQuery +
")" +
")" ;
2020-08-17 15:12:00 +00:00
String query = createQuery ( where , 0 , limit , true ) ;
2019-07-03 19:07:00 +00:00
return db . rawQuery ( query , null ) ;
}
2020-10-15 19:49:09 +00:00
public @NonNull List < ThreadRecord > getRecentV1Groups ( int limit ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String where = MESSAGE_COUNT + " != 0 AND " +
"(" +
GroupDatabase . TABLE_NAME + "." + GroupDatabase . ACTIVE + " = 1 AND " +
GroupDatabase . TABLE_NAME + "." + GroupDatabase . V2_MASTER_KEY + " IS NULL AND " +
GroupDatabase . TABLE_NAME + "." + GroupDatabase . MMS + " = 0" +
")" ;
String query = createQuery ( where , 0 , limit , true ) ;
List < ThreadRecord > threadRecords = new ArrayList < > ( ) ;
try ( Reader reader = readerFor ( db . rawQuery ( query , null ) ) ) {
ThreadRecord record ;
while ( ( record = reader . getNext ( ) ) ! = null ) {
threadRecords . add ( record ) ;
}
}
return threadRecords ;
}
2015-11-23 23:07:41 +00:00
public Cursor getArchivedConversationList ( ) {
2017-08-07 04:43:11 +00:00
return getConversationList ( "1" ) ;
}
2020-03-18 20:31:45 +00:00
public boolean isArchived ( @NonNull RecipientId recipientId ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String query = RECIPIENT_ID + " = ?" ;
String [ ] args = new String [ ] { recipientId . serialize ( ) } ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { ARCHIVED } , query , args , null , null , null ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return cursor . getInt ( cursor . getColumnIndexOrThrow ( ARCHIVED ) ) = = 1 ;
}
}
return false ;
}
public void setArchived ( @NonNull RecipientId recipientId , boolean status ) {
setArchived ( Collections . singletonMap ( recipientId , status ) ) ;
}
public void setArchived ( @NonNull Map < RecipientId , Boolean > status ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
String query = RECIPIENT_ID + " = ?" ;
for ( Map . Entry < RecipientId , Boolean > entry : status . entrySet ( ) ) {
2020-08-24 17:48:23 +00:00
ContentValues values = new ContentValues ( 2 ) ;
2020-08-14 19:50:23 +00:00
if ( entry . getValue ( ) ) {
values . put ( PINNED , "0" ) ;
}
2020-03-18 20:31:45 +00:00
values . put ( ARCHIVED , entry . getValue ( ) ? "1" : "0" ) ;
db . update ( TABLE_NAME , values , query , new String [ ] { entry . getKey ( ) . serialize ( ) } ) ;
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
notifyConversationListListeners ( ) ;
}
}
2020-08-24 17:48:23 +00:00
public void setArchived ( Set < Long > threadIds , boolean archive ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
for ( long threadId : threadIds ) {
ContentValues values = new ContentValues ( 2 ) ;
if ( archive ) {
values . put ( PINNED , "0" ) ;
}
values . put ( ARCHIVED , archive ? "1" : "0" ) ;
db . update ( TABLE_NAME , values , ID_WHERE , SqlUtil . buildArgs ( threadId ) ) ;
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
notifyConversationListListeners ( ) ;
}
}
2019-12-18 05:44:21 +00:00
public @NonNull Set < RecipientId > getArchivedRecipients ( ) {
Set < RecipientId > archived = new HashSet < > ( ) ;
2020-03-18 20:31:45 +00:00
try ( Cursor cursor = getArchivedConversationList ( ) ) {
2019-12-18 05:44:21 +00:00
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
archived . add ( RecipientId . from ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . RECIPIENT_ID ) ) ) ) ;
}
}
return archived ;
}
public @NonNull Map < RecipientId , Integer > getInboxPositions ( ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String query = createQuery ( MESSAGE_COUNT + " != ?" , 0 ) ;
Map < RecipientId , Integer > positions = new HashMap < > ( ) ;
try ( Cursor cursor = db . rawQuery ( query , new String [ ] { "0" } ) ) {
int i = 0 ;
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
RecipientId recipientId = RecipientId . from ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . RECIPIENT_ID ) ) ) ;
positions . put ( recipientId , i ) ;
i + + ;
}
}
return positions ;
}
2020-06-12 18:11:36 +00:00
public Cursor getArchivedConversationList ( long offset , long limit ) {
return getConversationList ( "1" , offset , limit ) ;
}
2017-08-07 04:43:11 +00:00
private Cursor getConversationList ( String archived ) {
2020-06-12 18:11:36 +00:00
return getConversationList ( archived , 0 , 0 ) ;
}
2020-08-14 19:50:23 +00:00
public Cursor getUnarchivedConversationList ( boolean pinned , long offset , long limit ) {
2020-08-26 14:13:01 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String pinnedWhere = PINNED + ( pinned ? " != 0" : " = 0" ) ;
String where = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + pinnedWhere ;
final String query ;
if ( pinned ) {
2020-10-15 19:49:09 +00:00
query = createQuery ( where , PINNED + " ASC" , offset , limit ) ;
2020-08-26 14:13:01 +00:00
} else {
query = createQuery ( where , offset , limit , false ) ;
}
Cursor cursor = db . rawQuery ( query , new String [ ] { } ) ;
2020-08-13 12:31:55 +00:00
setNotifyConversationListListeners ( cursor ) ;
return cursor ;
}
2020-06-12 18:11:36 +00:00
private Cursor getConversationList ( @NonNull String archived , long offset , long limit ) {
2017-09-15 17:36:33 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
2020-08-17 15:12:00 +00:00
String query = createQuery ( ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0" , offset , limit , false ) ;
2017-09-15 17:36:33 +00:00
Cursor cursor = db . rawQuery ( query , new String [ ] { archived } ) ;
2015-11-23 23:07:41 +00:00
2020-06-09 15:52:58 +00:00
setNotifyConversationListListeners ( cursor ) ;
2015-11-23 23:07:41 +00:00
2011-12-20 18:20:44 +00:00
return cursor ;
}
2012-09-09 23:10:46 +00:00
2020-08-13 12:31:55 +00:00
public int getArchivedConversationListCount ( ) {
2020-06-12 18:11:36 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String [ ] columns = new String [ ] { "COUNT(*)" } ;
String query = ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0" ;
2020-08-13 12:31:55 +00:00
String [ ] args = new String [ ] { "1" } ;
try ( Cursor cursor = db . query ( TABLE_NAME , columns , query , args , null , null , null ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return cursor . getInt ( 0 ) ;
}
}
return 0 ;
}
2020-08-14 19:50:23 +00:00
public int getPinnedConversationListCount ( ) {
2020-08-13 12:31:55 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String [ ] columns = new String [ ] { "COUNT(*)" } ;
2020-08-26 14:13:01 +00:00
String query = ARCHIVED + " = 0 AND " + PINNED + " != 0 AND " + MESSAGE_COUNT + " != 0" ;
2015-11-23 23:07:41 +00:00
2020-08-14 19:50:23 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , columns , query , null , null , null , null ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return cursor . getInt ( 0 ) ;
}
}
return 0 ;
}
public int getUnarchivedConversationListCount ( ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String [ ] columns = new String [ ] { "COUNT(*)" } ;
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0" ;
try ( Cursor cursor = db . query ( TABLE_NAME , columns , query , null , null , null , null ) ) {
2015-11-23 23:07:41 +00:00
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return cursor . getInt ( 0 ) ;
}
}
return 0 ;
}
2020-10-13 01:06:42 +00:00
/ * *
* @return Pinned recipients , in order from top to bottom .
* /
public @NonNull List < RecipientId > getPinnedRecipientIds ( ) {
2021-01-27 15:17:54 +00:00
String [ ] projection = new String [ ] { ID , RECIPIENT_ID } ;
2020-10-13 01:06:42 +00:00
List < RecipientId > pinned = new LinkedList < > ( ) ;
2021-01-27 15:17:54 +00:00
try ( Cursor cursor = getPinned ( projection ) ) {
2020-10-13 01:06:42 +00:00
while ( cursor . moveToNext ( ) ) {
pinned . add ( RecipientId . from ( CursorUtil . requireLong ( cursor , RECIPIENT_ID ) ) ) ;
}
}
return pinned ;
}
2021-01-27 15:17:54 +00:00
/ * *
* @return Pinned thread ids , in order from top to bottom .
* /
public @NonNull List < Long > getPinnedThreadIds ( ) {
String [ ] projection = new String [ ] { ID } ;
List < Long > pinned = new LinkedList < > ( ) ;
try ( Cursor cursor = getPinned ( projection ) ) {
while ( cursor . moveToNext ( ) ) {
pinned . add ( CursorUtil . requireLong ( cursor , ID ) ) ;
}
}
return pinned ;
}
/ * *
* @return Pinned recipients , in order from top to bottom .
* /
private @NonNull Cursor getPinned ( String [ ] projection ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String query = PINNED + " > ?" ;
String [ ] args = SqlUtil . buildArgs ( 0 ) ;
return db . query ( TABLE_NAME , projection , query , args , null , null , PINNED + " ASC" ) ;
}
public void restorePins ( @NonNull Collection < Long > threadIds ) {
Log . d ( TAG , "Restoring pinned threads " + StringUtil . join ( threadIds , "," ) ) ;
pinConversations ( threadIds , true ) ;
}
public void pinConversations ( @NonNull Collection < Long > threadIds ) {
Log . d ( TAG , "Pinning threads " + StringUtil . join ( threadIds , "," ) ) ;
pinConversations ( threadIds , false ) ;
}
private void pinConversations ( @NonNull Collection < Long > threadIds , boolean clearFirst ) {
2020-08-26 14:13:01 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
2021-01-27 15:17:54 +00:00
threadIds = new LinkedHashSet < > ( threadIds ) ;
2020-08-13 12:31:55 +00:00
2020-08-26 14:13:01 +00:00
try {
db . beginTransaction ( ) ;
2020-08-13 12:31:55 +00:00
2021-01-27 15:17:54 +00:00
if ( clearFirst ) {
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( PINNED , 0 ) ;
String query = PINNED + " > ?" ;
String [ ] args = SqlUtil . buildArgs ( 0 ) ;
db . update ( TABLE_NAME , contentValues , query , args ) ;
}
2020-08-26 14:13:01 +00:00
int pinnedCount = getPinnedConversationListCount ( ) ;
2021-01-27 15:17:54 +00:00
if ( pinnedCount > 0 & & clearFirst ) {
throw new AssertionError ( ) ;
}
2020-08-26 14:13:01 +00:00
for ( long threadId : threadIds ) {
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( PINNED , + + pinnedCount ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , SqlUtil . buildArgs ( threadId ) ) ;
2020-10-13 01:06:42 +00:00
2020-08-26 14:13:01 +00:00
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
notifyConversationListListeners ( ) ;
}
2020-08-13 12:31:55 +00:00
notifyConversationListListeners ( ) ;
2020-10-13 01:06:42 +00:00
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( Recipient . self ( ) . getId ( ) ) ;
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
2020-08-13 12:31:55 +00:00
}
public void unpinConversations ( @NonNull Set < Long > threadIds ) {
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
String placeholders = StringUtil . join ( Stream . of ( threadIds ) . map ( unused - > "?" ) . toList ( ) , "," ) ;
String selection = ID + " IN (" + placeholders + ")" ;
contentValues . put ( PINNED , 0 ) ;
db . update ( TABLE_NAME , contentValues , selection , SqlUtil . buildArgs ( Stream . of ( threadIds ) . toArray ( ) ) ) ;
notifyConversationListListeners ( ) ;
2020-10-13 01:06:42 +00:00
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( Recipient . self ( ) . getId ( ) ) ;
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
2020-08-13 12:31:55 +00:00
}
2015-11-23 23:07:41 +00:00
public void archiveConversation ( long threadId ) {
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
2020-08-14 19:50:23 +00:00
contentValues . put ( PINNED , 0 ) ;
2015-11-23 23:07:41 +00:00
contentValues . put ( ARCHIVED , 1 ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { threadId + "" } ) ;
notifyConversationListListeners ( ) ;
2020-03-18 20:31:45 +00:00
Recipient recipient = getRecipientForThreadId ( threadId ) ;
if ( recipient ! = null ) {
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( recipient . getId ( ) ) ;
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
}
2015-11-23 23:07:41 +00:00
}
public void unarchiveConversation ( long threadId ) {
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( ARCHIVED , 0 ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { threadId + "" } ) ;
notifyConversationListListeners ( ) ;
2020-03-18 20:31:45 +00:00
Recipient recipient = getRecipientForThreadId ( threadId ) ;
if ( recipient ! = null ) {
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( recipient . getId ( ) ) ;
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
}
2015-11-23 23:07:41 +00:00
}
2017-02-14 06:35:47 +00:00
public void setLastSeen ( long threadId ) {
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( LAST_SEEN , System . currentTimeMillis ( ) ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { String . valueOf ( threadId ) } ) ;
notifyConversationListListeners ( ) ;
}
2020-06-11 20:55:34 +00:00
public void setLastScrolled ( long threadId , long lastScrolledTimestamp ) {
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
ContentValues contentValues = new ContentValues ( 1 ) ;
2017-02-14 06:35:47 +00:00
2020-06-11 20:55:34 +00:00
contentValues . put ( LAST_SCROLLED , lastScrolledTimestamp ) ;
db . update ( TABLE_NAME , contentValues , ID_WHERE , new String [ ] { String . valueOf ( threadId ) } ) ;
}
public ConversationMetadata getConversationMetadata ( long threadId ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { LAST_SEEN , HAS_SENT , LAST_SCROLLED } , ID_WHERE , new String [ ] { String . valueOf ( threadId ) } , null , null , null ) ) {
2017-02-14 06:35:47 +00:00
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
2020-06-11 20:55:34 +00:00
return new ConversationMetadata ( cursor . getLong ( cursor . getColumnIndexOrThrow ( LAST_SEEN ) ) ,
cursor . getLong ( cursor . getColumnIndexOrThrow ( HAS_SENT ) ) = = 1 ,
cursor . getLong ( cursor . getColumnIndexOrThrow ( LAST_SCROLLED ) ) ) ;
2017-02-14 06:35:47 +00:00
}
2020-06-11 20:55:34 +00:00
return new ConversationMetadata ( - 1L , false , - 1 ) ;
2017-02-14 06:35:47 +00:00
}
}
2020-07-10 16:02:42 +00:00
public int getConversationMessageCount ( long threadId ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { MESSAGE_COUNT } , ID_WHERE , new String [ ] { String . valueOf ( threadId ) } , null , null , null ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return CursorUtil . requireInt ( cursor , MESSAGE_COUNT ) ;
}
}
return 0 ;
}
2011-12-20 18:20:44 +00:00
public void deleteConversation ( long threadId ) {
2021-02-24 03:59:53 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
DatabaseFactory . getSmsDatabase ( context ) . deleteThread ( threadId ) ;
DatabaseFactory . getMmsDatabase ( context ) . deleteThread ( threadId ) ;
DatabaseFactory . getDraftDatabase ( context ) . clearDrafts ( threadId ) ;
db . delete ( TABLE_NAME , ID_WHERE , new String [ ] { threadId + "" } ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2011-12-20 18:20:44 +00:00
notifyConversationListListeners ( ) ;
2021-02-24 03:59:53 +00:00
notifyConversationListeners ( threadId ) ;
ConversationUtil . clearShortcuts ( context , Collections . singleton ( threadId ) ) ;
2012-09-09 23:10:46 +00:00
}
2011-12-20 18:20:44 +00:00
public void deleteConversations ( Set < Long > selectedConversations ) {
2021-02-24 03:59:53 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
DatabaseFactory . getSmsDatabase ( context ) . deleteThreads ( selectedConversations ) ;
DatabaseFactory . getMmsDatabase ( context ) . deleteThreads ( selectedConversations ) ;
DatabaseFactory . getDraftDatabase ( context ) . clearDrafts ( selectedConversations ) ;
StringBuilder where = new StringBuilder ( ) ;
for ( long threadId : selectedConversations ) {
if ( where . length ( ) > 0 ) {
where . append ( " OR " ) ;
}
where . append ( ID + " = '" ) . append ( threadId ) . append ( "'" ) ;
}
db . delete ( TABLE_NAME , where . toString ( ) , null ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
2011-12-20 18:20:44 +00:00
notifyConversationListListeners ( ) ;
2021-02-24 03:59:53 +00:00
notifyConversationListeners ( selectedConversations ) ;
ConversationUtil . clearShortcuts ( context , selectedConversations ) ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
public void deleteAllConversations ( ) {
2021-02-24 03:59:53 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
DatabaseFactory . getSmsDatabase ( context ) . deleteAllThreads ( ) ;
DatabaseFactory . getMmsDatabase ( context ) . deleteAllThreads ( ) ;
DatabaseFactory . getDraftDatabase ( context ) . clearAllDrafts ( ) ;
db . delete ( TABLE_NAME , null , null ) ;
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
notifyConversationListListeners ( ) ;
ConversationUtil . clearAllShortcuts ( context ) ;
2011-12-20 18:20:44 +00:00
}
2020-09-22 20:24:13 +00:00
public long getThreadIdIfExistsFor ( @NonNull RecipientId recipientId ) {
2019-08-07 18:22:51 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
String where = RECIPIENT_ID + " = ?" ;
2020-09-22 20:24:13 +00:00
String [ ] recipientsArg = new String [ ] { recipientId . serialize ( ) } ;
2012-09-09 23:10:46 +00:00
2020-09-22 20:24:13 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { ID } , where , recipientsArg , null , null , null , "1" ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return CursorUtil . requireLong ( cursor , ID ) ;
} else {
return - 1 ;
}
2011-12-20 18:20:44 +00:00
}
}
2012-09-09 23:10:46 +00:00
2021-01-20 13:24:16 +00:00
public Map < RecipientId , Long > getThreadIdsIfExistsFor ( @NonNull RecipientId . . . recipientIds ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
SqlUtil . Query query = SqlUtil . buildCollectionQuery ( RECIPIENT_ID , Arrays . asList ( recipientIds ) ) ;
Map < RecipientId , Long > results = new HashMap < > ( ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { ID , RECIPIENT_ID } , query . getWhere ( ) , query . getWhereArgs ( ) , null , null , null , "1" ) ) {
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
results . put ( RecipientId . from ( CursorUtil . requireString ( cursor , RECIPIENT_ID ) ) , CursorUtil . requireLong ( cursor , ID ) ) ;
}
}
return results ;
}
2020-07-15 22:03:18 +00:00
public long getOrCreateValidThreadId ( @NonNull Recipient recipient , long candidateId ) {
return getOrCreateValidThreadId ( recipient , candidateId , DistributionTypes . DEFAULT ) ;
}
public long getOrCreateValidThreadId ( @NonNull Recipient recipient , long candidateId , int distributionType ) {
if ( candidateId ! = - 1 ) {
Optional < Long > remapped = RemappedRecords . getInstance ( ) . getThread ( context , candidateId ) ;
return remapped . isPresent ( ) ? remapped . get ( ) : candidateId ;
} else {
return getThreadIdFor ( recipient , distributionType ) ;
}
}
2019-12-03 16:05:03 +00:00
public long getThreadIdFor ( @NonNull Recipient recipient ) {
2017-08-01 15:56:00 +00:00
return getThreadIdFor ( recipient , DistributionTypes . DEFAULT ) ;
2013-04-26 01:59:49 +00:00
}
2019-12-03 16:05:03 +00:00
public long getThreadIdFor ( @NonNull Recipient recipient , int distributionType ) {
Long threadId = getThreadIdFor ( recipient . getId ( ) ) ;
if ( threadId ! = null ) {
return threadId ;
} else {
return createThreadForRecipient ( recipient . getId ( ) , recipient . isGroup ( ) , distributionType ) ;
}
}
2020-07-15 22:03:18 +00:00
public @Nullable Long getThreadIdFor ( @NonNull RecipientId recipientId ) {
2017-07-26 16:59:15 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
2019-08-07 18:22:51 +00:00
String where = RECIPIENT_ID + " = ?" ;
2019-12-03 16:05:03 +00:00
String [ ] recipientsArg = new String [ ] { recipientId . serialize ( ) } ;
2012-09-09 23:10:46 +00:00
2019-12-03 16:05:03 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { ID } , where , recipientsArg , null , null , null ) ) {
2017-07-26 16:59:15 +00:00
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
2012-10-01 02:56:29 +00:00
return cursor . getLong ( cursor . getColumnIndexOrThrow ( ID ) ) ;
2017-07-26 16:59:15 +00:00
} else {
2019-12-03 16:05:03 +00:00
return null ;
2017-07-26 16:59:15 +00:00
}
2011-12-20 18:20:44 +00:00
}
}
2012-09-09 23:10:46 +00:00
2019-12-03 16:05:03 +00:00
public @Nullable RecipientId getRecipientIdForThreadId ( long threadId ) {
2013-02-09 23:17:55 +00:00
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
2019-12-03 16:05:03 +00:00
try ( Cursor cursor = db . query ( TABLE_NAME , null , ID + " = ?" , new String [ ] { threadId + "" } , null , null , null ) ) {
2013-02-09 23:17:55 +00:00
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
2019-12-03 16:05:03 +00:00
return RecipientId . from ( cursor . getLong ( cursor . getColumnIndexOrThrow ( RECIPIENT_ID ) ) ) ;
2013-02-09 23:17:55 +00:00
}
}
return null ;
}
2019-12-03 16:05:03 +00:00
public @Nullable Recipient getRecipientForThreadId ( long threadId ) {
RecipientId id = getRecipientIdForThreadId ( threadId ) ;
if ( id = = null ) return null ;
return Recipient . resolved ( id ) ;
}
2020-10-07 17:32:59 +00:00
public @NonNull List < RecipientId > getRecipientIdsForThreadIds ( Collection < Long > threadIds ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
SqlUtil . Query query = SqlUtil . buildCollectionQuery ( ID , threadIds ) ;
List < RecipientId > ids = new ArrayList < > ( threadIds . size ( ) ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { RECIPIENT_ID } , query . getWhere ( ) , query . getWhereArgs ( ) , null , null , null ) ) {
while ( cursor ! = null & & cursor . moveToNext ( ) ) {
ids . add ( RecipientId . from ( CursorUtil . requireLong ( cursor , RECIPIENT_ID ) ) ) ;
}
}
return ids ;
}
2020-09-22 20:24:13 +00:00
public boolean hasThread ( @NonNull RecipientId recipientId ) {
return getThreadIdIfExistsFor ( recipientId ) > - 1 ;
}
2017-08-19 00:28:56 +00:00
public void setHasSent ( long threadId , boolean hasSent ) {
ContentValues contentValues = new ContentValues ( 1 ) ;
contentValues . put ( HAS_SENT , hasSent ? 1 : 0 ) ;
databaseHelper . getWritableDatabase ( ) . update ( TABLE_NAME , contentValues , ID_WHERE ,
new String [ ] { String . valueOf ( threadId ) } ) ;
notifyConversationListeners ( threadId ) ;
}
2017-11-14 02:01:05 +00:00
void updateReadState ( long threadId ) {
2020-10-07 17:32:59 +00:00
ThreadRecord previous = getThreadRecord ( threadId ) ;
int unreadCount = DatabaseFactory . getMmsSmsDatabase ( context ) . getUnreadCount ( threadId ) ;
2016-02-20 01:07:41 +00:00
ContentValues contentValues = new ContentValues ( ) ;
2020-10-07 17:32:59 +00:00
contentValues . put ( READ , unreadCount = = 0 ? ReadStatus . READ . serialize ( ) : ReadStatus . UNREAD . serialize ( ) ) ;
2017-11-14 02:01:05 +00:00
contentValues . put ( UNREAD_COUNT , unreadCount ) ;
2016-02-20 01:07:41 +00:00
2020-10-07 17:32:59 +00:00
databaseHelper . getWritableDatabase ( ) . update ( TABLE_NAME , contentValues , ID_WHERE , SqlUtil . buildArgs ( threadId ) ) ;
2016-02-20 01:07:41 +00:00
notifyConversationListListeners ( ) ;
2020-10-07 17:32:59 +00:00
if ( previous ! = null & & previous . isForcedUnread ( ) ) {
DatabaseFactory . getRecipientDatabase ( context ) . markNeedsSync ( previous . getRecipient ( ) . getId ( ) ) ;
StorageSyncHelper . scheduleSyncForDataChange ( ) ;
}
}
public void applyStorageSyncUpdate ( @NonNull RecipientId recipientId , @NonNull SignalContactRecord record ) {
applyStorageSyncUpdate ( recipientId , record . isArchived ( ) , record . isForcedUnread ( ) ) ;
}
public void applyStorageSyncUpdate ( @NonNull RecipientId recipientId , @NonNull SignalGroupV1Record record ) {
applyStorageSyncUpdate ( recipientId , record . isArchived ( ) , record . isForcedUnread ( ) ) ;
}
public void applyStorageSyncUpdate ( @NonNull RecipientId recipientId , @NonNull SignalGroupV2Record record ) {
applyStorageSyncUpdate ( recipientId , record . isArchived ( ) , record . isForcedUnread ( ) ) ;
}
public void applyStorageSyncUpdate ( @NonNull RecipientId recipientId , @NonNull SignalAccountRecord record ) {
2020-10-13 01:06:42 +00:00
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . beginTransaction ( ) ;
try {
applyStorageSyncUpdate ( recipientId , record . isNoteToSelfArchived ( ) , record . isNoteToSelfForcedUnread ( ) ) ;
ContentValues clearPinnedValues = new ContentValues ( ) ;
clearPinnedValues . put ( PINNED , 0 ) ;
db . update ( TABLE_NAME , clearPinnedValues , null , null ) ;
int pinnedPosition = 1 ;
for ( SignalAccountRecord . PinnedConversation pinned : record . getPinnedConversations ( ) ) {
ContentValues pinnedValues = new ContentValues ( ) ;
pinnedValues . put ( PINNED , pinnedPosition ) ;
Recipient pinnedRecipient ;
if ( pinned . getContact ( ) . isPresent ( ) ) {
pinnedRecipient = Recipient . externalPush ( context , pinned . getContact ( ) . get ( ) ) ;
} else if ( pinned . getGroupV1Id ( ) . isPresent ( ) ) {
try {
2020-11-25 18:21:33 +00:00
pinnedRecipient = Recipient . externalGroupExact ( context , GroupId . v1 ( pinned . getGroupV1Id ( ) . get ( ) ) ) ;
2020-10-13 01:06:42 +00:00
} catch ( BadGroupIdException e ) {
Log . w ( TAG , "Failed to parse pinned groupV1 ID!" , e ) ;
pinnedRecipient = null ;
}
} else if ( pinned . getGroupV2MasterKey ( ) . isPresent ( ) ) {
try {
2020-10-15 19:49:09 +00:00
pinnedRecipient = Recipient . externalGroupExact ( context , GroupId . v2 ( new GroupMasterKey ( pinned . getGroupV2MasterKey ( ) . get ( ) ) ) ) ;
2020-10-13 01:06:42 +00:00
} catch ( InvalidInputException e ) {
Log . w ( TAG , "Failed to parse pinned groupV2 master key!" , e ) ;
pinnedRecipient = null ;
}
} else {
Log . w ( TAG , "Empty pinned conversation on the AccountRecord?" ) ;
pinnedRecipient = null ;
}
if ( pinnedRecipient ! = null ) {
db . update ( TABLE_NAME , pinnedValues , RECIPIENT_ID + " = ?" , SqlUtil . buildArgs ( pinnedRecipient . getId ( ) ) ) ;
}
pinnedPosition + + ;
}
db . setTransactionSuccessful ( ) ;
} finally {
db . endTransaction ( ) ;
}
notifyConversationListListeners ( ) ;
2020-10-07 17:32:59 +00:00
}
private void applyStorageSyncUpdate ( @NonNull RecipientId recipientId , boolean archived , boolean forcedUnread ) {
ContentValues values = new ContentValues ( ) ;
values . put ( ARCHIVED , archived ) ;
if ( forcedUnread ) {
values . put ( READ , ReadStatus . FORCED_UNREAD . serialize ( ) ) ;
} else {
Long threadId = getThreadIdFor ( recipientId ) ;
if ( threadId ! = null ) {
int unreadCount = DatabaseFactory . getMmsSmsDatabase ( context ) . getUnreadCount ( threadId ) ;
values . put ( READ , unreadCount = = 0 ? ReadStatus . READ . serialize ( ) : ReadStatus . UNREAD . serialize ( ) ) ;
values . put ( UNREAD_COUNT , unreadCount ) ;
}
}
databaseHelper . getWritableDatabase ( ) . update ( TABLE_NAME , values , RECIPIENT_ID + " = ?" , SqlUtil . buildArgs ( recipientId ) ) ;
2016-02-20 01:07:41 +00:00
}
2015-11-23 23:07:41 +00:00
public boolean update ( long threadId , boolean unarchive ) {
2020-08-12 15:08:48 +00:00
return update ( threadId , unarchive , true ) ;
2020-07-15 22:03:18 +00:00
}
2020-08-12 15:08:48 +00:00
public boolean update ( long threadId , boolean unarchive , boolean allowDeletion ) {
2011-12-20 18:20:44 +00:00
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory . getMmsSmsDatabase ( context ) ;
2020-08-13 14:01:54 +00:00
long count = mmsSmsDatabase . getConversationCountForThreadSummary ( threadId ) ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
if ( count = = 0 ) {
2020-07-15 22:03:18 +00:00
if ( allowDeletion ) {
2021-02-24 03:59:53 +00:00
deleteConversation ( threadId ) ;
2020-07-15 22:03:18 +00:00
}
2015-03-31 20:36:04 +00:00
return true ;
2011-12-20 18:20:44 +00:00
}
2012-09-09 23:10:46 +00:00
2013-04-26 18:23:43 +00:00
MmsSmsDatabase . Reader reader = null ;
2012-09-09 23:10:46 +00:00
2011-12-20 18:20:44 +00:00
try {
2014-06-12 15:59:54 +00:00
reader = mmsSmsDatabase . readerFor ( mmsSmsDatabase . getConversationSnippet ( threadId ) ) ;
MessageRecord record ;
2013-04-26 18:23:43 +00:00
if ( reader ! = null & & ( record = reader . getNext ( ) ) ! = null ) {
2020-06-04 16:13:42 +00:00
updateThread ( threadId , count , ThreadBodyUtil . getFormattedBodyFor ( context , record ) , getAttachmentUriFor ( record ) ,
2019-06-11 06:18:45 +00:00
getContentTypeFor ( record ) , getExtrasFor ( record ) ,
2017-09-16 05:38:53 +00:00
record . getTimestamp ( ) , record . getDeliveryStatus ( ) , record . getDeliveryReceiptCount ( ) ,
2020-08-12 15:08:48 +00:00
record . getType ( ) , unarchive , record . getExpiresIn ( ) , record . getReadReceiptCount ( ) ) ;
2015-03-31 20:36:04 +00:00
notifyConversationListListeners ( ) ;
return false ;
2012-10-01 02:56:29 +00:00
} else {
2021-02-24 03:59:53 +00:00
deleteConversation ( threadId ) ;
2015-03-31 20:36:04 +00:00
return true ;
2012-10-01 02:56:29 +00:00
}
2011-12-20 18:20:44 +00:00
} finally {
2013-04-26 18:23:43 +00:00
if ( reader ! = null )
reader . close ( ) ;
2011-12-20 18:20:44 +00:00
}
}
2013-01-10 05:06:56 +00:00
2020-11-17 13:58:28 +00:00
public @NonNull ThreadRecord getThreadRecordFor ( @NonNull Recipient recipient ) {
return Objects . requireNonNull ( getThreadRecord ( getThreadIdFor ( recipient ) ) ) ;
}
2021-01-12 03:33:11 +00:00
public @NonNull Set < RecipientId > getAllThreadRecipients ( ) {
SQLiteDatabase db = databaseHelper . getReadableDatabase ( ) ;
Set < RecipientId > ids = new HashSet < > ( ) ;
try ( Cursor cursor = db . query ( TABLE_NAME , new String [ ] { RECIPIENT_ID } , null , null , null , null , null ) ) {
while ( cursor . moveToNext ( ) ) {
ids . add ( RecipientId . from ( CursorUtil . requireString ( cursor , RECIPIENT_ID ) ) ) ;
}
}
return ids ;
}
2020-07-15 22:03:18 +00:00
@NonNull MergeResult merge ( @NonNull RecipientId primaryRecipientId , @NonNull RecipientId secondaryRecipientId ) {
if ( ! databaseHelper . getWritableDatabase ( ) . inTransaction ( ) ) {
throw new IllegalStateException ( "Must be in a transaction!" ) ;
}
Log . w ( TAG , "Merging threads. Primary: " + primaryRecipientId + ", Secondary: " + secondaryRecipientId ) ;
ThreadRecord primary = getThreadRecord ( getThreadIdFor ( primaryRecipientId ) ) ;
ThreadRecord secondary = getThreadRecord ( getThreadIdFor ( secondaryRecipientId ) ) ;
if ( primary ! = null & & secondary = = null ) {
Log . w ( TAG , "[merge] Only had a thread for primary. Returning that." ) ;
2020-08-12 15:12:01 +00:00
return new MergeResult ( primary . getThreadId ( ) , - 1 , false ) ;
2020-07-15 22:03:18 +00:00
} else if ( primary = = null & & secondary ! = null ) {
Log . w ( TAG , "[merge] Only had a thread for secondary. Updating it to have the recipientId of the primary." ) ;
ContentValues values = new ContentValues ( ) ;
values . put ( RECIPIENT_ID , primaryRecipientId . serialize ( ) ) ;
databaseHelper . getWritableDatabase ( ) . update ( TABLE_NAME , values , ID_WHERE , SqlUtil . buildArgs ( secondary . getThreadId ( ) ) ) ;
2020-08-12 15:12:01 +00:00
return new MergeResult ( secondary . getThreadId ( ) , - 1 , false ) ;
2020-07-15 22:03:18 +00:00
} else if ( primary = = null & & secondary = = null ) {
Log . w ( TAG , "[merge] No thread for either." ) ;
2020-08-12 15:12:01 +00:00
return new MergeResult ( - 1 , - 1 , false ) ;
2020-07-15 22:03:18 +00:00
} else {
Log . w ( TAG , "[merge] Had a thread for both. Deleting the secondary and merging the attributes together." ) ;
SQLiteDatabase db = databaseHelper . getWritableDatabase ( ) ;
db . delete ( TABLE_NAME , ID_WHERE , SqlUtil . buildArgs ( secondary . getThreadId ( ) ) ) ;
if ( primary . getExpiresIn ( ) ! = secondary . getExpiresIn ( ) ) {
ContentValues values = new ContentValues ( ) ;
if ( primary . getExpiresIn ( ) = = 0 ) {
values . put ( EXPIRES_IN , secondary . getExpiresIn ( ) ) ;
} else if ( secondary . getExpiresIn ( ) = = 0 ) {
values . put ( EXPIRES_IN , primary . getExpiresIn ( ) ) ;
} else {
values . put ( EXPIRES_IN , Math . min ( primary . getExpiresIn ( ) , secondary . getExpiresIn ( ) ) ) ;
}
db . update ( TABLE_NAME , values , ID_WHERE , SqlUtil . buildArgs ( primary . getThreadId ( ) ) ) ;
}
ContentValues draftValues = new ContentValues ( ) ;
draftValues . put ( DraftDatabase . THREAD_ID , primary . getThreadId ( ) ) ;
db . update ( DraftDatabase . TABLE_NAME , draftValues , DraftDatabase . THREAD_ID + " = ?" , SqlUtil . buildArgs ( secondary . getThreadId ( ) ) ) ;
ContentValues searchValues = new ContentValues ( ) ;
searchValues . put ( SearchDatabase . THREAD_ID , primary . getThreadId ( ) ) ;
db . update ( SearchDatabase . SMS_FTS_TABLE_NAME , searchValues , SearchDatabase . THREAD_ID + " = ?" , SqlUtil . buildArgs ( secondary . getThreadId ( ) ) ) ;
db . update ( SearchDatabase . MMS_FTS_TABLE_NAME , searchValues , SearchDatabase . THREAD_ID + " = ?" , SqlUtil . buildArgs ( secondary . getThreadId ( ) ) ) ;
RemappedRecords . getInstance ( ) . addThread ( context , secondary . getThreadId ( ) , primary . getThreadId ( ) ) ;
2020-08-12 15:12:01 +00:00
return new MergeResult ( primary . getThreadId ( ) , secondary . getThreadId ( ) , true ) ;
2020-07-15 22:03:18 +00:00
}
}
private @Nullable ThreadRecord getThreadRecord ( @Nullable Long threadId ) {
if ( threadId = = null ) {
return null ;
}
String query = createQuery ( TABLE_NAME + "." + ID + " = ?" , 1 ) ;
try ( Cursor cursor = databaseHelper . getReadableDatabase ( ) . rawQuery ( query , SqlUtil . buildArgs ( threadId ) ) ) {
if ( cursor ! = null & & cursor . moveToFirst ( ) ) {
return readerFor ( cursor ) . getCurrent ( ) ;
}
}
return null ;
}
2015-10-16 20:59:40 +00:00
private @Nullable Uri getAttachmentUriFor ( MessageRecord record ) {
2015-11-04 23:39:01 +00:00
if ( ! record . isMms ( ) | | record . isMmsNotification ( ) | | record . isGroupAction ( ) ) return null ;
2015-10-16 20:59:40 +00:00
SlideDeck slideDeck = ( ( MediaMmsMessageRecord ) record ) . getSlideDeck ( ) ;
2019-12-13 06:23:32 +00:00
Slide thumbnail = Optional . fromNullable ( slideDeck . getThumbnailSlide ( ) ) . or ( Optional . fromNullable ( slideDeck . getStickerSlide ( ) ) ) . orNull ( ) ;
2015-10-16 20:59:40 +00:00
2019-07-31 23:33:56 +00:00
if ( thumbnail ! = null & & ! ( ( MmsMessageRecord ) record ) . isViewOnce ( ) ) {
2020-09-15 13:27:34 +00:00
return thumbnail . getUri ( ) ;
2019-04-17 14:21:30 +00:00
}
return null ;
2015-10-16 20:59:40 +00:00
}
2019-06-11 06:18:45 +00:00
private @Nullable String getContentTypeFor ( MessageRecord record ) {
if ( record . isMms ( ) ) {
SlideDeck slideDeck = ( ( MmsMessageRecord ) record ) . getSlideDeck ( ) ;
if ( slideDeck . getSlides ( ) . size ( ) > 0 ) {
return slideDeck . getSlides ( ) . get ( 0 ) . getContentType ( ) ;
}
}
return null ;
}
2021-01-28 15:39:28 +00:00
private @Nullable Extra getExtrasFor ( @NonNull MessageRecord record ) {
2020-02-21 18:52:27 +00:00
boolean messageRequestAccepted = RecipientUtil . isMessageRequestAccepted ( context , record . getThreadId ( ) ) ;
2020-02-19 22:08:34 +00:00
RecipientId threadRecipientId = getRecipientIdForThreadId ( record . getThreadId ( ) ) ;
2021-01-27 15:53:31 +00:00
RecipientId individualRecipient = record . getIndividualRecipient ( ) . getId ( ) ;
2020-02-19 22:08:34 +00:00
if ( ! messageRequestAccepted & & threadRecipientId ! = null ) {
2020-05-12 18:09:47 +00:00
Recipient resolved = Recipient . resolved ( threadRecipientId ) ;
if ( resolved . isPushGroup ( ) ) {
if ( resolved . isPushV2Group ( ) ) {
2020-09-05 13:09:31 +00:00
MessageRecord . InviteAddState inviteAddState = record . getGv2AddInviteState ( ) ;
if ( inviteAddState ! = null ) {
RecipientId from = RecipientId . from ( inviteAddState . getAddedOrInvitedBy ( ) , null ) ;
if ( inviteAddState . isInvited ( ) ) {
Log . i ( TAG , "GV2 invite message request from " + from ) ;
2021-01-27 15:53:31 +00:00
return Extra . forGroupV2invite ( from , individualRecipient ) ;
2020-09-05 13:09:31 +00:00
} else {
Log . i ( TAG , "GV2 message request from " + from ) ;
2021-01-27 15:53:31 +00:00
return Extra . forGroupMessageRequest ( from , individualRecipient ) ;
2020-07-30 20:17:23 +00:00
}
}
2020-09-05 13:09:31 +00:00
Log . w ( TAG , "Falling back to unknown message request state for GV2 message" ) ;
2021-01-27 15:53:31 +00:00
return Extra . forMessageRequest ( individualRecipient ) ;
2020-05-12 18:09:47 +00:00
} else {
RecipientId recipientId = DatabaseFactory . getMmsSmsDatabase ( context ) . getGroupAddedBy ( record . getThreadId ( ) ) ;
if ( recipientId ! = null ) {
2021-01-27 15:53:31 +00:00
return Extra . forGroupMessageRequest ( recipientId , individualRecipient ) ;
2020-05-12 18:09:47 +00:00
}
2020-02-19 22:08:34 +00:00
}
}
2021-01-27 15:53:31 +00:00
return Extra . forMessageRequest ( individualRecipient ) ;
2020-02-19 22:08:34 +00:00
}
2020-10-08 18:04:00 +00:00
if ( record . isRemoteDelete ( ) ) {
2021-01-27 15:53:31 +00:00
return Extra . forRemoteDelete ( individualRecipient ) ;
2020-10-08 18:04:00 +00:00
} else if ( record . isViewOnce ( ) ) {
2021-01-27 15:53:31 +00:00
return Extra . forViewOnce ( individualRecipient ) ;
2019-06-11 06:18:45 +00:00
} else if ( record . isMms ( ) & & ( ( MmsMessageRecord ) record ) . getSlideDeck ( ) . getStickerSlide ( ) ! = null ) {
2020-09-03 14:02:33 +00:00
StickerSlide slide = Objects . requireNonNull ( ( ( MmsMessageRecord ) record ) . getSlideDeck ( ) . getStickerSlide ( ) ) ;
2021-01-27 15:53:31 +00:00
return Extra . forSticker ( slide . getEmoji ( ) , individualRecipient ) ;
2019-06-11 06:18:45 +00:00
} else if ( record . isMms ( ) & & ( ( MmsMessageRecord ) record ) . getSlideDeck ( ) . getSlides ( ) . size ( ) > 1 ) {
2021-01-27 15:53:31 +00:00
return Extra . forAlbum ( individualRecipient ) ;
}
if ( threadRecipientId ! = null ) {
Recipient resolved = Recipient . resolved ( threadRecipientId ) ;
if ( resolved . isGroup ( ) ) {
return Extra . forDefault ( individualRecipient ) ;
}
2019-06-11 06:18:45 +00:00
}
2020-02-19 22:08:34 +00:00
2019-06-11 06:18:45 +00:00
return null ;
}
2020-06-12 18:11:36 +00:00
private @NonNull String createQuery ( @NonNull String where , long limit ) {
2020-08-17 15:12:00 +00:00
return createQuery ( where , 0 , limit , false ) ;
2020-06-12 18:11:36 +00:00
}
2020-08-17 15:12:00 +00:00
private @NonNull String createQuery ( @NonNull String where , long offset , long limit , boolean preferPinned ) {
String orderBy = ( preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "" ) + TABLE_NAME + "." + DATE + " DESC" ;
2020-08-26 14:13:01 +00:00
2020-10-15 19:49:09 +00:00
return createQuery ( where , orderBy , offset , limit ) ;
2020-08-26 14:13:01 +00:00
}
2020-10-15 19:49:09 +00:00
private @NonNull String createQuery ( @NonNull String where , @NonNull String orderBy , long offset , long limit ) {
2017-09-15 17:36:33 +00:00
String projection = Util . join ( COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION , "," ) ;
2020-08-17 15:12:00 +00:00
2017-11-16 23:21:46 +00:00
String query =
"SELECT " + projection + " FROM " + TABLE_NAME +
2017-09-15 17:36:33 +00:00
" LEFT OUTER JOIN " + RecipientDatabase . TABLE_NAME +
2019-08-07 18:22:51 +00:00
" ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + RecipientDatabase . TABLE_NAME + "." + RecipientDatabase . ID +
2017-09-15 17:36:33 +00:00
" LEFT OUTER JOIN " + GroupDatabase . TABLE_NAME +
2019-08-07 18:22:51 +00:00
" ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + GroupDatabase . TABLE_NAME + "." + GroupDatabase . RECIPIENT_ID +
2017-09-15 17:36:33 +00:00
" WHERE " + where +
2020-08-17 15:12:00 +00:00
" ORDER BY " + orderBy ;
2017-11-16 23:21:46 +00:00
if ( limit > 0 ) {
query + = " LIMIT " + limit ;
}
2020-06-12 18:11:36 +00:00
if ( offset > 0 ) {
query + = " OFFSET " + offset ;
}
2017-11-16 23:21:46 +00:00
return query ;
2017-09-15 17:36:33 +00:00
}
2020-11-12 14:52:21 +00:00
private boolean isSilentType ( long type ) {
return MmsSmsColumns . Types . isProfileChange ( type ) | |
MmsSmsColumns . Types . isGroupV1MigrationEvent ( type ) ;
}
2018-01-25 03:17:44 +00:00
public Reader readerFor ( Cursor cursor ) {
return new Reader ( 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
}
2013-04-26 01:59:49 +00:00
public static class DistributionTypes {
public static final int DEFAULT = 2 ;
public static final int BROADCAST = 1 ;
public static final int CONVERSATION = 2 ;
2015-11-23 23:07:41 +00:00
public static final int ARCHIVE = 3 ;
2017-11-13 17:11:58 +00:00
public static final int INBOX_ZERO = 4 ;
2013-04-26 01:59:49 +00:00
}
2020-08-14 19:50:23 +00:00
public class Reader extends StaticReader {
public Reader ( Cursor cursor ) {
super ( cursor , context ) ;
}
}
public static class StaticReader 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-14 19:50:23 +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
2020-08-14 19:50:23 +00:00
public StaticReader ( Cursor cursor , Context context ) {
this . cursor = cursor ;
this . 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 ThreadRecord getNext ( ) {
if ( cursor = = null | | ! cursor . moveToNext ( ) )
return null ;
return getCurrent ( ) ;
}
public ThreadRecord getCurrent ( ) {
2020-07-02 15:19:52 +00:00
RecipientId recipientId = RecipientId . from ( CursorUtil . requireLong ( cursor , ThreadDatabase . RECIPIENT_ID ) ) ;
2021-01-05 19:18:19 +00:00
RecipientSettings recipientSettings = RecipientDatabase . getRecipientSettings ( context , cursor , ThreadDatabase . RECIPIENT_ID ) ;
2020-07-02 15:19:52 +00:00
Recipient recipient ;
if ( recipientSettings . getGroupId ( ) ! = null ) {
GroupDatabase . GroupRecord group = new GroupDatabase . Reader ( cursor ) . getCurrent ( ) ;
if ( group ! = null ) {
RecipientDetails details = new RecipientDetails ( group . getTitle ( ) ,
group . hasAvatar ( ) ? Optional . of ( group . getAvatarId ( ) ) : Optional . absent ( ) ,
false ,
false ,
recipientSettings ,
null ) ;
recipient = new Recipient ( recipientId , details , false ) ;
} else {
recipient = Recipient . live ( recipientId ) . get ( ) ;
}
} else {
RecipientDetails details = RecipientDetails . forIndividual ( context , recipientSettings ) ;
2021-01-05 00:27:16 +00:00
recipient = new Recipient ( recipientId , details , true ) ;
2020-07-02 15:19:52 +00:00
}
2020-05-13 21:30:53 +00:00
int readReceiptCount = TextSecurePreferences . isReadReceiptsEnabled ( context ) ? cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . READ_RECEIPT_COUNT ) )
: 0 ;
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-05-13 21:30:53 +00:00
String extraString = cursor . getString ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET_EXTRAS ) ) ;
Extra extra = null ;
2019-06-11 06:18:45 +00:00
if ( extraString ! = null ) {
try {
extra = JsonUtils . fromJson ( extraString , Extra . class ) ;
} catch ( IOException e ) {
Log . w ( TAG , "Failed to decode extras!" ) ;
}
}
2020-05-13 21:30:53 +00:00
return new ThreadRecord . Builder ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . ID ) ) )
. setRecipient ( recipient )
. setType ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET_TYPE ) ) )
. setDistributionType ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . TYPE ) ) )
2020-06-26 14:46:44 +00:00
. setBody ( Util . emptyIfNull ( cursor . getString ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET ) ) ) )
2020-05-13 21:30:53 +00:00
. setDate ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . DATE ) ) )
2021-02-24 20:07:56 +00:00
. setArchived ( CursorUtil . requireInt ( cursor , ThreadDatabase . ARCHIVED ) ! = 0 )
2020-05-13 21:30:53 +00:00
. setDeliveryStatus ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . STATUS ) ) )
. setDeliveryReceiptCount ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . DELIVERY_RECEIPT_COUNT ) ) )
. setReadReceiptCount ( readReceiptCount )
. setExpiresIn ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . EXPIRES_IN ) ) )
. setLastSeen ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . LAST_SEEN ) ) )
. setSnippetUri ( getSnippetUri ( cursor ) )
. setContentType ( cursor . getString ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET_CONTENT_TYPE ) ) )
. setCount ( cursor . getLong ( cursor . getColumnIndexOrThrow ( ThreadDatabase . MESSAGE_COUNT ) ) )
. setUnreadCount ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . UNREAD_COUNT ) ) )
2020-05-28 13:27:00 +00:00
. setForcedUnread ( cursor . getInt ( cursor . getColumnIndexOrThrow ( ThreadDatabase . READ ) ) = = ReadStatus . FORCED_UNREAD . serialize ( ) )
2020-08-13 12:31:55 +00:00
. setPinned ( CursorUtil . requireBoolean ( cursor , ThreadDatabase . PINNED ) )
2020-05-13 21:30:53 +00:00
. setExtra ( extra )
. build ( ) ;
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
}
2015-10-16 20:59:40 +00:00
private @Nullable Uri getSnippetUri ( Cursor cursor ) {
if ( cursor . isNull ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET_URI ) ) ) {
return null ;
}
try {
return Uri . parse ( cursor . getString ( cursor . getColumnIndexOrThrow ( ThreadDatabase . SNIPPET_URI ) ) ) ;
} catch ( IllegalArgumentException e ) {
Log . w ( TAG , e ) ;
return null ;
}
}
2018-04-07 01:15:24 +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 ( ) {
2018-04-07 01:15:24 +00:00
if ( cursor ! = null ) {
cursor . close ( ) ;
}
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
}
}
2019-06-11 06:18:45 +00:00
public static final class Extra {
@JsonProperty private final boolean isRevealable ;
@JsonProperty private final boolean isSticker ;
2020-09-03 14:02:33 +00:00
@JsonProperty private final String stickerEmoji ;
2019-06-11 06:18:45 +00:00
@JsonProperty private final boolean isAlbum ;
2020-04-15 18:56:58 +00:00
@JsonProperty private final boolean isRemoteDelete ;
2020-02-19 22:08:34 +00:00
@JsonProperty private final boolean isMessageRequestAccepted ;
2020-05-12 18:09:47 +00:00
@JsonProperty private final boolean isGv2Invite ;
2020-02-19 22:08:34 +00:00
@JsonProperty private final String groupAddedBy ;
2021-01-27 15:53:31 +00:00
@JsonProperty private final String individualRecipientId ;
2019-06-11 06:18:45 +00:00
public Extra ( @JsonProperty ( "isRevealable" ) boolean isRevealable ,
@JsonProperty ( "isSticker" ) boolean isSticker ,
2020-09-03 14:02:33 +00:00
@JsonProperty ( "stickerEmoji" ) String stickerEmoji ,
2020-02-19 22:08:34 +00:00
@JsonProperty ( "isAlbum" ) boolean isAlbum ,
2020-04-15 18:56:58 +00:00
@JsonProperty ( "isRemoteDelete" ) boolean isRemoteDelete ,
2020-02-19 22:08:34 +00:00
@JsonProperty ( "isMessageRequestAccepted" ) boolean isMessageRequestAccepted ,
2020-05-12 18:09:47 +00:00
@JsonProperty ( "isGv2Invite" ) boolean isGv2Invite ,
2021-01-27 15:53:31 +00:00
@JsonProperty ( "groupAddedBy" ) String groupAddedBy ,
@JsonProperty ( "individualRecipientId" ) String individualRecipientId )
2019-06-11 06:18:45 +00:00
{
2020-02-19 22:08:34 +00:00
this . isRevealable = isRevealable ;
this . isSticker = isSticker ;
2020-09-03 14:02:33 +00:00
this . stickerEmoji = stickerEmoji ;
2020-02-19 22:08:34 +00:00
this . isAlbum = isAlbum ;
2020-04-15 18:56:58 +00:00
this . isRemoteDelete = isRemoteDelete ;
2020-02-19 22:08:34 +00:00
this . isMessageRequestAccepted = isMessageRequestAccepted ;
2020-05-12 18:09:47 +00:00
this . isGv2Invite = isGv2Invite ;
2020-02-19 22:08:34 +00:00
this . groupAddedBy = groupAddedBy ;
2021-01-27 15:53:31 +00:00
this . individualRecipientId = individualRecipientId ;
2019-06-11 06:18:45 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forViewOnce ( @NonNull RecipientId individualRecipient ) {
return new Extra ( true , false , null , false , false , true , false , null , individualRecipient . serialize ( ) ) ;
2019-06-11 06:18:45 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forSticker ( @Nullable String emoji , @NonNull RecipientId individualRecipient ) {
return new Extra ( false , true , emoji , false , false , true , false , null , individualRecipient . serialize ( ) ) ;
2019-06-11 06:18:45 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forAlbum ( @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , true , false , true , false , null , individualRecipient . serialize ( ) ) ;
2020-04-15 18:56:58 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forRemoteDelete ( @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , false , true , true , false , null , individualRecipient . serialize ( ) ) ;
2019-06-11 06:18:45 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forMessageRequest ( @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , false , false , false , false , null , individualRecipient . serialize ( ) ) ;
2020-02-19 22:08:34 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forGroupMessageRequest ( @NonNull RecipientId recipientId , @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , false , false , false , false , recipientId . serialize ( ) , individualRecipient . serialize ( ) ) ;
2020-05-12 18:09:47 +00:00
}
2021-01-27 15:53:31 +00:00
public static @NonNull Extra forGroupV2invite ( @NonNull RecipientId recipientId , @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , false , false , false , true , recipientId . serialize ( ) , individualRecipient . serialize ( ) ) ;
}
public static @NonNull Extra forDefault ( @NonNull RecipientId individualRecipient ) {
return new Extra ( false , false , null , false , false , true , false , null , individualRecipient . serialize ( ) ) ;
2020-02-19 22:08:34 +00:00
}
2019-06-11 06:18:45 +00:00
2020-04-15 18:56:58 +00:00
public boolean isViewOnce ( ) {
2019-06-11 06:18:45 +00:00
return isRevealable ;
}
public boolean isSticker ( ) {
return isSticker ;
}
2020-09-03 14:02:33 +00:00
public @Nullable String getStickerEmoji ( ) {
return stickerEmoji ;
}
2019-06-11 06:18:45 +00:00
public boolean isAlbum ( ) {
return isAlbum ;
}
2020-02-19 22:08:34 +00:00
2020-04-15 18:56:58 +00:00
public boolean isRemoteDelete ( ) {
return isRemoteDelete ;
}
2020-02-19 22:08:34 +00:00
public boolean isMessageRequestAccepted ( ) {
return isMessageRequestAccepted ;
}
2020-05-12 18:09:47 +00:00
public boolean isGv2Invite ( ) {
return isGv2Invite ;
}
2020-02-19 22:08:34 +00:00
public @Nullable String getGroupAddedBy ( ) {
return groupAddedBy ;
}
2021-01-27 15:53:31 +00:00
public @Nullable String getIndividualRecipientId ( ) {
return individualRecipientId ;
}
2019-06-11 06:18:45 +00:00
}
2020-05-28 13:27:00 +00:00
2020-10-07 17:32:59 +00:00
enum ReadStatus {
2020-05-28 13:27:00 +00:00
READ ( 1 ) , UNREAD ( 0 ) , FORCED_UNREAD ( 2 ) ;
private final int value ;
ReadStatus ( int value ) {
this . value = value ;
}
public static ReadStatus deserialize ( int value ) {
for ( ReadStatus status : ReadStatus . values ( ) ) {
if ( status . value = = value ) {
return status ;
}
}
throw new IllegalArgumentException ( "No matching status for value " + value ) ;
}
public int serialize ( ) {
return value ;
}
}
2020-06-11 20:55:34 +00:00
public static class ConversationMetadata {
private final long lastSeen ;
private final boolean hasSent ;
private final long lastScrolled ;
public ConversationMetadata ( long lastSeen , boolean hasSent , long lastScrolled ) {
this . lastSeen = lastSeen ;
this . hasSent = hasSent ;
this . lastScrolled = lastScrolled ;
}
public long getLastSeen ( ) {
return lastSeen ;
}
public boolean hasSent ( ) {
return hasSent ;
}
public long getLastScrolled ( ) {
return lastScrolled ;
}
}
2020-07-15 22:03:18 +00:00
static final class MergeResult {
final long threadId ;
2020-08-12 15:12:01 +00:00
final long previousThreadId ;
2020-07-15 22:03:18 +00:00
final boolean neededMerge ;
2020-08-12 15:12:01 +00:00
private MergeResult ( long threadId , long previousThreadId , boolean neededMerge ) {
this . threadId = threadId ;
this . previousThreadId = previousThreadId ;
this . neededMerge = neededMerge ;
2020-07-15 22:03:18 +00:00
}
}
2011-12-20 18:20:44 +00:00
}